File indexing completed on 2026-04-25 08:29:11
0001 """
0002 Workflow-specific views for the SWF monitor application.
0003 """
0004
0005 from django.shortcuts import render, get_object_or_404
0006 from django.http import JsonResponse
0007 from django.contrib.auth.decorators import login_required
0008 from django.urls import reverse
0009 from django.db.models import Count
0010 from django.utils import timezone
0011
0012 from .models import StfFile
0013 from .workflow_models import WorkflowDefinition, WorkflowExecution
0014
0015
0016 @login_required
0017 def workflows_home(request):
0018 """Workflows landing page with links to different workflow views."""
0019 return render(request, 'monitor_app/workflows_home.html')
0020
0021
0022 @login_required
0023 def workflow_definitions_list(request):
0024 """
0025 Professional workflow definitions list view using server-side DataTables.
0026 """
0027
0028 workflow_name = request.GET.get('workflow_name')
0029 workflow_type = request.GET.get('workflow_type')
0030 created_by = request.GET.get('created_by')
0031
0032
0033 workflow_names = WorkflowDefinition.objects.values_list('workflow_name', flat=True).distinct().order_by('workflow_name')
0034 workflow_types = WorkflowDefinition.objects.values_list('workflow_type', flat=True).distinct().order_by('workflow_type')
0035 created_bys = WorkflowDefinition.objects.values_list('created_by', flat=True).distinct().order_by('created_by')
0036
0037 columns = [
0038 {'name': 'workflow_name', 'title': 'Workflow Name', 'orderable': True},
0039 {'name': 'version', 'title': 'Version', 'orderable': True},
0040 {'name': 'workflow_type', 'title': 'Type', 'orderable': True},
0041 {'name': 'created_by', 'title': 'Created By', 'orderable': True},
0042 {'name': 'created_at', 'title': 'Created', 'orderable': True},
0043 {'name': 'execution_count', 'title': 'Executions', 'orderable': True},
0044 {'name': 'actions', 'title': 'Actions', 'orderable': False},
0045 ]
0046
0047 filter_fields = [
0048 {'name': 'workflow_type', 'label': 'Type', 'type': 'select'},
0049 {'name': 'created_by', 'label': 'Created By', 'type': 'select'},
0050 ]
0051
0052 context = {
0053 'table_title': 'Workflow Definitions',
0054 'table_description': 'View and manage workflow templates and configurations.',
0055 'ajax_url': reverse('monitor_app:workflow_definitions_datatable_ajax'),
0056 'filter_counts_url': reverse('monitor_app:workflow_definitions_filter_counts'),
0057 'columns': columns,
0058 'filter_fields': filter_fields,
0059 'workflow_names': list(workflow_names),
0060 'workflow_types': list(workflow_types),
0061 'created_bys': list(created_bys),
0062 'selected_workflow_name': workflow_name,
0063 'selected_workflow_type': workflow_type,
0064 'selected_created_by': created_by,
0065 }
0066 return render(request, 'monitor_app/workflow_definitions_list.html', context)
0067
0068
0069 def workflow_definitions_datatable_ajax(request):
0070 """AJAX endpoint for server-side DataTables processing of workflow definitions."""
0071 from .utils import DataTablesProcessor, format_datetime
0072
0073 columns = ['workflow_name', 'version', 'workflow_type', 'created_by', 'created_at', 'execution_count', 'actions']
0074 dt = DataTablesProcessor(request, columns, default_order_column=4, default_order_direction='desc')
0075
0076
0077 queryset = WorkflowDefinition.objects.annotate(
0078 execution_count=Count('executions')
0079 )
0080
0081
0082 workflow_name = request.GET.get('workflow_name')
0083 if workflow_name:
0084 queryset = queryset.filter(workflow_name=workflow_name)
0085
0086 workflow_type = request.GET.get('workflow_type')
0087 if workflow_type:
0088 queryset = queryset.filter(workflow_type=workflow_type)
0089
0090 created_by = request.GET.get('created_by')
0091 if created_by:
0092 queryset = queryset.filter(created_by=created_by)
0093
0094
0095 records_total = WorkflowDefinition.objects.count()
0096 search_fields = ['workflow_name', 'version', 'workflow_type', 'created_by']
0097 queryset = dt.apply_search(queryset, search_fields)
0098 records_filtered = queryset.count()
0099
0100 queryset = queryset.order_by(dt.get_order_by())
0101 definitions = dt.apply_pagination(queryset)
0102
0103
0104 data = []
0105 for definition in definitions:
0106 data.append([
0107 f'<a href="{reverse("monitor_app:workflow_definition_detail", args=[definition.workflow_name, definition.version])}" class="text-decoration-none">{definition.workflow_name}</a>',
0108 definition.version,
0109 definition.workflow_type,
0110 definition.created_by,
0111 format_datetime(definition.created_at),
0112 definition.execution_count,
0113 f'<a href="{reverse("monitor_app:workflow_definition_detail", args=[definition.workflow_name, definition.version])}" class="btn btn-sm btn-outline-primary">View</a>'
0114 ])
0115
0116 return dt.create_response(data, records_total, records_filtered)
0117
0118
0119 def workflow_definitions_filter_counts(request):
0120 """AJAX endpoint for dynamic filter counts."""
0121 workflow_type_counts = WorkflowDefinition.objects.values('workflow_type').annotate(count=Count('id')).order_by('workflow_type')
0122 created_by_counts = WorkflowDefinition.objects.values('created_by').annotate(count=Count('id')).order_by('created_by')
0123
0124 return JsonResponse({
0125 'workflow_type': list(workflow_type_counts),
0126 'created_by': list(created_by_counts),
0127 })
0128
0129
0130 @login_required
0131 def workflow_executions_list(request):
0132 """
0133 Professional workflow executions list view using server-side DataTables.
0134 """
0135
0136 workflow = request.GET.get('workflow')
0137 status = request.GET.get('status')
0138 executed_by = request.GET.get('executed_by')
0139 namespace = request.GET.get('namespace')
0140
0141
0142 workflows = WorkflowExecution.objects.select_related('workflow_definition').values_list(
0143 'workflow_definition__workflow_name', flat=True
0144 ).distinct().order_by('workflow_definition__workflow_name')
0145
0146 statuses = WorkflowExecution.objects.values_list('status', flat=True).distinct().order_by('status')
0147 executed_bys = WorkflowExecution.objects.values_list('executed_by', flat=True).distinct().order_by('executed_by')
0148 namespaces = WorkflowExecution.objects.exclude(namespace__isnull=True).exclude(namespace='').values_list(
0149 'namespace', flat=True
0150 ).distinct().order_by('namespace')
0151
0152 columns = [
0153 {'name': 'execution_id', 'title': 'Execution ID', 'orderable': True},
0154 {'name': 'workflow', 'title': 'Workflow', 'orderable': True},
0155 {'name': 'namespace', 'title': 'Namespace', 'orderable': True},
0156 {'name': 'status', 'title': 'Status', 'orderable': True},
0157 {'name': 'stf_count', 'title': 'STFs', 'orderable': False},
0158 {'name': 'executed_by', 'title': 'Executed By', 'orderable': True},
0159 {'name': 'start_time', 'title': 'Started', 'orderable': True},
0160 {'name': 'duration', 'title': 'Duration', 'orderable': True},
0161 {'name': 'actions', 'title': 'Actions', 'orderable': False},
0162 ]
0163
0164 filter_fields = [
0165 {'name': 'status', 'label': 'Status', 'type': 'select'},
0166 {'name': 'executed_by', 'label': 'Executed By', 'type': 'select'},
0167 ]
0168
0169 context = {
0170 'table_title': 'Workflow Executions',
0171 'table_description': 'Monitor workflow execution instances and their status.',
0172 'ajax_url': reverse('monitor_app:workflow_executions_datatable_ajax'),
0173 'filter_counts_url': reverse('monitor_app:workflow_executions_filter_counts'),
0174 'columns': columns,
0175 'filter_fields': filter_fields,
0176 'workflows': list(workflows),
0177 'statuses': list(statuses),
0178 'executed_bys': list(executed_bys),
0179 'namespaces': list(namespaces),
0180 'selected_workflow': workflow,
0181 'selected_status': status,
0182 'selected_executed_by': executed_by,
0183 'selected_namespace': namespace,
0184 }
0185 return render(request, 'monitor_app/workflow_executions_list.html', context)
0186
0187
0188 def workflow_executions_datatable_ajax(request):
0189 """AJAX endpoint for server-side DataTables processing of workflow executions."""
0190 from .utils import DataTablesProcessor, format_datetime, format_duration
0191 from .workflow_models import WorkflowMessage
0192
0193 columns = ['execution_id', 'workflow', 'namespace', 'status', 'stf_count', 'executed_by', 'start_time', 'duration', 'actions']
0194 dt = DataTablesProcessor(request, columns, default_order_column=6, default_order_direction='desc')
0195
0196
0197 queryset = WorkflowExecution.objects.select_related('workflow_definition')
0198
0199
0200 workflow = request.GET.get('workflow')
0201 if workflow:
0202 queryset = queryset.filter(workflow_definition__workflow_name=workflow)
0203
0204 status = request.GET.get('status')
0205 if status:
0206 queryset = queryset.filter(status=status)
0207
0208 executed_by = request.GET.get('executed_by')
0209 if executed_by:
0210 queryset = queryset.filter(executed_by=executed_by)
0211
0212 namespace = request.GET.get('namespace')
0213 if namespace:
0214 queryset = queryset.filter(namespace=namespace)
0215
0216
0217 records_total = WorkflowExecution.objects.count()
0218 search_fields = ['execution_id', 'workflow_definition__workflow_name', 'status', 'executed_by']
0219 queryset = dt.apply_search(queryset, search_fields)
0220 records_filtered = queryset.count()
0221
0222 queryset = queryset.order_by(dt.get_order_by())
0223 executions = dt.apply_pagination(queryset)
0224
0225
0226 data = []
0227 for execution in executions:
0228
0229 if execution.end_time:
0230 duration = execution.end_time - execution.start_time
0231 duration_str = format_duration(duration)
0232 elif execution.status == 'running':
0233 duration = timezone.now() - execution.start_time
0234 duration_str = format_duration(duration, is_ongoing=True)
0235 else:
0236 duration_str = '-'
0237
0238
0239 run_ids = WorkflowMessage.objects.filter(
0240 execution_id=execution.execution_id,
0241 run_id__isnull=False,
0242 ).values_list('run_id', flat=True).distinct()
0243 run_numbers = [int(r) for r in run_ids if r]
0244 stf_count = StfFile.objects.filter(run__run_number__in=run_numbers).count()
0245
0246
0247 if execution.namespace:
0248 namespace_link = f'<a href="{reverse("monitor_app:namespace_detail", args=[execution.namespace])}">{execution.namespace}</a>'
0249 else:
0250 namespace_link = ''
0251
0252 data.append([
0253 f'<a href="{reverse("monitor_app:workflow_execution_detail", args=[execution.execution_id])}" class="text-decoration-none">{execution.execution_id}</a>',
0254 f"{execution.workflow_definition.workflow_name} v{execution.workflow_definition.version}",
0255 namespace_link,
0256 execution.status,
0257 str(stf_count),
0258 execution.executed_by,
0259 format_datetime(execution.start_time),
0260 duration_str,
0261 f'<a href="{reverse("monitor_app:workflow_execution_detail", args=[execution.execution_id])}" class="btn btn-sm btn-outline-primary">View</a>'
0262 ])
0263
0264 return dt.create_response(data, records_total, records_filtered)
0265
0266
0267 def workflow_executions_filter_counts(request):
0268 """AJAX endpoint for dynamic filter counts."""
0269 status_counts = WorkflowExecution.objects.values('status').annotate(count=Count('id')).order_by('status')
0270 executed_by_counts = WorkflowExecution.objects.values('executed_by').annotate(count=Count('id')).order_by('executed_by')
0271
0272 return JsonResponse({
0273 'status': list(status_counts),
0274 'executed_by': list(executed_by_counts),
0275 })
0276
0277
0278 @login_required
0279 def namespaces_list(request):
0280 """
0281 Namespace list view using server-side DataTables.
0282 Primary source is the Namespace model with activity counts.
0283 """
0284 columns = [
0285 {'name': 'name', 'title': 'Namespace', 'orderable': True},
0286 {'name': 'owner', 'title': 'Owner', 'orderable': True},
0287 {'name': 'description', 'title': 'Description', 'orderable': False},
0288 {'name': 'agent_count', 'title': 'Agents', 'orderable': True},
0289 {'name': 'execution_count', 'title': 'Executions', 'orderable': True},
0290 {'name': 'message_count', 'title': 'Messages', 'orderable': True},
0291 {'name': 'updated_at', 'title': 'Modified', 'orderable': True},
0292 ]
0293
0294 context = {
0295 'table_title': 'Namespaces',
0296 'table_description': 'View registered namespaces and their associated agents, executions, and messages.',
0297 'ajax_url': reverse('monitor_app:namespaces_datatable_ajax'),
0298 'columns': columns,
0299 }
0300 return render(request, 'monitor_app/namespaces_list.html', context)
0301
0302
0303 def namespaces_datatable_ajax(request):
0304 """AJAX endpoint for server-side DataTables processing of namespaces."""
0305 from django.db.models import Count, Q
0306 from .utils import DataTablesProcessor
0307 from .models import SystemAgent
0308 from .workflow_models import WorkflowMessage, Namespace
0309
0310 columns = ['name', 'owner', 'description', 'agent_count', 'execution_count', 'message_count', 'updated_at']
0311 dt = DataTablesProcessor(request, columns, default_order_column=0, default_order_direction='asc')
0312
0313
0314 queryset = Namespace.objects.all()
0315 records_total = queryset.count()
0316
0317
0318 if dt.search_value:
0319 queryset = queryset.filter(
0320 Q(name__icontains=dt.search_value) |
0321 Q(owner__icontains=dt.search_value) |
0322 Q(description__icontains=dt.search_value)
0323 )
0324 records_filtered = queryset.count()
0325
0326
0327 order_column = dt.order_column
0328 if order_column in ['name', 'owner', 'description', 'updated_at']:
0329 order_by = dt.get_order_by()
0330 queryset = queryset.order_by(order_by)
0331 else:
0332 queryset = queryset.order_by('name')
0333
0334
0335 queryset = queryset[dt.start:dt.start + dt.length]
0336
0337
0338 agent_counts = dict(
0339 SystemAgent.objects.exclude(namespace__isnull=True).exclude(namespace='')
0340 .values('namespace').annotate(c=Count('id')).values_list('namespace', 'c')
0341 )
0342 execution_counts = dict(
0343 WorkflowExecution.objects.exclude(namespace__isnull=True).exclude(namespace='')
0344 .values('namespace').annotate(c=Count('execution_id')).values_list('namespace', 'c')
0345 )
0346 message_counts = dict(
0347 WorkflowMessage.objects.exclude(namespace__isnull=True).exclude(namespace='')
0348 .values('namespace').annotate(c=Count('pk')).values_list('namespace', 'c')
0349 )
0350
0351
0352 data = []
0353 for ns in queryset:
0354 description = ns.description if ns.description else '-'
0355 updated_at = ns.updated_at.strftime('%Y-%m-%d %H:%M')
0356
0357 data.append([
0358 f'<a href="{reverse("monitor_app:namespace_detail", args=[ns.name])}">{ns.name}</a>',
0359 ns.owner,
0360 description,
0361 str(agent_counts.get(ns.name, 0)),
0362 str(execution_counts.get(ns.name, 0)),
0363 str(message_counts.get(ns.name, 0)),
0364 updated_at,
0365 ])
0366
0367 return dt.create_response(data, records_total, records_filtered)
0368
0369
0370 @login_required
0371 def workflow_definition_detail(request, workflow_name, version):
0372 """Detail view for a specific workflow definition."""
0373 definition = get_object_or_404(WorkflowDefinition, workflow_name=workflow_name, version=version)
0374
0375
0376 total_executions = definition.executions.count()
0377
0378 context = {
0379 'definition': definition,
0380 'total_executions': total_executions,
0381 }
0382 return render(request, 'monitor_app/workflow_definition_detail.html', context)
0383
0384
0385 @login_required
0386 def workflow_execution_detail(request, execution_id):
0387 """Detail view for a specific workflow execution."""
0388 execution = get_object_or_404(WorkflowExecution, execution_id=execution_id)
0389
0390
0391 duration_text = None
0392 if execution.end_time and execution.start_time:
0393 delta = execution.end_time - execution.start_time
0394 total_seconds = delta.total_seconds()
0395
0396 minutes = int(total_seconds // 60)
0397 seconds = total_seconds % 60
0398
0399 if minutes > 0:
0400 duration_text = f"{minutes} minutes, {seconds:.2f} seconds"
0401 else:
0402 duration_text = f"{seconds:.2f} seconds"
0403
0404 context = {
0405 'execution': execution,
0406 'duration_text': duration_text,
0407 }
0408 return render(request, 'monitor_app/workflow_execution_detail.html', context)