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 %} - -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 %} +