diff --git a/cli/medperf/commands/benchmark/update_associations_poilcy.py b/cli/medperf/commands/benchmark/update_associations_poilcy.py index 37108334d..8ac1e8211 100644 --- a/cli/medperf/commands/benchmark/update_associations_poilcy.py +++ b/cli/medperf/commands/benchmark/update_associations_poilcy.py @@ -112,6 +112,21 @@ def validate_emails(self): if self.model_emails is not None: self.model_emails = validate_and_normalize_emails(self.model_emails) + def warn_if_empty_allow_list(self): + # self.dataset_emails is None means the field wasn't part of this + # submission at all (existing allow list, if any, stays untouched); + # only an explicitly-submitted empty list ([]) warrants a warning. + if self.dataset_mode == "ALLOWLIST" and self.dataset_emails == []: + config.ui.print_warning( + "Dataset allow list is empty: no dataset associations will be " + "auto-approved until you add emails to the allow list." + ) + if self.model_mode == "ALLOWLIST" and self.model_emails == []: + config.ui.print_warning( + "Model allow list is empty: no model associations will be " + "auto-approved until you add emails to the allow list." + ) + def update(self): if all( [ @@ -122,6 +137,7 @@ def update(self): ] ): return + self.warn_if_empty_allow_list() body = {} if self.dataset_emails is not None: body["dataset_auto_approval_allow_list"] = self.dataset_emails diff --git a/cli/medperf/commands/mlcube/grant_access.py b/cli/medperf/commands/mlcube/grant_access.py index 131b60db7..46696ab61 100644 --- a/cli/medperf/commands/mlcube/grant_access.py +++ b/cli/medperf/commands/mlcube/grant_access.py @@ -61,6 +61,11 @@ def get_approval(self): f"registered in Benchmark (UID: {self.benchmark_id}) access to " "your Model.\n" ) + if not self.allowed_emails: + msg += ( + "No allow list was provided, so ALL eligible Data Owners will " + "be granted access (no email filtering).\n" + ) if not self.approved and not approval_prompt(msg): raise CleanExit("Access granting operation cancelled") @@ -104,7 +109,7 @@ def prepare_certificates_list(self): self.cert_user_info = cert_user_info def filter_certificates(self): - if self.allowed_emails is None: + if not self.allowed_emails: return logging.debug("Filtering certificates based on allowed emails list") filtered_certificates = [] diff --git a/cli/medperf/tests/commands/mlcube/test_grant_access.py b/cli/medperf/tests/commands/mlcube/test_grant_access.py index bb6367d72..c62aaa876 100644 --- a/cli/medperf/tests/commands/mlcube/test_grant_access.py +++ b/cli/medperf/tests/commands/mlcube/test_grant_access.py @@ -217,9 +217,11 @@ def test_filter_certificates_when_empty(grantaccess): } grantaccess.allowed_emails = [] - # Act & Assert - with pytest.raises(CleanExit): - grantaccess.filter_certificates() + # Act + grantaccess.filter_certificates() + + # Assert + assert [cert.id for cert in grantaccess.certificates] == [1, 2] def test_verify_certificates_filters_invalid_certs(mocker, grantaccess): diff --git a/cli/medperf/ui/web_ui.py b/cli/medperf/ui/web_ui.py index d5ee40578..dd7f8038a 100644 --- a/cli/medperf/ui/web_ui.py +++ b/cli/medperf/ui/web_ui.py @@ -1,3 +1,4 @@ +import threading from queue import Queue from contextlib import contextmanager from yaspin import yaspin @@ -16,6 +17,39 @@ def __init__(self): self.task_id = None self.events_manager = EventsManager() self.global_events_manager = GlobalEventsManager() + self._silenced = threading.local() + + @property + def _is_silenced(self) -> bool: + return getattr(self._silenced, "value", False) + + @property + def _captured_messages(self): + return getattr(self._silenced, "captured", None) + + @contextmanager + def capture(self): + """Run a block without emitting task-log events or touching the + shared spinner/interactive state, scoped to the calling thread only, + collecting any messages that would have been emitted instead. + + Used by background workers (e.g. the auto grant access worker) that + call into commands reporting progress via this UI, so they don't + get attributed to - or interrupt - whichever foreground task + currently owns the task-log stream, while still keeping a record of + what happened. Callers not interested in the messages can simply + ignore the yielded list. + + Yields: + list[str]: messages emitted during the block, in arrival order. + """ + self._silenced.value = True + self._silenced.captured = [] + try: + yield self._silenced.captured + finally: + self._silenced.captured = None + self._silenced.value = False def print_error(self, msg: str): """Display an error message on the command line @@ -37,6 +71,12 @@ def print_warning(self, msg: str): self._print(msg, "warning") def _print(self, msg: str = "", type: str = "print"): + if self._is_silenced: + captured = self._captured_messages + if captured is not None: + captured.append(msg) + return + if self.is_interactive: self.spinner.write(msg) else: @@ -70,7 +110,10 @@ def interactive(self): Yields: CLI: Yields the current CLI instance with an interactive session initialized """ - if self.is_interactive: + if self._is_silenced: + # don't touch the shared spinner/interactive state + yield self + elif self.is_interactive: # if already interactive, do nothing yield self else: @@ -97,6 +140,12 @@ def text(self, msg: str = ""): # if not self.is_interactive: # self.print(msg) + if self._is_silenced: + captured = self._captured_messages + if captured is not None: + captured.append(msg) + return + self.set_event( Event( task_id=self.task_id, diff --git a/cli/medperf/web_ui/app.py b/cli/medperf/web_ui/app.py index 3a5f40b92..3b6f1c607 100644 --- a/cli/medperf/web_ui/app.py +++ b/cli/medperf/web_ui/app.py @@ -6,8 +6,9 @@ import typer from starlette.middleware.base import BaseHTTPMiddleware -from fastapi import FastAPI, Request -from fastapi.responses import RedirectResponse +from fastapi import FastAPI, HTTPException, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from medperf import config @@ -91,15 +92,8 @@ def startup_event(): # List of [schemas.Notification] will appear in the notifications tab web_app.state.notifications = [] - # Container auto grant access initial values - web_app.state.model_auto_give_access = { - "running": False, - "worker": None, - "benchmark": 0, - "model": 0, - "emails": "", - "interval": 0, - } + # Dictionary for tracking auto give access status for each model and benchmark combination + web_app.state.model_auto_give_access = {} # Set default UI mode to evaluation on startup, will be updated by NavModeMiddleware on each request based on cookie web_app.state.ui_mode = UI_MODE_EVALUATION @@ -129,6 +123,26 @@ def not_authenticated_exception_handler( return RedirectResponse(url=exc.redirect_url) +@web_app.exception_handler(HTTPException) +def http_exception_handler(request: Request, exc: HTTPException): + # Reshape into the {"status", "error"} format for consistency with other error responses, + # instead of FastAPI's default {"detail": ...} body. + return JSONResponse( + status_code=exc.status_code, + content={"status": "failed", "error": exc.detail}, + ) + + +@web_app.exception_handler(RequestValidationError) +def validation_exception_handler(request: Request, exc: RequestValidationError): + messages = [] + for error in exc.errors(): + field = ".".join(str(p) for p in error.get("loc", []) if p != "body") + messages.append(f"{field}: {error['msg']}" if field else error["msg"]) + message = "\n".join(messages) if messages else "Invalid request data." + return JSONResponse(status_code=422, content={"status": "failed", "error": message}) + + @web_app.get("/", include_in_schema=False) def read_root(request: Request): if request.app.state.ui_mode == UI_MODE_TRAINING: diff --git a/cli/medperf/web_ui/benchmarks/routes.py b/cli/medperf/web_ui/benchmarks/routes.py index 82a4589aa..8a1499c88 100644 --- a/cli/medperf/web_ui/benchmarks/routes.py +++ b/cli/medperf/web_ui/benchmarks/routes.py @@ -1,5 +1,7 @@ import logging +import anyio + from fastapi import APIRouter, Depends, Form from fastapi.responses import HTMLResponse, JSONResponse from fastapi import Request @@ -288,11 +290,17 @@ def update_associations_policy( request: Request, benchmark_id: int = Form(...), dataset_mode: Optional[str] = Form(None), - dataset_emails: Optional[str] = Form(None), model_mode: Optional[str] = Form(None), - model_emails: Optional[str] = Form(None), current_user: bool = Depends(check_user_api), ): + # dataset_emails/model_emails are read from the raw form instead of via + # FastAPI's Form(None): an empty-string value there is indistinguishable + # from the field being absent, which breaks "explicitly clear the allow + # list" (as opposed to "the field wasn't part of this submission at all"). + form_data = anyio.from_thread.run(lambda: request.form()) + dataset_emails = form_data.get("dataset_emails") + model_emails = form_data.get("model_emails") + initialize_state_task(request, task_name="update_associations_policy") return_response = {"status": "", "error": ""} try: @@ -325,7 +333,7 @@ def update_associations_policy( def update_committee_members( request: Request, benchmark_id: int = Form(...), - committee_emails: Optional[str] = Form(None), + committee_emails: str = Form(""), current_user: bool = Depends(check_user_api), ): initialize_state_task(request, task_name="update_committee_members") diff --git a/cli/medperf/web_ui/common.py b/cli/medperf/web_ui/common.py index 6b0dd53d5..9cfa81e12 100644 --- a/cli/medperf/web_ui/common.py +++ b/cli/medperf/web_ui/common.py @@ -39,6 +39,7 @@ "/current_task", "/api/running_tasks", "/api/stop_task", + "/containers/auto_access_logs", ] diff --git a/cli/medperf/web_ui/containers/routes.py b/cli/medperf/web_ui/containers/routes.py index 4eb3d10e5..11fc8260d 100644 --- a/cli/medperf/web_ui/containers/routes.py +++ b/cli/medperf/web_ui/containers/routes.py @@ -1,5 +1,7 @@ import logging import threading +from collections import deque +from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, Form, Request @@ -31,6 +33,33 @@ logger = logging.getLogger(__name__) +def _auto_access_key(model_id: int, benchmark_id: int) -> str: + return f"{model_id}-{benchmark_id}" + + +def _format_auto_access_log_line(message: str) -> str: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return f"[{timestamp}] {message}" + + +def _running_auto_access_for_container( + model_auto_give_access: dict, container_id: int +) -> dict: + running = {} + for key, state in model_auto_give_access.items(): + try: + model_id_str, benchmark_id_str = key.split("-", 1) + if int(model_id_str) == container_id: + running[int(benchmark_id_str)] = { + "name": state["name"], + "emails": state["emails"], + "interval": state["interval"], + } + except (ValueError, IndexError): + continue + return running + + @router.get("/ui", response_class=HTMLResponse) def containers_ui( request: Request, @@ -219,6 +248,14 @@ def container_access_ui( if cert_id in certs_mapping: existing_keys[key_id] = certs_mapping[cert_id] + running_auto_access = _running_auto_access_for_container( + request.app.state.model_auto_give_access, container_id + ) + running_benchmarks = [ + {"id": benchmark_id, "name": state["name"]} + for benchmark_id, state in running_auto_access.items() + ] + return templates.TemplateResponse( "container/container_access.html", { @@ -228,6 +265,8 @@ def container_access_ui( "is_owner": is_owner, "benchmark_allowed_ids": benchmark_allowed_ids, "keys": existing_keys, + "running_auto_access": running_auto_access, + "running_benchmarks": running_benchmarks, }, ) @@ -237,7 +276,7 @@ def grant_access( request: Request, benchmark_id: int = Form(...), model_id: int = Form(...), - emails: str = Form(...), + emails: str = Form(""), current_user: bool = Depends(check_user_api), ): @@ -266,19 +305,23 @@ def grant_access( def grant_access_worker( - benchmark_id, model_id, emails, interval, stop_event: threading.Event + benchmark_id, model_id, emails, interval, stop_event: threading.Event, logs: deque ): interval_in_seconds = interval * 60 while not stop_event.is_set(): try: - GrantAccess.run( - benchmark_id=benchmark_id, - model_id=model_id, - emails=emails, - approved=True, - ) - except Exception: - pass + with config.ui.capture() as messages: + GrantAccess.run( + benchmark_id=benchmark_id, + model_id=model_id, + approved=True, + allowed_emails=emails, + ) + except Exception as exp: + messages.append(f"Error: {exp}") + logger.exception(exp) + for message in messages: + logs.append(_format_auto_access_log_line(message)) if stop_event.wait(interval_in_seconds): break @@ -289,26 +332,36 @@ def start_auto_access( benchmark_id: int = Form(...), model_id: int = Form(...), interval: int = Form(...), - emails: str = Form(...), + emails: str = Form(""), current_user: bool = Depends(check_user_api), ): - if request.app.state.model_auto_give_access["running"]: - bmk = request.app.state.model_auto_give_access["benchmark"] - model = request.app.state.model_auto_give_access["model"] + model_auto_give_access = request.app.state.model_auto_give_access + key = _auto_access_key(model_id, benchmark_id) + if key in model_auto_give_access: return { "status": "failed", - "error": f"Auto give access is already running for benchmark: {bmk}, model: {model}", + "error": "Auto give access is already running for the selected container and benchmark.", } return_response = {"status": "", "error": ""} try: + benchmark_name = Benchmark.get(benchmark_id).name event = threading.Event() + logs = deque(maxlen=config.webui_max_log_messages) auto_access_worker = threading.Thread( target=grant_access_worker, - args=(benchmark_id, model_id, emails, interval, event), + args=(benchmark_id, model_id, emails, interval, event, logs), daemon=True, ) auto_access_worker.start() + model_auto_give_access[key] = { + "worker": auto_access_worker, + "event": event, + "name": benchmark_name, + "emails": emails, + "interval": interval, + "logs": logs, + } return_response["status"] = "success" notification_message = "Successfully started automatic grant access." except Exception as exp: @@ -322,16 +375,6 @@ def start_auto_access( return_response=return_response, url=f"/containers/ui/display/{model_id}/access", ) - - request.app.state.model_auto_give_access = { - "running": True, - "worker": auto_access_worker, - "event": event, - "benchmark": benchmark_id, - "model": model_id, - "emails": emails, - "interval": interval, - } return return_response @@ -339,9 +382,12 @@ def start_auto_access( def stop_auto_access( request: Request, model_id: int = Form(...), + benchmark_id: int = Form(...), current_user: bool = Depends(check_user_api), ): - if not request.app.state.model_auto_give_access["running"]: + model_auto_give_access = request.app.state.model_auto_give_access + key = _auto_access_key(model_id, benchmark_id) + if key not in model_auto_give_access: return { "status": "failed", "error": "Auto give access is not started, nothing to stop.", @@ -349,8 +395,9 @@ def stop_auto_access( return_response = {"status": "", "error": ""} try: - request.app.state.model_auto_give_access["event"].set() - request.app.state.model_auto_give_access["worker"].join() + model_auto_give_access[key]["event"].set() + model_auto_give_access[key]["worker"].join() + del model_auto_give_access[key] return_response["status"] = "success" notification_message = "Successfully stopped automatic grant access." except Exception as exp: @@ -364,18 +411,26 @@ def stop_auto_access( return_response=return_response, url=f"/containers/ui/display/{model_id}/access", ) + return return_response - request.app.state.model_auto_give_access = { - "running": False, - "worker": None, - "event": None, - "benchmark": 0, - "model": 0, - "emails": "", - "interval": 0, - } - return return_response +@router.get("/auto_access_logs", response_class=JSONResponse) +def auto_access_logs( + request: Request, + model_id: int, + benchmark_id: int, + current_user: bool = Depends(check_user_api), +): + model_auto_give_access = request.app.state.model_auto_give_access + key = _auto_access_key(model_id, benchmark_id) + if key not in model_auto_give_access: + return { + "status": "failed", + "error": "Auto give access is not running for the selected container and benchmark.", + "logs": [], + } + + return {"status": "success", "error": "", "logs": list(model_auto_give_access[key]["logs"])} @router.post("/revoke_user_access", response_class=JSONResponse) diff --git a/cli/medperf/web_ui/medperf_login.py b/cli/medperf/web_ui/medperf_login.py index ba448a550..ed9403b45 100644 --- a/cli/medperf/web_ui/medperf_login.py +++ b/cli/medperf/web_ui/medperf_login.py @@ -102,6 +102,12 @@ def logout( request: Request, current_user: bool = Depends(check_user_api), ): + if request.app.state.model_auto_give_access: + return { + "status": "failed", + "error": "Automatic grant access is currently running. Stop it before logging out.", + } + initialize_state_task(request, task_name="medperf_logout") return_response = {"status": "", "error": ""} diff --git a/cli/medperf/web_ui/static/js/benchmarks/benchmark_detail.js b/cli/medperf/web_ui/static/js/benchmarks/benchmark_detail.js index 73919b752..47f0d7a7c 100644 --- a/cli/medperf/web_ui/static/js/benchmarks/benchmark_detail.js +++ b/cli/medperf/web_ui/static/js/benchmarks/benchmark_detail.js @@ -48,24 +48,23 @@ function showErrorToast(message) { showToast("Validation Error", message, "text-bg-danger"); } -function checkUpdateAssociationsPolicyForm() { +function buildAssociationsPolicyConfirmMessage(message) { var datasetModeEl = document.getElementById("dataset-auto-approve-mode"); var modelModeEl = document.getElementById("model-auto-approve-mode"); - var datasetApproveMode = datasetModeEl ? datasetModeEl.value : "NEVER"; - var modelApproveMode = modelModeEl ? modelModeEl.value : "NEVER"; - var isDatasetValid = (datasetApproveMode === "NEVER" || datasetApproveMode === "ALWAYS"); - var isModelValid = (modelApproveMode === "NEVER" || modelApproveMode === "ALWAYS"); - if (!isDatasetValid) { + var warnings = []; + if (datasetModeEl && datasetModeEl.value === "ALLOWLIST") { var datasetAllowListArr = getEmailsList(document.getElementById("dataset-allow-list-emails")); - if (datasetAllowListArr.length) isDatasetValid = true; - else showErrorToast("Make sure that the dataset allow list is not empty"); + if (!datasetAllowListArr.length) warnings.push("dataset"); } - if (!isModelValid) { + if (modelModeEl && modelModeEl.value === "ALLOWLIST") { var modelAllowListArr = getEmailsList(document.getElementById("model-allow-list-emails")); - if (modelAllowListArr.length) isModelValid = true; - else showErrorToast("Make sure that the model allow list is not empty"); + if (!modelAllowListArr.length) warnings.push("model"); } - return isDatasetValid && isModelValid; + if (!warnings.length) return message; + var listWord = warnings.length > 1 ? "allow lists are" : "allow list is"; + return message + " Note: the " + warnings.join(" and ") + " " + listWord + " empty " + + "- no " + warnings.join("/") + " associations will be auto-approved until " + + "you add emails."; } function onUpdateAssociationsPolicySuccess(response) { @@ -168,7 +167,8 @@ function initBenchmarkDetail() { var savePolicyBtn = document.getElementById("save-policy-btn"); if (savePolicyBtn) savePolicyBtn.addEventListener("click", function (e) { - if (checkUpdateAssociationsPolicyForm()) showConfirmModal(e.currentTarget, updateAssociationsPolicy, "update benchmark associations policy?"); + var message = buildAssociationsPolicyConfirmMessage("update benchmark associations policy?"); + showConfirmModal(e.currentTarget, updateAssociationsPolicy, message); }); var saveCommitteeBtn = document.getElementById("save-committee-members-btn"); diff --git a/cli/medperf/web_ui/static/js/common.js b/cli/medperf/web_ui/static/js/common.js index 5f37e7139..e04e24ae0 100644 --- a/cli/medperf/web_ui/static/js/common.js +++ b/cli/medperf/web_ui/static/js/common.js @@ -199,6 +199,7 @@ function showErrorModal(errorTitle, response) { var responseError = (response && response.error) || ""; var responseStatus = (response && response.status) || ""; var errorText = (responseError + (responseError ? "
" : "") + responseStatus).replace(/\n/g, "
"); + if (!errorText) errorText = "Something went wrong. Please try again."; var modalBody = "

