From d7a450521c2adf4074bde111c8b0a57c93200e47 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 31 Oct 2016 17:09:01 -0400 Subject: [PATCH] Added console dynamic source, console output only flag in tasks API --- app/api/tasks.py | 18 +++++++++-- app/scheduler.py | 2 +- app/static/app/js/Console.jsx | 30 +++++++++++++++++++ app/static/app/js/components/TaskListItem.jsx | 28 +++++++++++++---- app/static/app/js/css/TaskListItem.scss | 4 +++ app/tests/classes.py | 1 - app/tests/test_api.py | 22 ++++++++++++++ 7 files changed, 96 insertions(+), 9 deletions(-) diff --git a/app/api/tasks.py b/app/api/tasks.py index 5519236a..00c68330 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -48,6 +48,12 @@ class TaskViewSet(viewsets.ViewSet): raise exceptions.NotFound() return project + @staticmethod + def task_output_only(request, task): + line_num = max(0, int(request.query_params.get('line', 0))) + output = task.console_output or "" + return '\n'.join(output.split('\n')[line_num:]) + def list(self, request, project_pk=None): project = self.get_and_check_project(request, project_pk) tasks = self.queryset.filter(project=project_pk) @@ -61,8 +67,16 @@ class TaskViewSet(viewsets.ViewSet): task = self.queryset.get(pk=pk, project=project_pk) except ObjectDoesNotExist: raise exceptions.NotFound() - serializer = TaskSerializer(task) - return Response(serializer.data) + + response_data = None + + if request.query_params.get('output_only', '').lower() in ['true', '1']: + response_data = self.task_output_only(request, task) + else: + serializer = TaskSerializer(task) + response_data = serializer.data + + return Response(response_data) def create(self, request, project_pk=None): project = self.get_and_check_project(request, project_pk, ('change_project', )) diff --git a/app/scheduler.py b/app/scheduler.py index c8209043..d4a54c34 100644 --- a/app/scheduler.py +++ b/app/scheduler.py @@ -84,7 +84,7 @@ def setup(): try: scheduler.start() scheduler.add_job(update_nodes_info, 'interval', seconds=30) - scheduler.add_job(process_pending_tasks, 'interval', seconds=5) + scheduler.add_job(process_pending_tasks, 'interval', seconds=15) except SchedulerAlreadyRunningError: logger.warn("Scheduler already running (this is OK while testing)") diff --git a/app/static/app/js/Console.jsx b/app/static/app/js/Console.jsx index c3ebd26a..35c7669a 100644 --- a/app/static/app/js/Console.jsx +++ b/app/static/app/js/Console.jsx @@ -26,6 +26,36 @@ class Console extends React.Component { componentDidMount(){ this.checkAutoscroll(); + + // Dynamic source? + if (this.props.source !== undefined){ + let currentLineNumber = 0; + + const updateFromSource = () => { + let sourceUrl = typeof this.props.source === 'function' ? + this.props.source(currentLineNumber) : + this.props.source; + + // Fetch + this.sourceRequest = $.get(sourceUrl, text => { + let lines = text.split("\n"); + lines.forEach(line => this.addLine(line)); + currentLineNumber += (lines.length - 1); + }) + .always(() => { + if (this.props.refreshInterval !== undefined){ + this.sourceTimeout = setTimeout(updateFromSource, this.props.refreshInterval); + } + }); + }; + + updateFromSource(); + } + } + + componentWillUnmount(){ + if (this.sourceTimeout) clearTimeout(this.sourceTimeout); + if (this.sourceRequest) this.sourceRequest.abort(); } setRef(domNode){ diff --git a/app/static/app/js/components/TaskListItem.jsx b/app/static/app/js/components/TaskListItem.jsx index 9f892a00..f49eb209 100644 --- a/app/static/app/js/components/TaskListItem.jsx +++ b/app/static/app/js/components/TaskListItem.jsx @@ -11,9 +11,11 @@ class TaskListItem extends React.Component { } this.toggleExpanded = this.toggleExpanded.bind(this); + this.consoleOutputUrl = this.consoleOutputUrl.bind(this); } componentDidMount(){ + } toggleExpanded(){ @@ -22,17 +24,32 @@ class TaskListItem extends React.Component { }); } + consoleOutputUrl(line){ + return `/api/projects/${this.props.data.project}/tasks/${this.props.data.id}/?output_only=true&line=${line}`; + } + render() { let name = this.props.data.name !== null ? this.props.data.name : `Task #${this.props.data.id}`; let expanded = ""; if (this.state.expanded){ expanded = ( -
-
- Status: Running
+
+
+
+
+ Status: Running
+
- +
+ +
+
+
- ); +
+ ); } return ( diff --git a/app/static/app/js/css/TaskListItem.scss b/app/static/app/js/css/TaskListItem.scss index 781f52b7..fb485dee 100644 --- a/app/static/app/js/css/TaskListItem.scss +++ b/app/static/app/js/css/TaskListItem.scss @@ -6,6 +6,10 @@ .row{ margin: 0; padding: 4px; + + .no-padding{ + padding: 0; + } } .name{ diff --git a/app/tests/classes.py b/app/tests/classes.py index 5b9007db..981c3275 100644 --- a/app/tests/classes.py +++ b/app/tests/classes.py @@ -2,7 +2,6 @@ from django.test import TestCase from django.contrib.auth.models import User, Group from app.models import Project from app.boot import boot -from app import scheduler class BootTestCase(TestCase): ''' diff --git a/app/tests/test_api.py b/app/tests/test_api.py index 55f73d1e..726ce7ff 100644 --- a/app/tests/test_api.py +++ b/app/tests/test_api.py @@ -101,6 +101,28 @@ class TestApi(BootTestCase): # images_count field exists self.assertTrue(res.data["images_count"] == 0) + # Get console output + res = client.get('/api/projects/{}/tasks/{}/?output_only=true'.format(project.id, task.id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue(res.data == "") + + task.console_output = "line1\nline2\nline3" + task.save() + + res = client.get('/api/projects/{}/tasks/{}/?output_only=true'.format(project.id, task.id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue(res.data == task.console_output) + + # Console output with line num + res = client.get('/api/projects/{}/tasks/{}/?output_only=true&line=2'.format(project.id, task.id)) + self.assertTrue(res.data == "line3") + + # Console output with line num out of bounds + res = client.get('/api/projects/{}/tasks/{}/?output_only=true&line=3'.format(project.id, task.id)) + self.assertTrue(res.data == "") + res = client.get('/api/projects/{}/tasks/{}/?output_only=true&line=-1'.format(project.id, task.id)) + self.assertTrue(res.data == task.console_output) + # Cannot list task details for a task belonging to a project we don't have access to res = client.get('/api/projects/{}/tasks/{}/'.format(other_project.id, other_task.id)) self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)