diff --git a/services/static-webserver/client/source/class/osparc/Application.js b/services/static-webserver/client/source/class/osparc/Application.js index 10136bc0ad7..b73de02cff8 100644 --- a/services/static-webserver/client/source/class/osparc/Application.js +++ b/services/static-webserver/client/source/class/osparc/Application.js @@ -569,7 +569,7 @@ qx.Class.define("osparc.Application", { }, __closeAllAndToLoginPage: function() { - osparc.data.PollTasks.getInstance().removeTasks(); + osparc.store.PollTasks.getInstance().removeTasks(); osparc.MaintenanceTracker.getInstance().stopTracker(); osparc.CookieExpirationTracker.getInstance().stopTracker(); osparc.NewUITracker.getInstance().stopTracker(); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonPlaceholder.js b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonTaskPlaceholder.js similarity index 98% rename from services/static-webserver/client/source/class/osparc/dashboard/GridButtonPlaceholder.js rename to services/static-webserver/client/source/class/osparc/dashboard/GridButtonTaskPlaceholder.js index 4c2c933042b..f3e8a24e718 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonPlaceholder.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonTaskPlaceholder.js @@ -15,7 +15,7 @@ ************************************************************************ */ -qx.Class.define("osparc.dashboard.GridButtonPlaceholder", { +qx.Class.define("osparc.dashboard.GridButtonTaskPlaceholder", { extend: osparc.dashboard.GridButtonBase, construct: function() { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonPlaceholder.js b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonTaskPlaceholder.js similarity index 98% rename from services/static-webserver/client/source/class/osparc/dashboard/ListButtonPlaceholder.js rename to services/static-webserver/client/source/class/osparc/dashboard/ListButtonTaskPlaceholder.js index d813261ef3c..2f6a43e21d5 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonPlaceholder.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonTaskPlaceholder.js @@ -15,7 +15,7 @@ ************************************************************************ */ -qx.Class.define("osparc.dashboard.ListButtonPlaceholder", { +qx.Class.define("osparc.dashboard.ListButtonTaskPlaceholder", { extend: osparc.dashboard.ListButtonBase, construct: function() { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js index 8d36a43bc7b..3bf13ec4a46 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js @@ -177,6 +177,10 @@ qx.Class.define("osparc.dashboard.ResourceBrowserBase", { return (card instanceof osparc.dashboard.GridButtonItem || card instanceof osparc.dashboard.ListButtonItem); }, + isCardTaskPlaceholder: function(card) { + return (card instanceof osparc.dashboard.GridButtonTaskPlaceholder || card instanceof osparc.dashboard.ListButtonTaskPlaceholder); + }, + createToolbarRadioButton: function(label, icon, toolTipText, pos) { const rButton = new qx.ui.toolbar.RadioButton().set({ label, @@ -474,8 +478,26 @@ qx.Class.define("osparc.dashboard.ResourceBrowserBase", { } }, - _taskDataReceived: function(taskData) { - throw new Error("Abstract method called!"); + _addTaskCard: function(task, cardTitle, cardIcon) { + if (task) { + const taskPlaceholders = this._resourcesContainer.getCards().filter(card => osparc.dashboard.ResourceBrowserBase.isCardTaskPlaceholder(card)); + if (taskPlaceholders.find(taskPlaceholder => taskPlaceholder.getTask() === task)) { + return null; + } + } + + const isGrid = this._resourcesContainer.getMode() === "grid"; + const taskCard = isGrid ? new osparc.dashboard.GridButtonTaskPlaceholder() : new osparc.dashboard.ListButtonTaskPlaceholder(); + taskCard.setTask(task); + taskCard.buildLayout( + cardTitle, + cardIcon + (isGrid ? "/60" : "/24"), + null, + true + ); + taskCard.subscribeToFilterGroup("searchBarFilter"); + this._resourcesContainer.addNonResourceCard(taskCard); + return taskCard; }, _populateCardMenu: function(card) { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js index 010225e27ef..884dbc1f8ad 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js @@ -251,12 +251,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { return; } - osparc.data.Resources.get("tasks") - .then(tasks => { - if (tasks && tasks.length) { - this.__tasksReceived(tasks); - } - }); + this.__tasksToCards(); this._loadingResourcesBtn.setFetching(true); this._loadingResourcesBtn.setVisibility("visible"); @@ -1889,18 +1884,6 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { return this._resourcesList.find(study => study.uuid === id); }, - __createDuplicateCard: function(studyName) { - const isGrid = this._resourcesContainer.getMode() === "grid"; - const duplicatingStudyCard = isGrid ? new osparc.dashboard.GridButtonPlaceholder() : new osparc.dashboard.ListButtonPlaceholder(); - duplicatingStudyCard.buildLayout( - this.tr("Duplicating ") + studyName, - osparc.task.Duplicate.ICON + (isGrid ? "60" : "24"), - null, - true - ); - return duplicatingStudyCard; - }, - __duplicateStudy: function(studyData) { const text = this.tr("Duplicate process started and added to the background tasks"); osparc.FlashMessenger.logAs(text, "INFO"); @@ -1915,54 +1898,53 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }; const fetchPromise = osparc.data.Resources.fetch("studies", "duplicate", params, options); const interval = 1000; - const pollTasks = osparc.data.PollTasks.getInstance(); + const pollTasks = osparc.store.PollTasks.getInstance(); pollTasks.createPollingTask(fetchPromise, interval) .then(task => this.__taskDuplicateReceived(task, studyData["name"])) .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while duplicating"))); }, __exportStudy: function(studyData) { - const exportTask = new osparc.task.Export(studyData); - exportTask.start(); - exportTask.setSubtitle(this.tr("Preparing files")); + const exportTaskUI = new osparc.task.Export(studyData); + exportTaskUI.setSubtitle(this.tr("Preparing files")); + + osparc.task.TasksContainer.getInstance().addTaskUI(exportTaskUI); + const text = this.tr("Exporting process started and added to the background tasks"); osparc.FlashMessenger.logAs(text, "INFO"); const url = window.location.href + "v0/projects/" + studyData["uuid"] + ":xport"; const progressCB = () => { const textSuccess = this.tr("Download started"); - exportTask.setSubtitle(textSuccess); + exportTaskUI.setSubtitle(textSuccess); }; osparc.utils.Utils.downloadLink(url, "POST", null, progressCB) .catch(err => { const msg = osparc.data.Resources.getErrorMsg(JSON.parse(err.response)) || this.tr("Something went wrong while exporting the study"); osparc.FlashMessenger.logError(err, msg); }) - .finally(() => { - exportTask.stop(); - }); + .finally(() => osparc.task.TasksContainer.getInstance().removeTaskUI(exportTaskUI)); }, __importStudy: function(file) { const uploadingLabel = this.tr("Uploading file"); - const importTask = new osparc.task.Import(); - importTask.start(); - importTask.setSubtitle(uploadingLabel); + const importTaskUI = new osparc.task.Import(); + importTaskUI.setSubtitle(uploadingLabel); + + osparc.task.TasksContainer.getInstance().addTaskUI(importTaskUI); const text = this.tr("Importing process started and added to the background tasks"); osparc.FlashMessenger.logAs(text, "INFO"); - const isGrid = this._resourcesContainer.getMode() === "grid"; - const importingStudyCard = isGrid ? new osparc.dashboard.GridButtonPlaceholder() : new osparc.dashboard.ListButtonPlaceholder(); - importingStudyCard.buildLayout( - this.tr("Importing Study..."), - "@FontAwesome5Solid/cloud-upload-alt/" + (isGrid ? "60" : "24"), - uploadingLabel, - true - ); - importingStudyCard.subscribeToFilterGroup("searchBarFilter"); - this._resourcesContainer.addNonResourceCard(importingStudyCard); + const cardTitle = this.tr("Importing Study..."); + const cardIcon = "@FontAwesome5Solid/cloud-upload-alt"; + const importingStudyCard = this._addTaskCard(null, cardTitle, cardIcon); + if (importingStudyCard) { + this.__attachImportEventHandler(file, importTaskUI, importingStudyCard); + } + }, + __attachImportEventHandler: function(file, importTaskUI, importingStudyCard) { const body = new FormData(); body.append("fileName", file); @@ -1975,7 +1957,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { if (percentComplete === 100) { const processingLabel = this.tr("Processing study"); importingStudyCard.getChildControl("state-label").setValue(processingLabel); - importTask.setSubtitle(processingLabel); + importTaskUI.setSubtitle(processingLabel); importingStudyCard.getChildControl("progress-bar").exclude(); } } else { @@ -1987,7 +1969,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { if (req.status == 200) { const processingLabel = this.tr("Processing study"); importingStudyCard.getChildControl("state-label").setValue(processingLabel); - importTask.setSubtitle(processingLabel); + importTaskUI.setSubtitle(processingLabel); importingStudyCard.getChildControl("progress-bar").exclude(); const data = JSON.parse(req.responseText); const params = { @@ -1999,11 +1981,11 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { .then(studyData => this._updateStudyData(studyData)) .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while fetching the study"))) .finally(() => { - importTask.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(importTaskUI); this._resourcesContainer.removeNonResourceCard(importingStudyCard); }); } else if (req.status == 400) { - importTask.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(importTaskUI); this._resourcesContainer.removeNonResourceCard(importingStudyCard); const msg = osparc.data.Resources.getErrorMsg(JSON.parse(req.response)) || this.tr("Something went wrong while importing the study"); osparc.FlashMessenger.logError(msg); @@ -2011,14 +1993,14 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }); req.addEventListener("error", e => { // transferFailed - importTask.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(importTaskUI); this._resourcesContainer.removeNonResourceCard(importingStudyCard); const msg = osparc.data.Resources.getErrorMsg(e) || this.tr("Something went wrong while importing the study"); osparc.FlashMessenger.logError(msg); }); req.addEventListener("abort", e => { // transferAborted - importTask.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(importTaskUI); this._resourcesContainer.removeNonResourceCard(importingStudyCard); const msg = osparc.data.Resources.getErrorMsg(e) || this.tr("Something went wrong while importing the study"); osparc.FlashMessenger.logError(msg); @@ -2133,34 +2115,25 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }, // TASKS // - __tasksReceived: function(tasks) { - tasks.forEach(taskData => this._taskDataReceived(taskData)); - }, - - _taskDataReceived: function(taskData) { - // a bit hacky - if (taskData["task_id"].includes("from_study") && !taskData["task_id"].includes("as_template")) { - const interval = 1000; - const pollTasks = osparc.data.PollTasks.getInstance(); - const task = pollTasks.addTask(taskData, interval); - if (task === null) { - return; - } - // ask backend for studyData? + __tasksToCards: function() { + const tasks = osparc.store.PollTasks.getInstance().getDuplicateStudyTasks(); + tasks.forEach(task => { const studyName = ""; this.__taskDuplicateReceived(task, studyName); - } + }); }, __taskDuplicateReceived: function(task, studyName) { const duplicateTaskUI = new osparc.task.Duplicate(studyName); duplicateTaskUI.setTask(task); - duplicateTaskUI.start(); - const duplicatingStudyCard = this.__createDuplicateCard(studyName); - duplicatingStudyCard.setTask(task); - duplicatingStudyCard.subscribeToFilterGroup("searchBarFilter"); - this._resourcesContainer.addNonResourceCard(duplicatingStudyCard); - this.__attachDuplicateEventHandler(task, duplicateTaskUI, duplicatingStudyCard); + + osparc.task.TasksContainer.getInstance().addTaskUI(duplicateTaskUI); + + const cardTitle = this.tr("Duplicating ") + studyName; + const duplicatingStudyCard = this._addTaskCard(task, cardTitle, osparc.task.Duplicate.ICON); + if (duplicatingStudyCard) { + this.__attachDuplicateEventHandler(task, duplicateTaskUI, duplicatingStudyCard); + } }, __attachDuplicateEventHandler: function(task, taskUI, duplicatingStudyCard) { @@ -2168,7 +2141,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { if (msg) { osparc.FlashMessenger.logAs(msg, msgLevel); } - taskUI.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(taskUI); this._resourcesContainer.removeNonResourceCard(duplicatingStudyCard); }; diff --git a/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js index c009635307c..675801a3016 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js @@ -75,6 +75,8 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { }, __reloadTemplates: function() { + this.__tasksToCards(); + osparc.data.Resources.getInstance().getAllPages("templates") .then(templates => this.__setResourcesToList(templates)) .catch(err => { @@ -349,7 +351,7 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { if (msg) { osparc.FlashMessenger.logAs(msg, msgLevel); } - taskUI.stop(); + osparc.task.TasksContainer.getInstance().removeTaskUI(taskUI); this._resourcesContainer.removeNonResourceCard(toTemplateCard); }; @@ -380,43 +382,26 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { }); }, - _taskDataReceived: function(taskData) { - // a bit hacky - if (taskData["task_id"].includes("from_study") && taskData["task_id"].includes("as_template")) { - const interval = 1000; - const pollTasks = osparc.data.PollTasks.getInstance(); - const task = pollTasks.addTask(taskData, interval); - if (task === null) { - return; - } - // ask backend for studyData? + __tasksToCards: function() { + const tasks = osparc.store.PollTasks.getInstance().getPublishTemplateTasks(); + tasks.forEach(task => { const studyName = ""; this.taskToTemplateReceived(task, studyName); - } + }); }, taskToTemplateReceived: function(task, studyName) { const toTemplateTaskUI = new osparc.task.ToTemplate(studyName); toTemplateTaskUI.setTask(task); - toTemplateTaskUI.start(); - const toTemplateCard = this.__createToTemplateCard(studyName); - toTemplateCard.setTask(task); - this.__attachToTemplateEventHandler(task, toTemplateTaskUI, toTemplateCard); - }, - __createToTemplateCard: function(studyName) { - const isGrid = this._resourcesContainer.getMode() === "grid"; - const toTemplateCard = isGrid ? new osparc.dashboard.GridButtonPlaceholder() : new osparc.dashboard.ListButtonPlaceholder(); - toTemplateCard.buildLayout( - this.tr("Publishing ") + studyName, - osparc.task.ToTemplate.ICON + (isGrid ? "60" : "24"), - null, - true - ); - toTemplateCard.subscribeToFilterGroup("searchBarFilter"); - this._resourcesContainer.addNonResourceCard(toTemplateCard); - return toTemplateCard; - } + osparc.task.TasksContainer.getInstance().addTaskUI(toTemplateTaskUI); + + const cardTitle = this.tr("Publishing ") + studyName; + const toTemplateCard = this._addTaskCard(task, cardTitle, osparc.task.ToTemplate.ICON); + if (toTemplateCard) { + this.__attachToTemplateEventHandler(task, toTemplateTaskUI, toTemplateCard); + } + }, // TASKS // } }); diff --git a/services/static-webserver/client/source/class/osparc/data/Job.js b/services/static-webserver/client/source/class/osparc/data/Job.js new file mode 100644 index 00000000000..fa8046d3a94 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/Job.js @@ -0,0 +1,84 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.data.Job", { + extend: qx.core.Object, + + construct: function(jobData) { + this.base(arguments); + + this.set({ + jobId: jobData["job_id"], + solver: jobData["solver"], + status: jobData["status"], + progress: jobData["progress"], + submittedAt: jobData["submitted_at"] ? new Date(jobData["submitted_at"]) : null, + startedAt: jobData["started_at"] ? new Date(jobData["started_at"]) : null, + instance: jobData["instance"], + }); + }, + + properties: { + jobId: { + check: "String", + nullable: false, + init: null, + }, + + solver: { + check: "String", + nullable: false, + init: null, + }, + + status: { + check: "String", + nullable: false, + init: null, + }, + + progress: { + check: "Number", + init: null, + nullable: true, + }, + + submittedAt: { + check: "Date", + init: null, + nullable: true, + }, + + startedAt: { + check: "Date", + init: null, + nullable: true, + }, + + instance: { + check: "String", + nullable: false, + init: null, + }, + + info: { + check: "Object", + nullable: false, + init: null, + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/data/PollTask.js b/services/static-webserver/client/source/class/osparc/data/PollTask.js index 8823faf2084..ce9ee8dc707 100644 --- a/services/static-webserver/client/source/class/osparc/data/PollTask.js +++ b/services/static-webserver/client/source/class/osparc/data/PollTask.js @@ -92,9 +92,13 @@ qx.Class.define("osparc.data.PollTask", { statics: { extractPathname: function(href) { - // For the long running tasks, only the pathname is relevant to the frontend - const url = new URL(href); - return url.pathname; + try { + // For the long running tasks, only the pathname is relevant to the frontend + const url = new URL(href); + return url.pathname; + } catch (_) { + return href; + } } }, diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 5689afe6cb9..f48440a43c3 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -553,7 +553,6 @@ qx.Class.define("osparc.data.Resources", { */ "tasks": { useCache: false, - idField: "id", endpoints: { get: { method: "GET", diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js index 7a5463eb95d..28133e59690 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js @@ -68,6 +68,10 @@ qx.Class.define("osparc.desktop.MainPage", { preloadPromises.push(store.getAllClassifiers(true)); preloadPromises.push(osparc.store.Tags.getInstance().fetchTags()); preloadPromises.push(osparc.store.Products.getInstance().fetchUiConfig()); + preloadPromises.push(osparc.store.PollTasks.getInstance().fetchTasks()); + if (osparc.utils.DisabledPlugins.isJobsEnabled()) { + preloadPromises.push(osparc.store.Jobs.getInstance().fetchJobs()); + } Promise.all(preloadPromises) .then(() => { const mainStack = this.__createMainStack(); @@ -232,7 +236,7 @@ qx.Class.define("osparc.desktop.MainPage", { pollTask: true }; const fetchPromise = osparc.data.Resources.fetch("studies", "postToTemplate", params, options); - const pollTasks = osparc.data.PollTasks.getInstance(); + const pollTasks = osparc.store.PollTasks.getInstance(); const interval = 1000; pollTasks.createPollingTask(fetchPromise, interval) .then(task => { diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js index ee935adab67..577ddece319 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js @@ -62,6 +62,8 @@ qx.Class.define("osparc.desktop.MainPageDesktop", { } preloadPromises.push(store.getAllClassifiers(true)); preloadPromises.push(osparc.store.Tags.getInstance().fetchTags()); + preloadPromises.push(osparc.store.Products.getInstance().fetchUiConfig()); + preloadPromises.push(osparc.store.PollTasks.getInstance().fetchTasks()); Promise.all(preloadPromises) .then(() => { const desktopCenter = new osparc.desktop.credits.DesktopCenter(); diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobInfo.js b/services/static-webserver/client/source/class/osparc/jobs/JobInfo.js new file mode 100644 index 00000000000..b05bca077df --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/jobs/JobInfo.js @@ -0,0 +1,59 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.jobs.JobInfo", { + extend: qx.ui.core.Widget, + + construct(jobId) { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox()); + + const jobInfoViewer = this.getChildControl("job-info-viewer"); + osparc.store.Jobs.getInstance().fetchJobInfo(jobId) + .then(info => { + jobInfoViewer.setJson(info); + }); + }, + + statics: { + popUpInWindow: function(jobInfo) { + const title = qx.locale.Manager.tr("Job Info"); + const win = osparc.ui.window.Window.popUpInWindow(jobInfo, title, 600, 400); + win.open(); + return win; + } + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "job-info-viewer": { + control = new osparc.ui.basic.JsonTreeWidget(); + const container = new qx.ui.container.Scroll(); + container.add(control); + this._add(container); + break; + } + } + + return control || this.base(arguments, id); + }, + } +}) diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsBrowser.js b/services/static-webserver/client/source/class/osparc/jobs/JobsBrowser.js new file mode 100644 index 00000000000..bab800f08d6 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsBrowser.js @@ -0,0 +1,85 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.jobs.JobsBrowser", { + extend: qx.ui.core.Widget, + + construct() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + const jobsFilter = this.getChildControl("jobs-filter"); + this.getChildControl("jobs-ongoing"); + const jobsTable = this.getChildControl("jobs-table"); + + jobsFilter.getChildControl("textfield").addListener("input", e => { + const filterText = e.getData(); + jobsTable.getTableModel().setFilters({ + text: filterText, + }); + }); + }, + + statics: { + popUpInWindow: function(jobsBrowser) { + if (!jobsBrowser) { + jobsBrowser = new osparc.jobs.JobsBrowser(); + } + const title = qx.locale.Manager.tr("Jobs"); + const win = osparc.ui.window.Window.popUpInWindow(jobsBrowser, title, 1100, 500); + win.open(); + return win; + } + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "header-filter": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + this._add(control); + break; + case "jobs-filter": + control = new osparc.filter.TextFilter("text", "jobsList").set({ + allowStretchX: true, + margin: 0 + }); + this.getChildControl("header-filter").add(control, { + flex: 1 + }); + break; + case "jobs-ongoing": + control = new qx.ui.form.CheckBox().set({ + label: "Hide finished jobs", + value: true, + enabled: false, + }); + this.getChildControl("header-filter").add(control); + break; + case "jobs-table": + control = new osparc.jobs.JobsTable(); + this._add(control); + break; + } + + return control || this.base(arguments, id); + }, + } +}) diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js new file mode 100644 index 00000000000..0cb4379bab3 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js @@ -0,0 +1,84 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.jobs.JobsButton", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.Canvas()); + + this.set({ + width: 30, + alignX: "center", + cursor: "pointer", + visibility: "excluded", + toolTipText: this.tr("Jobs"), + }); + + const jobsStore = osparc.store.Jobs.getInstance(); + jobsStore.addListener("changeJobs", e => this.__updateJobsButton(), this); + this.addListener("tap", () => osparc.jobs.JobsBrowser.popUpInWindow(), this); + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "icon": { + control = new qx.ui.basic.Image("@FontAwesome5Solid/cog/22"); + osparc.utils.Utils.addClass(control.getContentElement(), "rotateSlow"); + + const logoContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ + alignY: "middle" + })); + logoContainer.add(control); + + this._add(logoContainer, { + height: "100%" + }); + break; + } + case "number": + control = new qx.ui.basic.Label().set({ + backgroundColor: "background-main-1", + font: "text-12" + }); + control.getContentElement().setStyles({ + "border-radius": "4px" + }); + this._add(control, { + bottom: 8, + right: 4 + }); + break; + } + return control || this.base(arguments, id); + }, + + __updateJobsButton: function() { + this._createChildControlImpl("icon"); + const number = this.getChildControl("number"); + + const jobsStore = osparc.store.Jobs.getInstance(); + const nJobs = jobsStore.getJobs().length; + number.setValue(nJobs.toString()); + nJobs ? this.show() : this.exclude(); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsTable.js b/services/static-webserver/client/source/class/osparc/jobs/JobsTable.js new file mode 100644 index 00000000000..615e305706e --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsTable.js @@ -0,0 +1,163 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.jobs.JobsTable", { + extend: qx.ui.table.Table, + + construct: function(filters) { + this.base(arguments); + + const model = new osparc.jobs.JobsTableModel(filters); + this.setTableModel(model); + + this.set({ + statusBarVisible: false, + headerCellHeight: 26, + rowHeight: 26, + }); + + const columnModel = this.getTableColumnModel(); + columnModel.setColumnVisible(this.self().COLS.JOB_ID.column, true); + + Object.values(this.self().COLS).forEach(col => columnModel.setColumnWidth(col.column, col.width)); + + const iconPathInfo = "osparc/circle-info-text.svg"; + const fontButtonRendererInfo = new osparc.ui.table.cellrenderer.ImageButtonRenderer("info", iconPathInfo); + columnModel.setDataCellRenderer(this.self().COLS.INFO.column, fontButtonRendererInfo); + + const iconPathStop = "osparc/circle-stop-text.svg"; + const fontButtonRendererStop = new osparc.ui.table.cellrenderer.ImageButtonRenderer("stop", iconPathStop); + columnModel.setDataCellRenderer(this.self().COLS.ACTION_STOP.column, fontButtonRendererStop); + + const iconPathDelete = "osparc/trash-text.svg"; + const fontButtonRendererDelete = new osparc.ui.table.cellrenderer.ImageButtonRenderer("delete", iconPathDelete); + columnModel.setDataCellRenderer(this.self().COLS.ACTION_DELETE.column, fontButtonRendererDelete); + + const iconPathLogs = "osparc/logs-text.svg"; + const fontButtonRendererLogs = new osparc.ui.table.cellrenderer.ImageButtonRenderer("logs", iconPathLogs); + columnModel.setDataCellRenderer(this.self().COLS.ACTION_LOGS.column, fontButtonRendererLogs); + + this.__attachHandlers(); + }, + + statics: { + COLS: { + JOB_ID: { + id: "jobId", + column: 0, + label: qx.locale.Manager.tr("Job Id"), + width: 170 + }, + SOLVER: { + id: "solver", + column: 1, + label: qx.locale.Manager.tr("Solver"), + width: 100 + }, + STATUS: { + id: "status", + column: 2, + label: qx.locale.Manager.tr("Status"), + width: 170 + }, + PROGRESS: { + id: "progress", + column: 3, + label: qx.locale.Manager.tr("Progress"), + width: 80 + }, + SUBMIT: { + id: "submit", + column: 4, + label: qx.locale.Manager.tr("Submitted"), + width: 130 + }, + START: { + id: "start", + column: 5, + label: qx.locale.Manager.tr("Started"), + width: 130 + }, + INFO: { + id: "info", + column: 6, + label: qx.locale.Manager.tr("Info"), + width: 40 + }, + INSTANCE: { + id: "instance", + column: 7, + label: qx.locale.Manager.tr("Instance"), + width: 90 + }, + ACTION_STOP: { + id: "info", + column: 8, + label: "", + width: 40 + }, + ACTION_DELETE: { + id: "info", + column: 9, + label: "", + width: 40 + }, + ACTION_LOGS: { + id: "info", + column: 10, + label: "", + width: 40 + }, + } + }, + + members: { + __attachHandlers: function() { + this.addListener("cellTap", e => { + const row = e.getRow(); + const target = e.getOriginalTarget(); + if (target.closest(".qx-material-button") && (target.tagName === "IMG" || target.tagName === "DIV")) { + const action = target.closest(".qx-material-button").getAttribute("data-action"); + if (action) { + this.__handleButtonClick(action, row); + } + } + }); + }, + + __handleButtonClick: function(action, row) { + const rowData = this.getTableModel().getRowData(row); + switch (action) { + case "info": { + const jobInfo = new osparc.jobs.JobInfo(rowData["jobId"]); + osparc.jobs.JobInfo.popUpInWindow(jobInfo); + break; + } + case "stop": + case "delete": + case "logs": { + const msg = `I wish I could ${action} the job ${rowData["jobId"]}`; + osparc.FlashMessenger.logAs(msg, "WARNING"); + break; + } + default: + console.warn(`Unknown action: ${action}`); + } + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsTableModel.js b/services/static-webserver/client/source/class/osparc/jobs/JobsTableModel.js new file mode 100644 index 00000000000..bfac55fd45e --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsTableModel.js @@ -0,0 +1,194 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.jobs.JobsTableModel", { + extend: qx.ui.table.model.Remote, + + construct(filters) { + this.base(arguments); + + const jobsCols = osparc.jobs.JobsTable.COLS; + const colLabels = Object.values(jobsCols).map(col => col.label); + const colIDs = Object.values(jobsCols).map(col => col.id); + this.setColumns(colLabels, colIDs); + + if (filters) { + this.setFilters(filters); + } + + this.setSortColumnIndexWithoutSortingData(jobsCols.SUBMIT.column); + this.setSortAscendingWithoutSortingData(false); + this.setColumnSortable(jobsCols.INFO.column, false); + this.setColumnSortable(jobsCols.ACTION_STOP.column, false); + this.setColumnSortable(jobsCols.ACTION_DELETE.column, false); + this.setColumnSortable(jobsCols.ACTION_LOGS.column, false); + }, + + properties: { + isFetching: { + check: "Boolean", + init: false, + event: "changeFetching" + }, + + filters: { + check: "Object", + init: null, + apply: "reloadData", // force reload + }, + + orderBy: { + check: "Object", + init: { + field: "started_at", + direction: "desc" + } + }, + }, + + statics: { + SERVER_MAX_LIMIT: 49, + COLUMN_ID_TO_DB_COLUMN_MAP: { + 0: "started_at", + }, + }, + + members: { + // this should be done by the backend + __filterJobs: function(jobs) { + const filters = this.getFilters(); + return jobs.filter(job => { + if (filters) { + let match = false; + [ + "jobId", + "solver", + "status", + "instance", + ].forEach(filterableField => { + const getter = "get" + qx.lang.String.firstUp(filterableField); + const value = job[getter](); + // lowercase both + if (!match && value && value.toLowerCase().includes(filters.text.toLowerCase())) { + match = true; + } + }); + return match; + } + return true; + }); + }, + + // overridden + sortByColumn(columnIndex, ascending) { + this.setOrderBy({ + field: this.self().COLUMN_ID_TO_DB_COLUMN_MAP[columnIndex], + direction: ascending ? "asc" : "desc" + }) + this.base(arguments, columnIndex, ascending); + }, + + // overridden + _loadRowCount() { + const urlParams = { + offset: 0, + limit: 1, + filters: this.getFilters() ? + JSON.stringify({ + "started_at": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()), + }; + const options = { + resolveWResponse: true + }; + osparc.store.Jobs.getInstance().fetchJobs(urlParams, options) + .then(jobs => { + const filteredJobs = this.__filterJobs(jobs); + this._onRowCountLoaded(filteredJobs.length); + }) + .catch(() => this._onRowCountLoaded(null)); + }, + + // overridden + _loadRowData(firstRow, qxLastRow) { + this.setIsFetching(true); + + const lastRow = Math.min(qxLastRow, this._rowCount - 1); + // Returns a request promise with given offset and limit + const getFetchPromise = (offset, limit=this.self().SERVER_MAX_LIMIT) => { + const urlParams = { + limit, + offset, + filters: this.getFilters() ? + JSON.stringify({ + "started_at": this.getFilters() + }) : + null, + orderBy: JSON.stringify(this.getOrderBy()) + }; + return osparc.store.Jobs.getInstance().fetchJobs(urlParams) + .then(jobs => { + const filteredJobs = this.__filterJobs(jobs); + const data = []; + const jobsCols = osparc.jobs.JobsTable.COLS; + filteredJobs.forEach(job => { + data.push({ + [jobsCols.JOB_ID.id]: job.getJobId(), + [jobsCols.SOLVER.id]: job.getSolver(), + [jobsCols.STATUS.id]: job.getStatus(), + [jobsCols.PROGRESS.id]: job.getProgress() ? (job.getProgress() + "%") : "-", + [jobsCols.SUBMIT.id]: job.getSubmittedAt() ? osparc.utils.Utils.formatDateAndTime(job.getSubmittedAt()) : "-", + [jobsCols.START.id]: job.getStartedAt() ? osparc.utils.Utils.formatDateAndTime(job.getStartedAt()) : "-", + [jobsCols.INSTANCE.id]: job.getInstance(), + }); + }); + return data; + }); + }; + + // Divides the model row request into several server requests to comply with the number of rows server limit + const reqLimit = lastRow - firstRow + 1; // Number of requested rows + const nRequests = Math.ceil(reqLimit / this.self().SERVER_MAX_LIMIT); + if (nRequests > 1) { + const requests = []; + for (let i=firstRow; i <= lastRow; i += this.self().SERVER_MAX_LIMIT) { + requests.push(getFetchPromise(i, i > lastRow - this.self().SERVER_MAX_LIMIT + 1 ? reqLimit % this.self().SERVER_MAX_LIMIT : this.self().SERVER_MAX_LIMIT)) + } + Promise.all(requests) + .then(responses => this._onRowDataLoaded(responses.flat())) + .catch(err => { + console.error(err); + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } else { + getFetchPromise(firstRow, reqLimit) + .then(data => { + this._onRowDataLoaded(data); + }) + .catch(err => { + console.error(err) + this._onRowDataLoaded(null); + }) + .finally(() => this.setIsFetching(false)); + } + } + } +}) diff --git a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js index cfd30bbf01a..f9f046dd0b3 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js @@ -117,6 +117,7 @@ qx.Class.define("osparc.navigation.NavigationBar", { // right-items this.getChildControl("tasks-button"); + this.getChildControl("jobs-button"); this.getChildControl("notifications-button"); this.getChildControl("expiration-icon"); this.getChildControl("help"); @@ -218,14 +219,14 @@ qx.Class.define("osparc.navigation.NavigationBar", { this.getChildControl("center-items").add(control); break; } - case "credits-button": - control = new osparc.desktop.credits.CreditsIndicatorButton(); - this.getChildControl("right-items").add(control); - break; case "tasks-button": control = new osparc.task.TasksButton(); this.getChildControl("right-items").add(control); break; + case "jobs-button": + control = new osparc.jobs.JobsButton(); + this.getChildControl("right-items").add(control); + break; case "notifications-button": control = new osparc.notification.NotificationsButton(); this.getChildControl("right-items").add(control); @@ -262,6 +263,10 @@ qx.Class.define("osparc.navigation.NavigationBar", { osparc.utils.Utils.setIdToWidget(control, "helpNavigationBtn"); this.getChildControl("right-items").add(control); break; + case "credits-button": + control = new osparc.desktop.credits.CreditsIndicatorButton(); + this.getChildControl("right-items").add(control); + break; case "log-in-button": { control = this.__createLoginBtn().set({ visibility: "excluded" diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js index dd8d4b543d4..3c1cfd12152 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js @@ -53,8 +53,10 @@ qx.Class.define("osparc.notification.NotificationsButton", { case "icon": { control = new qx.ui.basic.Image(); const iconContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ - alignY: "middle" - })); + alignY: "middle", + })).set({ + paddingLeft: 5, + }); iconContainer.add(control); this._add(iconContainer, { height: "100%" diff --git a/services/static-webserver/client/source/class/osparc/store/Jobs.js b/services/static-webserver/client/source/class/osparc/store/Jobs.js new file mode 100644 index 00000000000..155a9759755 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/store/Jobs.js @@ -0,0 +1,78 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +/** + * @asset(osparc/mock_jobs.json") + */ + +qx.Class.define("osparc.store.Jobs", { + extend: qx.core.Object, + type: "singleton", + + properties: { + jobs: { + check: "Array", + init: [], + nullable: true, + event: "changeJobs" + } + }, + + members: { + fetchJobs: function() { + return osparc.utils.Utils.fetchJSON("/resource/osparc/mock_jobs.json") + .then(jobsData => { + if ("jobs" in jobsData) { + jobsData["jobs"].forEach(jobData => { + this.addJob(jobData); + }); + } + return this.getJobs(); + }) + .catch(err => console.error(err)); + }, + + fetchJobInfo: function(jobId) { + return osparc.utils.Utils.fetchJSON("/resource/osparc/mock_jobs.json") + .then(jobsData => { + if ("jobs_info" in jobsData && jobId in jobsData["jobs_info"]) { + return jobsData["jobs_info"][jobId]; + } + return null; + }) + .catch(err => console.error(err)); + }, + + addJob: function(jobData) { + const jobs = this.getJobs(); + const index = jobs.findIndex(t => t.getJobId() === jobData["job_id"]); + if (index === -1) { + const job = new osparc.data.Job(jobData); + jobs.push(job); + this.fireEvent("changeJobs"); + return job; + } + return null; + }, + + removeJobs: function() { + const jobs = this.getJobs(); + jobs.forEach(job => job.dispose()); + this.fireEvent("changeJobs"); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/data/PollTasks.js b/services/static-webserver/client/source/class/osparc/store/PollTasks.js similarity index 64% rename from services/static-webserver/client/source/class/osparc/data/PollTasks.js rename to services/static-webserver/client/source/class/osparc/store/PollTasks.js index 33c06d0e3ba..5126fdff983 100644 --- a/services/static-webserver/client/source/class/osparc/data/PollTasks.js +++ b/services/static-webserver/client/source/class/osparc/store/PollTasks.js @@ -15,14 +15,10 @@ ************************************************************************ */ -qx.Class.define("osparc.data.PollTasks", { +qx.Class.define("osparc.store.PollTasks", { extend: qx.core.Object, type: "singleton", - construct: function() { - this.initTasks(); - }, - properties: { tasks: { check: "Array", @@ -33,7 +29,18 @@ qx.Class.define("osparc.data.PollTasks", { }, members: { - addTask: function(taskData, interval) { + fetchTasks: function() { + return osparc.data.Resources.get("tasks") + .then(tasksData => { + tasksData.forEach(taskData => { + const interval = 1000; + this.addTask(taskData, interval); + }); + }) + .catch(err => console.error(err)); + }, + + addTask: function(taskData, interval = 1000) { const tasks = this.getTasks(); const index = tasks.findIndex(t => t.getTaskId() === taskData["task_id"]); if (index === -1) { @@ -59,9 +66,17 @@ qx.Class.define("osparc.data.PollTasks", { }); }, + getDuplicateStudyTasks: function() { + return this.getTasks().filter(task => task.getTaskId().includes("from_study") && !task.getTaskId().includes("as_template")); + }, + + getPublishTemplateTasks: function() { + return this.getTasks().filter(task => task.getTaskId().includes("from_study") && task.getTaskId().includes("as_template")); + }, + removeTasks: function() { const tasks = this.getTasks(); tasks.forEach(task => task.dispose()); - } + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index 2bb16f8fe2c..f548d08b85a 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -162,7 +162,7 @@ qx.Class.define("osparc.study.Utils", { pollTask: true }; const fetchPromise = osparc.data.Resources.fetch("studies", "postNewStudy", params, options); - const pollTasks = osparc.data.PollTasks.getInstance(); + const pollTasks = osparc.store.PollTasks.getInstance(); const interval = 1000; pollTasks.createPollingTask(fetchPromise, interval) .then(task => { @@ -206,7 +206,7 @@ qx.Class.define("osparc.study.Utils", { pollTask: true }; const fetchPromise = osparc.data.Resources.fetch("studies", "postNewStudyFromTemplate", params, options); - const pollTasks = osparc.data.PollTasks.getInstance(); + const pollTasks = osparc.store.PollTasks.getInstance(); const interval = 1000; pollTasks.createPollingTask(fetchPromise, interval) .then(task => { diff --git a/services/static-webserver/client/source/class/osparc/task/TaskUI.js b/services/static-webserver/client/source/class/osparc/task/TaskUI.js index 513bf34ec91..3e79c75137b 100644 --- a/services/static-webserver/client/source/class/osparc/task/TaskUI.js +++ b/services/static-webserver/client/source/class/osparc/task/TaskUI.js @@ -133,16 +133,6 @@ qx.Class.define("osparc.task.TaskUI", { }, this); }, - start: function() { - const tasks = osparc.task.Tasks.getInstance(); - tasks.addTask(this); - }, - - stop: function() { - const tasks = osparc.task.Tasks.getInstance(); - tasks.removeTask(this); - }, - setIcon: function(source) { this.getChildControl("icon").getContentElement().removeClass("rotate"); this.getChildControl("icon").setSource(source); diff --git a/services/static-webserver/client/source/class/osparc/task/TasksButton.js b/services/static-webserver/client/source/class/osparc/task/TasksButton.js index cbf113d4eb3..0d6f85e24d4 100644 --- a/services/static-webserver/client/source/class/osparc/task/TasksButton.js +++ b/services/static-webserver/client/source/class/osparc/task/TasksButton.js @@ -27,10 +27,11 @@ qx.Class.define("osparc.task.TasksButton", { width: 30, alignX: "center", cursor: "pointer", - visibility: "excluded" + visibility: "excluded", + toolTipText: this.tr("Tasks"), }); - const tasks = osparc.task.Tasks.getInstance(); + const tasks = osparc.task.TasksContainer.getInstance(); tasks.getTasks().addListener("change", e => this.__updateTasksButton(), this); this.addListener("tap", () => this.__showTasks(), this); }, @@ -40,8 +41,8 @@ qx.Class.define("osparc.task.TasksButton", { let control; switch (id) { case "icon": { - control = new qx.ui.basic.Image("@FontAwesome5Solid/cog/24"); - osparc.utils.Utils.addClass(control.getContentElement(), "rotate"); + control = new qx.ui.basic.Image("@FontAwesome5Solid/cog/22"); + osparc.utils.Utils.addClass(control.getContentElement(), "rotateSlow"); const logoContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ alignY: "middle" @@ -74,7 +75,7 @@ qx.Class.define("osparc.task.TasksButton", { this._createChildControlImpl("icon"); const number = this.getChildControl("number"); - const tasks = osparc.task.Tasks.getInstance(); + const tasks = osparc.task.TasksContainer.getInstance(); const nTasks = tasks.getTasks().length; number.setValue(nTasks.toString()); nTasks ? this.show() : this.exclude(); @@ -83,7 +84,7 @@ qx.Class.define("osparc.task.TasksButton", { __showTasks: function() { const that = this; const tapListener = event => { - const tasks = osparc.task.Tasks.getInstance(); + const tasks = osparc.task.TasksContainer.getInstance(); const tasksContainer = tasks.getTasksContainer(); if (osparc.utils.Utils.isMouseOnElement(tasksContainer, event)) { return; @@ -103,14 +104,14 @@ qx.Class.define("osparc.task.TasksButton", { bounds.top = parseInt(rect.y); } } - const tasks = osparc.task.Tasks.getInstance(); + const tasks = osparc.task.TasksContainer.getInstance(); tasks.setTasksContainerPosition(bounds.left+bounds.width, osparc.navigation.NavigationBar.HEIGHT+3); tasks.getTasksContainer().show(); document.addEventListener("mousedown", tapListener); }, __hideTasks: function() { - const tasks = osparc.task.Tasks.getInstance(); + const tasks = osparc.task.TasksContainer.getInstance(); tasks.getTasksContainer().exclude(); } } diff --git a/services/static-webserver/client/source/class/osparc/task/Tasks.js b/services/static-webserver/client/source/class/osparc/task/TasksContainer.js similarity index 72% rename from services/static-webserver/client/source/class/osparc/task/Tasks.js rename to services/static-webserver/client/source/class/osparc/task/TasksContainer.js index 8494db9754b..6e22ee89c96 100644 --- a/services/static-webserver/client/source/class/osparc/task/Tasks.js +++ b/services/static-webserver/client/source/class/osparc/task/TasksContainer.js @@ -15,7 +15,7 @@ ************************************************************************ */ -qx.Class.define("osparc.task.Tasks", { +qx.Class.define("osparc.task.TasksContainer", { extend: qx.core.Object, type: "singleton", @@ -39,17 +39,21 @@ qx.Class.define("osparc.task.Tasks", { __tasks: null, __tasksContainer: null, - addTask: function(task) { - this.__tasks.push(task); - this.__tasksContainer.addAt(task, 0); + addTaskUI: function(taskUI) { + const alreadyExists = this.__tasks.filter(task => task.getTask().getTaskId() === taskUI.getTask().getTaskId()).length; + if (alreadyExists) { + return; + } + this.__tasks.push(taskUI); + this.__tasksContainer.addAt(taskUI, 0); }, - removeTask: function(task) { - if (this.__tasks.indexOf(task) > -1) { - this.__tasks.remove(task); + removeTaskUI: function(taskUI) { + if (this.__tasks.indexOf(taskUI) > -1) { + this.__tasks.remove(taskUI); } - if (this.__tasksContainer.indexOf(task) > -1) { - this.__tasksContainer.remove(task); + if (this.__tasksContainer.indexOf(taskUI) > -1) { + this.__tasksContainer.remove(taskUI); } }, diff --git a/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ButtonRenderer.js b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ButtonRenderer.js new file mode 100644 index 00000000000..445840c570d --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ButtonRenderer.js @@ -0,0 +1,58 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2035 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.ui.table.cellrenderer.ButtonRenderer", { + extend: osparc.ui.table.cellrenderer.Html, + + construct: function(clickAction) { + this.base(arguments); + + this.setClickAction(clickAction); + }, + + properties: { + clickAction: { + check: "String", + nullable: false, + init: "clickAction", + }, + + buttonContent: { + check: "String", + nullable: false, + init: "", + } + }, + + members: { + // Override + _getContentHtml: function(cellInfo) { + const clickAction = this.getClickAction(); + const buttonContent = this.getButtonContent(); + + // Return the button with the image + return ` +
+ ${buttonContent} +
+ `; + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/Html.js b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/Html.js index bc715a54051..11e1c5b976e 100644 --- a/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/Html.js +++ b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/Html.js @@ -20,9 +20,7 @@ */ qx.Class.define("osparc.ui.table.cellrenderer.Html", { extend: qx.ui.table.cellrenderer.Html, - construct: function() { - this.base(arguments); - }, + members: { // Override _getCellStyle: function(cellInfo) { diff --git a/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ImageButtonRenderer.js b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ImageButtonRenderer.js new file mode 100644 index 00000000000..8b9fd7896bd --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/ui/table/cellrenderer/ImageButtonRenderer.js @@ -0,0 +1,44 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2035 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.ui.table.cellrenderer.ImageButtonRenderer", { + extend: osparc.ui.table.cellrenderer.ButtonRenderer, + + construct: function(clickAction, iconPath) { + this.base(arguments, clickAction); + + this.setIconPath(iconPath); + }, + + properties: { + iconPath: { + check: "String", + init: null, + nullable: false, + apply: "__applyIconPath", + }, + }, + + members: { + __applyIconPath: function(iconPath) { + const resMgr = qx.util.ResourceManager.getInstance(); + const iconUrl = resMgr.toUri(iconPath); // Resolves to the correct URL of the asset + + this.setButtonContent(`icon`); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/utils/DisabledPlugins.js b/services/static-webserver/client/source/class/osparc/utils/DisabledPlugins.js index 0578f4b73f9..ccd68623f94 100644 --- a/services/static-webserver/client/source/class/osparc/utils/DisabledPlugins.js +++ b/services/static-webserver/client/source/class/osparc/utils/DisabledPlugins.js @@ -52,6 +52,13 @@ qx.Class.define("osparc.utils.DisabledPlugins", { return this.__isPluginDisabled(this.LICENSES); }, + isJobsEnabled: function() { + if (osparc.utils.Utils.isDevelopmentPlatform() && osparc.product.Utils.isProduct("s4lacad")) { + return true; + } + return false; + }, + __isPluginDisabled: function(key) { const statics = osparc.store.Store.getInstance().get("statics"); if (statics) { diff --git a/services/static-webserver/client/source/resource/common/common.css b/services/static-webserver/client/source/resource/common/common.css index 034546a9155..d032c109151 100644 --- a/services/static-webserver/client/source/resource/common/common.css +++ b/services/static-webserver/client/source/resource/common/common.css @@ -15,7 +15,11 @@ } .rotate { - animation: rotation 1.5s infinite linear; + animation: rotation 2s infinite linear; +} + +.rotateSlow { + animation: rotation 4s infinite linear; } .verticalText { diff --git a/services/static-webserver/client/source/resource/osparc/circle-info-text.svg b/services/static-webserver/client/source/resource/osparc/circle-info-text.svg new file mode 100644 index 00000000000..b753c59738b --- /dev/null +++ b/services/static-webserver/client/source/resource/osparc/circle-info-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/static-webserver/client/source/resource/osparc/circle-stop-text.svg b/services/static-webserver/client/source/resource/osparc/circle-stop-text.svg new file mode 100644 index 00000000000..1470fd0f295 --- /dev/null +++ b/services/static-webserver/client/source/resource/osparc/circle-stop-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/static-webserver/client/source/resource/osparc/logs-text.svg b/services/static-webserver/client/source/resource/osparc/logs-text.svg new file mode 100644 index 00000000000..20dd82103ce --- /dev/null +++ b/services/static-webserver/client/source/resource/osparc/logs-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/static-webserver/client/source/resource/osparc/mock_jobs.json b/services/static-webserver/client/source/resource/osparc/mock_jobs.json new file mode 100644 index 00000000000..3aa775971e7 --- /dev/null +++ b/services/static-webserver/client/source/resource/osparc/mock_jobs.json @@ -0,0 +1,85 @@ +{ + "jobs": [{ + "job_id": "0cde4607-07de-4bb5-b3e6-487f22387a70", + "solver": "isolve:2.4.12", + "status": "PUBLISHED", + "progress": 0, + "submitted_at": "2024-11-27 14:03:17.357523", + "started_at": null, + "instance": "p5.8xlarge" + }, { + "job_id": "5685a699-4927-479e-9b34-e5ab1616303a", + "solver": "fenics:1.0.4", + "status": "WAITING FOR RESOURCES", + "progress": 2, + "submitted_at": "2024-11-27 14:03:17.413844", + "started_at": "2024-11-27 16:03:17.413844", + "instance": "p5.8xlarge" + }, { + "job_id": "a8ca6a02-8816-48a6-8c6b-b94b3e431c8c", + "solver": "isolve:2.4.12", + "status": "WAITING FOR CLUSTER", + "progress": 0, + "submitted_at": "2024-11-27 14:03:17.357523", + "started_at": null, + "instance": "p5.8xlarge" + }, { + "job_id": "a8ca6a02-8816-48a6-8c6b-b94b3e431c8d", + "solver": "sleeper:2.1.2", + "status": "STARTED", + "progress": 50, + "submitted_at": "2024-11-27 14:03:17.357523", + "started_at": "2025-03-19 14:03:17.357523", + "instance": "p5.8xlarge" + }], + "jobs_info": { + "a8ca6a02-8816-48a6-8c6b-b94b3e431c8d": { + "parent_project_id" : "522f544a-1e05-461e-82ed-e26806e4640d", + "parent_project": "My Project", + "job_id": "a8ca6a02-8816-48a6-8c6b-b94b3e431c8d", + "job_name": "EM", + "log_state": 3, + "solver_key": "simcore/services/comp/sleeper", + "solver_version": "2.1.2", + "version_display" : "The latest", + "input_file_path": "/home/user/blah_blah_a8ca6a02-8816-48a6-8c6b-b94b3e431c8d_Input.h5", + "internal_job_id": "19b883a4-8407-4b81-9c5b-abe5a7f42bc8" + }, + "5685a699-4927-479e-9b34-e5ab1616303a": { + "parent_project_id" : "522f544a-1e05-461e-82ed-e26806e4640d", + "parent_project": "My Project", + "job_id": "5685a699-4927-479e-9b34-e5ab1616303a", + "job_name": "EM", + "log_state": 3, + "solver_key": "simcore/services/comp/sleeper", + "solver_version": "2.1.2", + "version_display" : "The latest", + "input_file_path": "/home/user/blah_blah_5685a699-4927-479e-9b34-e5ab1616303a_Input.h5", + "internal_job_id": "19b883a4-8407-4b81-9c5b-abe5a7f42bc8" + }, + "a8ca6a02-8816-48a6-8c6b-b94b3e431c8c": { + "parent_project_id" : "522f544a-1e05-461e-82ed-e26806e4640d", + "parent_project": "My Project", + "job_id": "a8ca6a02-8816-48a6-8c6b-b94b3e431c8c", + "job_name": "EM", + "log_state": 3, + "solver_key": "simcore/services/comp/sleeper", + "solver_version": "2.1.2", + "version_display" : "The latest", + "input_file_path": "/home/user/blah_blah_a8ca6a02-8816-48a6-8c6b-b94b3e431c8c_Input.h5", + "internal_job_id": "19b883a4-8407-4b81-9c5b-abe5a7f42bc8" + }, + "0cde4607-07de-4bb5-b3e6-487f22387a70": { + "parent_project_id" : "522f544a-1e05-461e-82ed-e26806e4640d", + "parent_project": "My Project", + "job_id": "0cde4607-07de-4bb5-b3e6-487f22387a70", + "job_name": "EM", + "log_state": 3, + "solver_key": "simcore/services/comp/sleeper", + "solver_version": "2.1.2", + "version_display" : "The latest", + "input_file_path": "/home/user/blah_blah_0cde4607-07de-4bb5-b3e6-487f22387a70_Input.h5", + "internal_job_id": "19b883a4-8407-4b81-9c5b-abe5a7f42bc8" + } + } +} diff --git a/services/static-webserver/client/source/resource/osparc/trash-text.svg b/services/static-webserver/client/source/resource/osparc/trash-text.svg new file mode 100644 index 00000000000..0cbfe135be5 --- /dev/null +++ b/services/static-webserver/client/source/resource/osparc/trash-text.svg @@ -0,0 +1 @@ + \ No newline at end of file