" + errorText + "

"; var modalFooter = ""; showModal({ title: errorTitle, body: modalBody, footer: modalFooter }); diff --git a/cli/medperf/web_ui/static/js/containers/container_access.js b/cli/medperf/web_ui/static/js/containers/container_access.js index 27ef5f4ee..c71d50e35 100644 --- a/cli/medperf/web_ui/static/js/containers/container_access.js +++ b/cli/medperf/web_ui/static/js/containers/container_access.js @@ -19,32 +19,123 @@ function createEmailChip(email, inputElement) { if (inputElement && inputElement.parentNode) inputElement.parentNode.insertBefore(chip, inputElement); } +function clearEmailChips(container) { + if (!container) return; + container.querySelectorAll(".email-chip").forEach(function (chip) { chip.remove(); }); +} + +function setEmailChips(container, emails) { + clearEmailChips(container); + var inputEl = container ? container.querySelector("input") : null; + (emails || []).forEach(function (email) { + email = (email || "").trim(); + if (email) createEmailChip(email, inputEl); + }); +} + function parseEmails(element) { if (!element || !element.getAttribute) return; var raw = element.getAttribute("data-allowed-list") || "[]"; try { var jsonList = JSON.parse(raw); - var inputEl = element.querySelector("input"); - jsonList.forEach(function (email) { createEmailChip(email, inputEl); }); + setEmailChips(element, jsonList); } catch (_) {} } +function parseRunningAutoAccess(panel) { + if (!panel) return {}; + try { + return JSON.parse(panel.getAttribute("data-running-auto-access") || "{}"); + } catch (_) { + return {}; + } +} + +function getSelectedBenchmarkId() { + var benchmarkEl = document.getElementById("benchmark-auto"); + return benchmarkEl && benchmarkEl.value ? benchmarkEl.value : ""; +} + +function getRunningStateForBenchmark(runningAutoAccess, benchmarkId) { + if (!benchmarkId) return null; + return runningAutoAccess[benchmarkId] || null; +} + +function parseStoredEmails(emails) { + if (!emails) return []; + return String(emails).trim().split(/\s+/).filter(Boolean); +} + +function setElementVisible(element, visible) { + if (!element) return; + element.style.display = visible ? "" : "none"; + element.classList.toggle("hidden", !visible); +} + +function updateAutoAccessUI() { + var panel = document.getElementById("auto-access-panel"); + var actionsEl = document.getElementById("auto-access-actions"); + var startBtn = document.getElementById("start-auto-access-btn"); + var stopBtn = document.getElementById("stop-auto-access-btn"); + var viewLogsBtn = document.getElementById("view-auto-access-logs-btn"); + var runningBadge = document.getElementById("running-badge"); + var intervalEl = document.getElementById("interval-auto"); + var emailContainer = document.getElementById("allowed-email-list-auto"); + var emailInput = document.getElementById("email-input-auto"); + var benchmarkId = getSelectedBenchmarkId(); + var runningAutoAccess = parseRunningAutoAccess(panel); + var runningState = getRunningStateForBenchmark(runningAutoAccess, benchmarkId); + var isRunning = Boolean(runningState); + + setElementVisible(startBtn, false); + setElementVisible(stopBtn, false); + setElementVisible(viewLogsBtn, false); + setElementVisible(runningBadge, false); + + if (!benchmarkId) { + setElementVisible(actionsEl, false); + if (intervalEl) { + intervalEl.value = "5"; + intervalEl.disabled = true; + } + if (emailInput) emailInput.disabled = true; + clearEmailChips(emailContainer); + return; + } + + setElementVisible(actionsEl, true); + + if (isRunning) { + if (intervalEl) { + intervalEl.value = runningState.interval || 5; + intervalEl.disabled = true; + } + if (emailInput) emailInput.disabled = true; + setEmailChips(emailContainer, parseStoredEmails(runningState.emails)); + setElementVisible(stopBtn, true); + setElementVisible(viewLogsBtn, true); + setElementVisible(runningBadge, true); + } else { + if (intervalEl) { + intervalEl.value = "5"; + intervalEl.disabled = false; + } + if (emailInput) emailInput.disabled = false; + clearEmailChips(emailContainer); + setElementVisible(startBtn, true); + } +} + function checkAccessForm() { - var allowListArr = getEmailsList(document.getElementById("allowed-email-list")); if (!document.getElementById("benchmark") || !document.getElementById("benchmark").value) { showErrorToast("Make sure that you've selected a benchmark"); return false; } - if (!allowListArr.length) { - showErrorToast("Make sure that the email allow list is not empty"); - return false; - } return true; } function checkAutoAccessForm() { - var allowListArr = getEmailsList(document.getElementById("allowed-email-list-auto")); - if (!document.getElementById("benchmark-auto") || !document.getElementById("benchmark-auto").value) { + if (!getSelectedBenchmarkId()) { showErrorToast("Make sure that you've selected a benchmark"); return false; } @@ -54,19 +145,22 @@ function checkAutoAccessForm() { showErrorToast("Make sure that the time interval is between 5 and 60 (inclusive)"); return false; } - if (!allowListArr.length) { - showErrorToast("Make sure that the email allow list is not empty"); - return false; - } return true; } +function emptyAllowListWarning(allowListArr, message) { + if (allowListArr.length) return message; + return message + " Note: no emails were added - this will grant access to ALL " + + "eligible data owners, with no email filtering."; +} + function startAutoGrant(startBtn) { disableElements(".card button, .card input, .card select"); + var panel = document.getElementById("auto-access-panel"); var allowListArr = getEmailsList(document.getElementById("allowed-email-list-auto")); var formData = new FormData(); - formData.append("benchmark_id", document.getElementById("benchmark-auto").value); - formData.append("model_id", startBtn.getAttribute("data-model-id")); + formData.append("benchmark_id", getSelectedBenchmarkId()); + formData.append("model_id", panel ? panel.getAttribute("data-model-id") : ""); formData.append("interval", document.getElementById("interval-auto").value); formData.append("emails", allowListArr.join(" ")); ajaxRequest("/containers/start_auto_access", "POST", formData, function (response) { @@ -77,14 +171,33 @@ function startAutoGrant(startBtn) { function stopAutoGrant(stopBtn) { disableElements(".card button, .card input, .card select"); + var panel = document.getElementById("auto-access-panel"); var formData = new FormData(); - formData.append("model_id", stopBtn.getAttribute("data-model-id")); + formData.append("model_id", panel ? panel.getAttribute("data-model-id") : ""); + formData.append("benchmark_id", getSelectedBenchmarkId()); ajaxRequest("/containers/stop_auto_access", "POST", formData, function (response) { if (response && response.status === "success") showReloadModal({ title: "Successfully Stopped Auto Grant Access", seconds: 2 }); else showErrorModal("Failed to Stop Auto Grant Access", response); }, "Failed to stop auto grant access"); } +function viewAutoAccessLogs() { + var panel = document.getElementById("auto-access-panel"); + var modelId = panel ? panel.getAttribute("data-model-id") : ""; + var benchmarkId = getSelectedBenchmarkId(); + var url = "/containers/auto_access_logs?model_id=" + encodeURIComponent(modelId) + + "&benchmark_id=" + encodeURIComponent(benchmarkId); + ajaxRequest(url, "GET", null, function (response) { + var logs = (response && response.logs) || []; + var body = logs.length + ? "
" +
+                logs.map(function (line) { return escapeHtml(cleanMsg(line)); }).join("\n") + "
" + : "

