Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/specs/web-server/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
FunctionGroupPathParams,
)
from simcore_service_webserver.functions._controller._functions_rest_schemas import (
FunctionDeleteQueryParams,
FunctionGetQueryParams,
FunctionPathParams,
FunctionsListQueryParams,
Expand Down Expand Up @@ -80,6 +81,7 @@ async def update_function(
)
async def delete_function(
_path: Annotated[FunctionPathParams, Depends()],
_query: Annotated[as_query(FunctionDeleteQueryParams), Depends()],
): ...


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class FunctionIDNotFoundError(FunctionBaseError):
status_code: int = 404 # Not Found


class FunctionHasJobsCannotDeleteError(FunctionBaseError):
msg_template: str = (
"Cannot delete function {function_id} because it has {jobs_count} associated job(s)."
)
status_code: int = 409 # Conflict


class FunctionJobIDNotFoundError(FunctionBaseError):
msg_template: str = "Function job {function_job_id} not found"
status_code: int = 404 # Not Found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1640,8 +1640,6 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
return deleteButton;
},



__createSelectButton: function() {
const selectButton = new qx.ui.form.ToggleButton().set({
appearance: "form-button-outlined",
Expand Down Expand Up @@ -1794,12 +1792,12 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
this._reloadCards();
},

__removeFromStudyList: function(studyId) {
const idx = this._resourcesList.findIndex(study => study["uuid"] === studyId);
__removeFromList: function(resourceUuid) {
const idx = this._resourcesList.findIndex(resource => resource["uuid"] === resourceUuid);
if (idx > -1) {
this._resourcesList.splice(idx, 1);
}
this._resourcesContainer.removeCard(studyId);
this._resourcesContainer.removeCard(resourceUuid);
},

_populateCardMenu: function(card) {
Expand All @@ -1812,7 +1810,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
this._populateTemplateCardMenu(card);
break;
case "function":
card.getChildControl("menu-selection-stack").exclude();
this.__populateFunctionCardMenu(card);
break;
}
},
Expand Down Expand Up @@ -1919,6 +1917,14 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
card.evaluateMenuButtons();
},

__populateFunctionCardMenu: function(card) {
const menu = card.getMenu();
const functionData = card.getResourceData();

const deleteButton = this.__getDeleteFunctionMenuButton(functionData);
menu.add(deleteButton);
},

__getOpenLocationMenuButton: function(studyData) {
const openLocationButton = new qx.ui.menu.Button(this.tr("Open location"), "@FontAwesome5Solid/external-link-alt/12");
openLocationButton.addListener("execute", () => {
Expand Down Expand Up @@ -1994,7 +2000,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
__doMoveStudy: function(studyData, destWorkspaceId, destFolderId) {
this.__moveStudyToWorkspace(studyData, destWorkspaceId) // first move to workspace
.then(() => this.__moveStudyToFolder(studyData, destFolderId)) // then move to folder
.then(() => this.__removeFromStudyList(studyData["uuid"]))
.then(() => this.__removeFromList(studyData["uuid"]))
.catch(err => osparc.FlashMessenger.logError(err));
},

Expand Down Expand Up @@ -2192,6 +2198,54 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
return deleteButton;
},

__getDeleteFunctionMenuButton: function(functionData) {
const deleteButton = new qx.ui.menu.Button(this.tr("Delete"), "@FontAwesome5Solid/trash/12");
deleteButton.set({
appearance: "menu-button"
});
osparc.utils.Utils.setIdToWidget(deleteButton, "functionItemMenuDelete");
deleteButton.addListener("execute", () => {
this.__popUpDeleteFunctionWindow(functionData, false);
}, this);
return deleteButton;
},

__popUpDeleteFunctionWindow: function(functionData, force, message) {
const win = this.__createConfirmDeleteWindow([functionData.title]);
win.setCaption(this.tr("Delete function"));
if (force) {
if (message) {
win.setMessage(message);
} else {
const msg = this.tr("The function has associated jobs. Are you sure you want to delete it?");
win.setMessage(msg);
}
}
win.center();
win.open();
win.addListener("close", () => {
if (win.getConfirmed()) {
this.__doDeleteFunction(functionData, force);
}
}, this);
},

__doDeleteFunction: function(functionData, force = false) {
osparc.store.Functions.deleteFunction(functionData.uuid, force)
.then(() => {
this.__removeFromList(functionData.uuid);
const msg = this.tr("Successfully deleted");
osparc.FlashMessenger.logAs(msg, "INFO");
})
.catch(err => {
if (err && err.status && err.status === 409) {
this.__popUpDeleteFunctionWindow(functionData, true, err.message);
} else {
osparc.FlashMessenger.logError(err);
}
});
},

__getStudyData: function(id) {
return this._resourcesList.find(study => study.uuid === id);
},
Expand Down Expand Up @@ -2303,7 +2357,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
__untrashStudy: function(studyData) {
osparc.store.Study.getInstance().untrashStudy(studyData.uuid)
.then(() => {
this.__removeFromStudyList(studyData.uuid);
this.__removeFromList(studyData.uuid);
const msg = this.tr("Successfully restored");
osparc.FlashMessenger.logAs(msg, "INFO");
this._resourceFilter.evaluateTrashEmpty();
Expand All @@ -2315,7 +2369,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
__trashStudy: function(studyData) {
osparc.store.Study.getInstance().trashStudy(studyData.uuid)
.then(() => {
this.__removeFromStudyList(studyData.uuid);
this.__removeFromList(studyData.uuid);
const msg = this.tr("Successfully deleted");
osparc.FlashMessenger.logAs(msg, "INFO");
this._resourceFilter.setTrashEmpty(false);
Expand Down Expand Up @@ -2353,7 +2407,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", {
operationPromise = osparc.store.Study.getInstance().deleteStudy(studyData.uuid);
}
operationPromise
.then(() => this.__removeFromStudyList(studyData.uuid))
.then(() => this.__removeFromList(studyData.uuid))
.catch(err => osparc.FlashMessenger.logError(err))
.finally(() => this.resetSelection());
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,10 @@ qx.Class.define("osparc.data.Resources", {
method: "POST",
url: statics.API + "/functions"
},
delete: {
method: "DELETE",
url: statics.API + "/functions/{functionId}?force={force}"
},
patch: {
method: "PATCH",
url: statics.API + "/functions/{functionId}?include_extras=true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ qx.Class.define("osparc.store.Functions", {
});
},

deleteFunction: function(functionId, force = false) {
const params = {
url: {
functionId,
force,
}
};
return osparc.data.Resources.fetch("functions", "delete", params)
.catch(error => {
console.error("Error deleting function:", error);
throw error; // Rethrow the error to propagate it to the caller
});
},

__putCollaborator: function(functionData, gid, newPermissions) {
const params = {
url: {
Expand Down
2 changes: 1 addition & 1 deletion services/web/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.77.2
0.78.0
2 changes: 1 addition & 1 deletion services/web/server/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.77.2
current_version = 0.78.0
commit = True
message = services/webserver api version: {current_version} → {new_version}
tag = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.1.0
info:
title: simcore-service-webserver
description: Main service with an interface (http-API & websockets) to the web front-end
version: 0.77.2
version: 0.78.0
servers:
- url: ''
description: webserver
Expand Down Expand Up @@ -3788,6 +3788,13 @@ paths:
type: string
format: uuid
title: Function Id
- name: force
in: query
required: false
schema:
type: boolean
default: false
title: Force
responses:
'204':
description: Successful Response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from .._services_metadata.proxy import ServiceMetadata
from ._functions_rest_exceptions import handle_rest_requests_exceptions
from ._functions_rest_schemas import (
FunctionDeleteQueryParams,
FunctionFilters,
FunctionGetQueryParams,
FunctionGroupPathParams,
Expand Down Expand Up @@ -370,12 +371,18 @@ async def update_function(request: web.Request) -> web.Response:
async def delete_function(request: web.Request) -> web.Response:
path_params = parse_request_path_parameters_as(FunctionPathParams, request)
function_id = path_params.function_id

query_params: FunctionDeleteQueryParams = parse_request_query_parameters_as(
FunctionDeleteQueryParams, request
)

req_ctx = AuthenticatedRequestContext.model_validate(request)
await _functions_service.delete_function(
app=request.app,
function_id=function_id,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
function_id=function_id,
force=query_params.force,
)

return web.json_response(status=status.HTTP_204_NO_CONTENT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,13 @@ class FunctionsListQueryParams(
): ...


class FunctionDeleteQueryParams(BaseModel):
force: Annotated[
bool,
Field(
description="If true, deletes the function even if it has associated jobs.",
),
] = False


__all__: tuple[str, ...] = ("AuthenticatedRequestContext",)
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from models_library.functions_errors import (
FunctionBaseError,
FunctionExecuteAccessDeniedError,
FunctionHasJobsCannotDeleteError,
FunctionIDNotFoundError,
FunctionJobCollectionExecuteAccessDeniedError,
FunctionJobCollectionIDNotFoundError,
Expand Down Expand Up @@ -819,6 +820,7 @@ async def delete_function(
user_id: UserID,
product_name: ProductName,
function_id: FunctionID,
force: bool = False,
) -> None:
async with transaction_context(get_asyncpg_engine(app), connection) as transaction:
await check_user_permissions(
Expand All @@ -840,6 +842,20 @@ async def delete_function(
if row is None:
raise FunctionIDNotFoundError(function_id=function_id)

# Check for existing function jobs if force is not True
if not force:
jobs_result = await transaction.execute(
function_jobs_table.select()
.with_only_columns(func.count())
.where(function_jobs_table.c.function_uuid == function_id)
)
jobs_count = jobs_result.scalar() or 0

if jobs_count > 0:
raise FunctionHasJobsCannotDeleteError(
function_id=function_id, jobs_count=jobs_count
)

# Proceed with deletion
await transaction.execute(
functions_table.delete().where(functions_table.c.uuid == function_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,14 @@ async def delete_function(
user_id: UserID,
product_name: ProductName,
function_id: FunctionID,
force: bool = False,
) -> None:
await _functions_repository.delete_function(
app=app,
user_id=user_id,
product_name=product_name,
function_id=function_id,
force=force,
)


Expand Down
Loading
Loading