|
|
|
|
@ -57,6 +57,99 @@ def _getUserMandateIds(userId: str) -> list[str]:
|
|
|
|
|
return [um.mandateId for um in memberships if um.mandateId and um.enabled]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getAccessibleMandateIds(userId: str) -> list[str]:
|
|
|
|
|
"""Mandate IDs visible on the cross-mandate automations dashboard.
|
|
|
|
|
|
|
|
|
|
Union of explicit UserMandate memberships and mandates that own feature
|
|
|
|
|
instances the user has enabled FeatureAccess for (same idea as the
|
|
|
|
|
automation workspace list).
|
|
|
|
|
"""
|
|
|
|
|
mandateIds: set[str] = set(_getUserMandateIds(userId))
|
|
|
|
|
rootIface = getRootInterface()
|
|
|
|
|
from modules.interfaces.interfaceFeatures import getFeatureInterface
|
|
|
|
|
|
|
|
|
|
featureIf = getFeatureInterface(rootIface.db)
|
|
|
|
|
for access in rootIface.getFeatureAccessesForUser(userId) or []:
|
|
|
|
|
if not access.enabled or not access.featureInstanceId:
|
|
|
|
|
continue
|
|
|
|
|
instance = featureIf.getFeatureInstance(access.featureInstanceId)
|
|
|
|
|
if instance and instance.mandateId:
|
|
|
|
|
mandateIds.add(str(instance.mandateId))
|
|
|
|
|
return sorted(mandateIds)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getUserAccessibleInstanceIds(userId: str) -> list[str]:
|
|
|
|
|
"""Feature instance IDs with enabled FeatureAccess (workspace parity)."""
|
|
|
|
|
rootIface = getRootInterface()
|
|
|
|
|
return [
|
|
|
|
|
str(a.featureInstanceId)
|
|
|
|
|
for a in (rootIface.getFeatureAccessesForUser(userId) or [])
|
|
|
|
|
if a.enabled and a.featureInstanceId
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getWorkflowVisibilityScope(userId: str) -> dict:
|
|
|
|
|
"""Scope for which AutoWorkflow rows a user may see on the dashboard."""
|
|
|
|
|
return {
|
|
|
|
|
"mandateIds": _getAccessibleMandateIds(userId),
|
|
|
|
|
"instanceIds": _getUserAccessibleInstanceIds(userId),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflowVisibilityForContext(context: RequestContext) -> Optional[dict]:
|
|
|
|
|
"""None = platform admin (no visibility restriction)."""
|
|
|
|
|
if context.isPlatformAdmin:
|
|
|
|
|
return None
|
|
|
|
|
userId = str(context.user.id) if context.user else None
|
|
|
|
|
if not userId:
|
|
|
|
|
return {"mandateIds": [], "instanceIds": []}
|
|
|
|
|
return _getWorkflowVisibilityScope(userId)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _workflowIsVisible(row: dict, visibility_scope: Optional[dict]) -> bool:
|
|
|
|
|
if not visibility_scope:
|
|
|
|
|
return True
|
|
|
|
|
mandate_ids = set(visibility_scope.get("mandateIds") or [])
|
|
|
|
|
instance_ids = set(visibility_scope.get("instanceIds") or [])
|
|
|
|
|
mid = row.get("mandateId")
|
|
|
|
|
if mid and str(mid) in mandate_ids:
|
|
|
|
|
return True
|
|
|
|
|
fi = row.get("featureInstanceId")
|
|
|
|
|
if fi and str(fi) in instance_ids:
|
|
|
|
|
return True
|
|
|
|
|
tid = row.get("targetFeatureInstanceId") or fi
|
|
|
|
|
if tid and str(tid) in instance_ids:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _appendWorkflowVisibilityWhere(
|
|
|
|
|
whereParts: list,
|
|
|
|
|
values: list,
|
|
|
|
|
visibility_scope: Optional[dict],
|
|
|
|
|
table_alias: str = "w",
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Append (mandate IN … OR featureInstanceId IN … OR targetFeatureInstanceId IN …)."""
|
|
|
|
|
if not visibility_scope:
|
|
|
|
|
return
|
|
|
|
|
mandate_ids = visibility_scope.get("mandateIds") or []
|
|
|
|
|
instance_ids = visibility_scope.get("instanceIds") or []
|
|
|
|
|
vis_parts: list[str] = []
|
|
|
|
|
if mandate_ids:
|
|
|
|
|
vis_parts.append(f'{table_alias}."mandateId" = ANY(%s)')
|
|
|
|
|
values.append(mandate_ids)
|
|
|
|
|
if instance_ids:
|
|
|
|
|
vis_parts.append(
|
|
|
|
|
f'({table_alias}."featureInstanceId" = ANY(%s) '
|
|
|
|
|
f'OR {table_alias}."targetFeatureInstanceId" = ANY(%s))'
|
|
|
|
|
)
|
|
|
|
|
values.extend([instance_ids, instance_ids])
|
|
|
|
|
if not vis_parts:
|
|
|
|
|
whereParts.append("FALSE")
|
|
|
|
|
else:
|
|
|
|
|
whereParts.append("(" + " OR ".join(vis_parts) + ")")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getAdminMandateIds(userId: str, mandateIds: list) -> list:
|
|
|
|
|
"""Batch-check which mandates the user is admin for (UserMandate → UserMandateRole → Role)."""
|
|
|
|
|
if not mandateIds:
|
|
|
|
|
@ -126,8 +219,8 @@ def _scopedRunFilter(context: RequestContext) -> Optional[dict]:
|
|
|
|
|
"""
|
|
|
|
|
Build a DB filter dict based on RBAC:
|
|
|
|
|
- sysadmin: None (no filter)
|
|
|
|
|
- mandate admin: mandateId IN user's mandates
|
|
|
|
|
- normal user: ownerId = userId
|
|
|
|
|
- mandate admin: mandateId IN admin mandates (from accessible mandate set)
|
|
|
|
|
- normal user: ownerId = userId (own runs across all mandates)
|
|
|
|
|
"""
|
|
|
|
|
if context.isPlatformAdmin:
|
|
|
|
|
return None
|
|
|
|
|
@ -136,7 +229,7 @@ def _scopedRunFilter(context: RequestContext) -> Optional[dict]:
|
|
|
|
|
if not userId:
|
|
|
|
|
return {"ownerId": "__impossible__"}
|
|
|
|
|
|
|
|
|
|
mandateIds = _getUserMandateIds(userId)
|
|
|
|
|
mandateIds = _getAccessibleMandateIds(userId)
|
|
|
|
|
adminMandateIds = _getAdminMandateIds(userId, mandateIds)
|
|
|
|
|
|
|
|
|
|
if adminMandateIds:
|
|
|
|
|
@ -147,22 +240,14 @@ def _scopedRunFilter(context: RequestContext) -> Optional[dict]:
|
|
|
|
|
|
|
|
|
|
def _scopedWorkflowFilter(context: RequestContext) -> Optional[dict]:
|
|
|
|
|
"""
|
|
|
|
|
Build a DB filter for AutoWorkflow based on RBAC:
|
|
|
|
|
- sysadmin: None (no filter, sees all)
|
|
|
|
|
- normal user: mandateId IN user's mandates
|
|
|
|
|
Legacy hook for filterValues/ids modes — only applies isTemplate.
|
|
|
|
|
|
|
|
|
|
Row visibility uses _workflowVisibilityForContext + SQL OR clause
|
|
|
|
|
(mandate membership OR accessible feature instances), not mandateId alone.
|
|
|
|
|
"""
|
|
|
|
|
if context.isPlatformAdmin:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
userId = str(context.user.id) if context.user else None
|
|
|
|
|
if not userId:
|
|
|
|
|
return {"mandateId": "__impossible__"}
|
|
|
|
|
|
|
|
|
|
mandateIds = _getUserMandateIds(userId)
|
|
|
|
|
if mandateIds:
|
|
|
|
|
return {"mandateId": mandateIds}
|
|
|
|
|
|
|
|
|
|
return {"mandateId": "__impossible__"}
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _userMayDeleteWorkflow(context: RequestContext, wfMandateId: Optional[str]) -> bool:
|
|
|
|
|
@ -436,6 +521,7 @@ def _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
recordFilter: dict,
|
|
|
|
|
pagination,
|
|
|
|
|
wfFields: dict,
|
|
|
|
|
visibility_scope: Optional[dict] = None,
|
|
|
|
|
) -> tuple:
|
|
|
|
|
"""WHERE / ORDER BY / LIMIT for joined AutoWorkflow + run stats listing."""
|
|
|
|
|
wfFieldNames = set(wfFields.keys())
|
|
|
|
|
@ -445,6 +531,8 @@ def _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
for field, value in (recordFilter or {}).items():
|
|
|
|
|
if value is None:
|
|
|
|
|
whereParts.append(f'w."{field}" IS NULL')
|
|
|
|
|
elif field == "isTemplate" and value is False:
|
|
|
|
|
whereParts.append('w."isTemplate" IS NOT TRUE')
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
|
whereParts.append(f'w."{field}" = ANY(%s)')
|
|
|
|
|
values.append(value)
|
|
|
|
|
@ -452,6 +540,7 @@ def _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
whereParts.append(f'w."{field}" = %s')
|
|
|
|
|
values.append(value)
|
|
|
|
|
|
|
|
|
|
_appendWorkflowVisibilityWhere(whereParts, values, visibility_scope, table_alias="w")
|
|
|
|
|
_appendJoinedListingFilters(whereParts, values, pagination, wfFields)
|
|
|
|
|
|
|
|
|
|
whereClause = " WHERE " + " AND ".join(whereParts) if whereParts else ""
|
|
|
|
|
@ -481,17 +570,38 @@ def _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
return whereClause, orderClause, limitClause, values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _countWorkflowsJoined(
|
|
|
|
|
db: DatabaseConnector,
|
|
|
|
|
recordFilter: dict,
|
|
|
|
|
visibility_scope: Optional[dict] = None,
|
|
|
|
|
) -> int:
|
|
|
|
|
"""COUNT for AutoWorkflow with the same visibility clause as the joined listing."""
|
|
|
|
|
from modules.connectors.connectorDbPostgre import getModelFields
|
|
|
|
|
|
|
|
|
|
wfFields = getModelFields(AutoWorkflow)
|
|
|
|
|
whereClause, _, _, values = _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
recordFilter, None, wfFields, visibility_scope=visibility_scope,
|
|
|
|
|
)
|
|
|
|
|
fromSql = f'"AutoWorkflow" w LEFT JOIN {_RUN_STATS_SUBQUERY.strip()} ON rs."workflowId" = w."id"'
|
|
|
|
|
countSql = f"SELECT COUNT(*) AS cnt FROM {fromSql}{whereClause}"
|
|
|
|
|
db._ensure_connection()
|
|
|
|
|
with db.borrowCursor() as cursor:
|
|
|
|
|
cursor.execute(countSql, values)
|
|
|
|
|
return int(cursor.fetchone()["cnt"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getWorkflowsJoinedPaginated(
|
|
|
|
|
db: DatabaseConnector,
|
|
|
|
|
recordFilter: dict,
|
|
|
|
|
paginationParams: PaginationParams,
|
|
|
|
|
visibility_scope: Optional[dict] = None,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""SQL listing: AutoWorkflow LEFT JOIN aggregated AutoRun stats (one query + count)."""
|
|
|
|
|
from modules.connectors.connectorDbPostgre import getModelFields, parseRecordFields
|
|
|
|
|
|
|
|
|
|
wfFields = getModelFields(AutoWorkflow)
|
|
|
|
|
whereClause, orderClause, limitClause, values = _buildJoinedWorkflowWhereOrderLimit(
|
|
|
|
|
recordFilter, paginationParams, wfFields,
|
|
|
|
|
recordFilter, paginationParams, wfFields, visibility_scope=visibility_scope,
|
|
|
|
|
)
|
|
|
|
|
countValues = list(values)
|
|
|
|
|
|
|
|
|
|
@ -663,27 +773,16 @@ def get_workflow_metrics(
|
|
|
|
|
"""
|
|
|
|
|
db = _getDb()
|
|
|
|
|
|
|
|
|
|
# --- Workflow counts (same filter as /workflows endpoint) ---
|
|
|
|
|
# --- Workflow counts (same visibility as /workflows endpoint) ---
|
|
|
|
|
workflowCount = 0
|
|
|
|
|
activeWorkflows = 0
|
|
|
|
|
if db._ensureTableExists(AutoWorkflow):
|
|
|
|
|
wfBaseFilter = _scopedWorkflowFilter(context)
|
|
|
|
|
wfFilter = dict(wfBaseFilter) if wfBaseFilter else {}
|
|
|
|
|
wfFilter["isTemplate"] = False
|
|
|
|
|
|
|
|
|
|
wfCount = db.getRecordsetPaginated(
|
|
|
|
|
AutoWorkflow, pagination=PaginationParams(page=1, pageSize=1),
|
|
|
|
|
recordFilter=wfFilter if wfFilter else None,
|
|
|
|
|
)
|
|
|
|
|
workflowCount = wfCount.get("totalItems", 0) if isinstance(wfCount, dict) else wfCount.totalItems
|
|
|
|
|
|
|
|
|
|
visibility_scope = _workflowVisibilityForContext(context)
|
|
|
|
|
wfFilter = {"isTemplate": False}
|
|
|
|
|
workflowCount = _countWorkflowsJoined(db, wfFilter, visibility_scope)
|
|
|
|
|
activeFilter = dict(wfFilter)
|
|
|
|
|
activeFilter["active"] = True
|
|
|
|
|
activeCount = db.getRecordsetPaginated(
|
|
|
|
|
AutoWorkflow, pagination=PaginationParams(page=1, pageSize=1),
|
|
|
|
|
recordFilter=activeFilter,
|
|
|
|
|
)
|
|
|
|
|
activeWorkflows = activeCount.get("totalItems", 0) if isinstance(activeCount, dict) else activeCount.totalItems
|
|
|
|
|
activeWorkflows = _countWorkflowsJoined(db, activeFilter, visibility_scope)
|
|
|
|
|
|
|
|
|
|
# --- Run counts (same filter as /runs endpoint) ---
|
|
|
|
|
if not db._ensureTableExists(AutoRun):
|
|
|
|
|
@ -760,15 +859,16 @@ def get_system_workflows(
|
|
|
|
|
return _enrichedFilterValues(db, context, AutoWorkflow, _scopedWorkflowFilter, column)
|
|
|
|
|
|
|
|
|
|
if mode == "ids":
|
|
|
|
|
from modules.routes.routeHelpers import handleIdsMode
|
|
|
|
|
baseFilter = _scopedWorkflowFilter(context)
|
|
|
|
|
recordFilter = dict(baseFilter) if baseFilter else {}
|
|
|
|
|
recordFilter["isTemplate"] = False
|
|
|
|
|
return handleIdsMode(db, AutoWorkflow, pagination, recordFilter)
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
visibility_scope = _workflowVisibilityForContext(context)
|
|
|
|
|
rf = {"isTemplate": False}
|
|
|
|
|
rows = db.getRecordset(AutoWorkflow, recordFilter=rf) or []
|
|
|
|
|
if visibility_scope:
|
|
|
|
|
rows = [r for r in rows if _workflowIsVisible(dict(r), visibility_scope)]
|
|
|
|
|
return JSONResponse(content=[str(r["id"]) for r in rows if r.get("id")])
|
|
|
|
|
|
|
|
|
|
baseFilter = _scopedWorkflowFilter(context)
|
|
|
|
|
recordFilter = dict(baseFilter) if baseFilter else {}
|
|
|
|
|
recordFilter["isTemplate"] = False
|
|
|
|
|
visibility_scope = _workflowVisibilityForContext(context)
|
|
|
|
|
recordFilter: dict = {"isTemplate": False}
|
|
|
|
|
|
|
|
|
|
if active is not None:
|
|
|
|
|
recordFilter["active"] = active
|
|
|
|
|
@ -806,10 +906,11 @@ def get_system_workflows(
|
|
|
|
|
userId = str(context.user.id) if context.user else None
|
|
|
|
|
adminMandateIds = []
|
|
|
|
|
if userId and not context.isPlatformAdmin:
|
|
|
|
|
userMandateIds = _getUserMandateIds(userId)
|
|
|
|
|
adminMandateIds = _getAdminMandateIds(userId, userMandateIds)
|
|
|
|
|
accessibleMandateIds = _getAccessibleMandateIds(userId)
|
|
|
|
|
adminMandateIds = _getAdminMandateIds(userId, accessibleMandateIds)
|
|
|
|
|
|
|
|
|
|
fkSortField = _firstFkSortFieldForWorkflows(paginationParams)
|
|
|
|
|
# Visibility uses SQL OR (mandate + instance); FK-label sort path cannot express that.
|
|
|
|
|
fkSortField = _firstFkSortFieldForWorkflows(paginationParams) if not visibility_scope else None
|
|
|
|
|
if fkSortField:
|
|
|
|
|
from modules.routes.routeHelpers import getRecordsetPaginatedWithFkSort, applyFiltersAndSort
|
|
|
|
|
_COMPUTED_FIELDS = {"lastStartedAt", "runCount", "isRunning"}
|
|
|
|
|
@ -906,6 +1007,7 @@ def get_system_workflows(
|
|
|
|
|
else:
|
|
|
|
|
result = _getWorkflowsJoinedPaginated(
|
|
|
|
|
db, recordFilter if recordFilter else {}, paginationParams,
|
|
|
|
|
visibility_scope=visibility_scope,
|
|
|
|
|
)
|
|
|
|
|
pageItems = result.get("items", [])
|
|
|
|
|
totalItems = result.get("totalItems", 0)
|
|
|
|
|
@ -1052,12 +1154,18 @@ def _enrichedFilterValues(
|
|
|
|
|
if _isTimestampColumn(modelClass, column):
|
|
|
|
|
return JSONResponse(content=[])
|
|
|
|
|
|
|
|
|
|
visibility_scope = (
|
|
|
|
|
_workflowVisibilityForContext(context) if modelClass == AutoWorkflow else None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if column in ("mandateLabel", "mandateId"):
|
|
|
|
|
baseFilter = scopeFilter(context)
|
|
|
|
|
recordFilter = dict(baseFilter) if baseFilter else {}
|
|
|
|
|
if modelClass == AutoWorkflow:
|
|
|
|
|
recordFilter["isTemplate"] = False
|
|
|
|
|
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["mandateId"]) or []
|
|
|
|
|
if visibility_scope:
|
|
|
|
|
items = [r for r in items if _workflowIsVisible(r, visibility_scope)]
|
|
|
|
|
allVals = {r.get("mandateId") for r in items}
|
|
|
|
|
mandateIds = sorted(v for v in allVals if v)
|
|
|
|
|
hasEmpty = None in allVals or "" in allVals
|
|
|
|
|
@ -1072,7 +1180,9 @@ def _enrichedFilterValues(
|
|
|
|
|
recordFilter = dict(baseFilter) if baseFilter else {}
|
|
|
|
|
if modelClass == AutoWorkflow:
|
|
|
|
|
recordFilter["isTemplate"] = False
|
|
|
|
|
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["featureInstanceId"]) or []
|
|
|
|
|
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["featureInstanceId", "targetFeatureInstanceId", "mandateId"]) or []
|
|
|
|
|
if visibility_scope:
|
|
|
|
|
items = [r for r in items if _workflowIsVisible(r, visibility_scope)]
|
|
|
|
|
allVals = {r.get("featureInstanceId") for r in items}
|
|
|
|
|
instanceIds = sorted(v for v in allVals if v)
|
|
|
|
|
hasEmpty = None in allVals or "" in allVals
|
|
|
|
|
@ -1095,7 +1205,11 @@ def _enrichedFilterValues(
|
|
|
|
|
if column == "workflowLabel":
|
|
|
|
|
baseFilter = scopeFilter(context)
|
|
|
|
|
recordFilter = dict(baseFilter) if baseFilter else {}
|
|
|
|
|
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["workflowId", "label"]) or []
|
|
|
|
|
if modelClass == AutoWorkflow:
|
|
|
|
|
recordFilter["isTemplate"] = False
|
|
|
|
|
items = db.getRecordset(modelClass, recordFilter=recordFilter or None, fieldFilter=["workflowId", "label", "mandateId", "featureInstanceId", "targetFeatureInstanceId"]) or []
|
|
|
|
|
if visibility_scope:
|
|
|
|
|
items = [r for r in items if _workflowIsVisible(r, visibility_scope)]
|
|
|
|
|
labels = set()
|
|
|
|
|
wfIds = set()
|
|
|
|
|
hasEmpty = False
|
|
|
|
|
|