No logs recorded yet.

"; + var footer = ""; + showModal({ title: "Automatic Grant Access Logs", body: body, footer: footer, modalClasses: "max-w-2xl" }); + }, "Failed to fetch automatic grant access logs"); +} + function showErrorToast(message) { showToast("Validation Error", message, "text-bg-danger"); } @@ -95,7 +208,6 @@ function isValidEmail(email) { function init() { parseEmails(document.getElementById("allowed-email-list")); - parseEmails(document.getElementById("allowed-email-list-auto")); document.querySelectorAll(".email-input").forEach(function (input) { input.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " " || e.key === ",") { @@ -129,12 +241,25 @@ function init() { } }); }); + + var benchmarkAutoEl = document.getElementById("benchmark-auto"); + if (benchmarkAutoEl) benchmarkAutoEl.addEventListener("change", updateAutoAccessUI); + var startBtn = document.getElementById("start-auto-access-btn"); if (startBtn) startBtn.addEventListener("click", function (e) { - if (checkAutoAccessForm()) showConfirmModal(e.currentTarget, startAutoGrant, "start automatic grant access for the selected benchmark?"); + if (!checkAutoAccessForm()) return; + var allowListArr = getEmailsList(document.getElementById("allowed-email-list-auto")); + var message = emptyAllowListWarning(allowListArr, "start automatic grant access for the selected benchmark?"); + showConfirmModal(e.currentTarget, startAutoGrant, message); }); var stopBtn = document.getElementById("stop-auto-access-btn"); - if (stopBtn) stopBtn.addEventListener("click", function (e) { showConfirmModal(e.currentTarget, stopAutoGrant, "stop automatic grant access?"); }); + if (stopBtn) stopBtn.addEventListener("click", function (e) { + showConfirmModal(e.currentTarget, stopAutoGrant, "stop automatic grant access?"); + }); + var viewLogsBtn = document.getElementById("view-auto-access-logs-btn"); + if (viewLogsBtn) viewLogsBtn.addEventListener("click", viewAutoAccessLogs); + + updateAutoAccessUI(); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); diff --git a/cli/medperf/web_ui/templates/container/container_access.html b/cli/medperf/web_ui/templates/container/container_access.html index e63a8290f..1f460a036 100644 --- a/cli/medperf/web_ui/templates/container/container_access.html +++ b/cli/medperf/web_ui/templates/container/container_access.html @@ -3,14 +3,6 @@ {% block title %}Manage Container Access{% endblock %} {% block detail_panel %} -{% set auto_access_running = request.app.state.model_auto_give_access["running"] %} -{% set auto_access_for_this_model = auto_access_running and request.app.state.model_auto_give_access["model"] == entity.id %} -{% if auto_access_for_this_model %} - {% set selected_benchmark = request.app.state.model_auto_give_access["benchmark"] %} - {% set selected_interval = request.app.state.model_auto_give_access["interval"] %} - {% set selected_emails = request.app.state.model_auto_give_access["emails"] %} -{% endif %} -
← Back to Container Details

Manage Access | {{ entity_name }}

@@ -44,9 +36,18 @@

Grant Access

-
+

Automatic Access

Automatically grant access to new data owners associated to a benchmark.

+ {% if running_benchmarks %} +

Currently running for: + {% for benchmark in running_benchmarks %} + {{ benchmark.name }}{% if not loop.last %}, {% endif %} + {% endfor %} +

+

Note: select the benchmark below to stop it or view its logs.

+ {% endif %} +
@@ -55,28 +56,26 @@

Automatic Access

{% set input_id = "benchmark-auto" %} {% set placeholder = "Search approved benchmarks…" %} {% set allowed_ids = benchmark_allowed_ids %} - {% set selected_value = selected_benchmark if auto_access_for_this_model else '' %} - {% set disabled = auto_access_for_this_model %} + {% set required = false %} + {% set disabled = task_running %} {% include "partials/searchable_select.html" %}
- +
- -
- {% if auto_access_for_this_model %} - - Running - {% else %} - - {% endif %} +