From 9f4e667908573cec0d948d80082a6585b15e2250 Mon Sep 17 00:00:00 2001
From: Stefano Alberto Russo <stefano.russo@gmail.com>
Date: Thu, 8 Apr 2021 20:28:04 +0200
Subject: [PATCH] UI/UX improvements for new tasks and navigation. Fixes.

---
 .../management/commands/core_app_populate.py  |   6 +-
 .../templates/components/container.html       |   2 +-
 .../core_app/templates/containers.html        |  11 +-
 .../core_app/templates/create_task.html       |  94 +++----
 .../code/rosetta/core_app/templates/main.html |  13 +-
 .../rosetta/core_app/templates/tasks.html     |   2 +-
 .../webapp/code/rosetta/core_app/views.py     | 248 ++++++++++--------
 7 files changed, 201 insertions(+), 175 deletions(-)

diff --git a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py
index feadcdf..3d74c37 100644
--- a/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py
+++ b/services/webapp/code/rosetta/core_app/management/commands/core_app_populate.py
@@ -48,9 +48,9 @@ class Command(BaseCommand):
   To change it, head to the <a href="/admin">admin</a> page and edit the <code>Text</code> model.
   <br/><br/>
   The default installation provides a test user register with email <code>testuser@rosetta.platform</code>
-  and password <code>testpass</code>, which you can use to login on the menu on the right or using the link
-  below and give Rosetta a try immediately. If you run with the default docker-compose file (i.e. you just
-  run <code>rosetta/setup</code>), then you will also have a few demo computing resources you can play with
+  and password <code>testpass</code>, which you can use to login on the menu on the rightand give Rosetta
+  a try immediately. If you run with the default docker-compose file (i.e. you just run 
+  <code>rosetta/setup</code>), then you will also have a few demo computing resources you can play with
   out-of-the-box, including a small Slurm cluster. Otherwise, you will need to setup your own computing
   resources either platform-wide or as user.
 </div>
diff --git a/services/webapp/code/rosetta/core_app/templates/components/container.html b/services/webapp/code/rosetta/core_app/templates/components/container.html
index c5838cf..ccdb78f 100644
--- a/services/webapp/code/rosetta/core_app/templates/components/container.html
+++ b/services/webapp/code/rosetta/core_app/templates/components/container.html
@@ -112,7 +112,7 @@
         </div>
         
         <div style="margin-bottom:13px; margin-top: 3px; text-align:center">
-        <a href="/create_task?task_container_uuid={{ container.uuid }}" class="btn btn-light" style="border: #c0c0c0 1px solid">&nbsp;<i class="fa fa-play" style="color:green"></i></a>
+        <a href="/create_task?task_container_uuid={{ container.uuid }}&step=two" class="btn btn-light" style="border: #c0c0c0 1px solid">&nbsp;<i class="fa fa-play" style="color:green"></i></a>
         </div>    
       
       
diff --git a/services/webapp/code/rosetta/core_app/templates/containers.html b/services/webapp/code/rosetta/core_app/templates/containers.html
index badcfc4..22f1299 100644
--- a/services/webapp/code/rosetta/core_app/templates/containers.html
+++ b/services/webapp/code/rosetta/core_app/templates/containers.html
@@ -12,15 +12,24 @@
       {% if data.container %}
       <h1><a href="/containers">Containers</a> <span style="font-size:18px"> / {{ data.container.name }}</span></h1>
       {% else %}
+      {% if data.mode == 'new_task' %}
+      <h1>New task</h1>
+      <hr/>
+      <h3>Step 1: choose software container.</h3>
+      <br/> 
+      {% else %}
       <h1>Containers</h1>
-
       <hr/>
+      {% endif %}
+ 
 
       
       
       <div class="form-filter" style="margin-bottom:20px">
         <form action="" method="POST">
           
+          <input type="hidden" name="mode" value="{{data.mode}}">
+                    
           <input type="text" class="form-control" id="search_text" name="search_text" placeholder="Search..." style="width:200px; margin:0; display:inline" value="{{data.search_text}}" autofocus>
 
           <select class="form-control" id="search_type" name="search_type" style="width:120px; margin:0; display:inline">
diff --git a/services/webapp/code/rosetta/core_app/templates/create_task.html b/services/webapp/code/rosetta/core_app/templates/create_task.html
index 3bfd578..8de1770 100644
--- a/services/webapp/code/rosetta/core_app/templates/create_task.html
+++ b/services/webapp/code/rosetta/core_app/templates/create_task.html
@@ -11,45 +11,49 @@
       <h1>New Task</h1> 
       <hr>
 
-      {% if data.step == 'one' %}
 
-          <h3>Step 1: name, container and computing.</h3> 
-          
-          <br/>
-          
+      {% if data.step == 'two' %} 
+      <h3>Step 2: set task name and computing.</h3> 
+      {% elif data.step == 'three' %}  
+      <h3>Step 3: add authentication and computing details.</h3>    
+      {% endif %}
+
+      <br/>
+
+
+      <div style="width:300px; float:left; border: #e0e0e0 solid 1px; margin:10px; background:#f8f8f8; margin-bottom:15px">
+        <div style="padding:10px; margin-top:5px; text-align:center; border-bottom: {{data.task_container.color}} solid 10px; ">
+        <a href="?uuid={{ container.uuid }}">{{ data.task_container.name }}</a>&nbsp; 
+        {% if data.task_container.type == 'docker' %}<img src="/static/img/docker-logo.svg" style="height:18px; width:18px; margin-bottom:4px" />{% endif %}
+        {% if data.task_container.type == 'singularity' %}<img src="/static/img/singularity-logo.svg" style="height:18px; width:18px; margin-bottom:4px" />{% endif %}
+        </div>  
+        <div style="padding:10px; height: 64px; vertical-align: middle;">
+        <b>Image:</b> {{ data.task_container.image_name }}<br/>
+        <b>Version:</b> {{ data.task_container.image_tag }}
+        </div>
+      </div>
+
+      {% if data.step == 'two' %}
+
           <form action="/create_task/" method="POST">
           {% csrf_token %}
-          <input type="hidden" name="step" value="one" />
+          <input type="hidden" name="step" value="{{ data.next_step }}" />
+          <input type="hidden" name="task_container_uuid" value="{{data.task_container.uuid}}">
 
+      <div style="width:300px; float:left; border: #e0e0e0 solid 0px; margin:10px; background:#f8f8f8; margin-bottom:15px">
 
-          <table class="dashboard" style="max-width:700px">
+          <table style="max-width:100%; border: #e0e0e0 solid 1px; margin:0">
           <tr><td colspan="2"></td></tr>
 
            <tr>
             <td><b>Task name </b></td>
             <td>
-             <input type="text" name="task_name" value="" placeholder="" size="23" required />
+             <input type="text" name="task_name" value="" placeholder="" size="" required />
             </td>
            </tr>
 
-          <tr>
-            <td><b>Task container</b></td><td>
-              {% if data.task_container %}
-              <select name="task_container_uuid">
-              <option value="{{data.task_container.uuid}}" selected>{{data.task_container.name}} ({{data.task_container.type.title}})</option>
-              </select>
-              {% else %}
-              <select name="task_container_uuid" >
-              {% for container in data.containers %}
-              <option value="{{container.uuid}}">{{container.name}} ({{container.type.title}})</option>
-              {% endfor %}
-              </select>
-              {% endif %}
-            </td>
-           </tr>
-           
            <tr>
-            <td><b>Computing resource</b></td><td>
+            <td><b>Computing</b></td><td>
               <select name="task_computing_uuid" >
               {% for computing in data.computings %}}
               <option value="{{ computing.uuid }}">{{ computing.name}}</option>
@@ -59,38 +63,30 @@
            </tr>
 
            <tr>
-           <td colspan=2 align=center style="padding:20px">
+           <td colspan=2 align=center style="padding:9px">
            <input type="submit" value="Next">
            </td>
            </tr>
           </table>
+          </div>
           </form>
 
-      {% elif data.step == 'two' %}
-
-          <h3>Step 2: add authentication and computing details</h3> 
-
-          <br/>
-          <table class="dashboard" style="max-width:700px">
-          <tr><td colspan="2"></td></tr>
+      {% elif data.step == 'three' %}
+          
+          
+          <div style="width:300px; float:left; border: #e0e0e0 solid 0px; margin:10px; background:#f8f8f8; margin-bottom:15px">
+          <table style="width:100%; height:126px;  border: #e0e0e0 solid 1px; margin:0">
 
-           <tr>
+           <tr valign="bottom">
             <td><b>Task name </b></td>
             <td>
-             <input type="text" name="task_name_RECAP" value="{{ data.task_name }}" placeholder="" size="23" disabled />
+             <input type="text" name="task_name_RECAP" value="{{ data.task_name }}" placeholder="" size="" disabled />
             </td>
            </tr>
 
-          <tr>
-            <td><b>Task container</b></td><td>
-              <select name="task_container_uuid_RECAP">
-              <option value="" selected>{{data.task_container.name}} ({{data.task_container.type.title}})</option>
-              </select>
-            </td>
-           </tr>
            
-           <tr>
-            <td><b>Computing resource</b></td><td>
+           <tr valign="top">
+            <td><b>Computing</b></td><td>
               <select name="task_computing_uuid_RECAP" >
               <option value="">{{ data.task_computing.name}}</option>
               </select>
@@ -98,8 +94,10 @@
            </tr>
 
           </table>
+          </div>
+          
+          <div style="width:640px; float:left; border: #e0e0e0 solid 0px; margin:10px; background:#f8f8f8; margin-bottom:15px">
           
-          <br>          
           {% if data.task_container.type == 'singularity' and not data.task_container.supports_dynamic_ports %}
           <div> <p style="font-size:15px; max-width:700px; margin-bottom:20px; margin-left:5px">
           <i class="fa fa-exclamation-triangle" style="color:orange"></i> This container does not support dynamic ports and you are running it with Singularity, without network insulation. This means that if the container port is already occupied, it will not be able to start.
@@ -114,7 +112,8 @@
         
           <form action="/create_task/" method="POST">
           {% csrf_token %}
-          <input type="hidden" name="step" value="two" />
+          <input type="hidden" name="task_container_uuid" value="{{data.task_container.uuid}}">
+          <input type="hidden" name="step" value="{{ data.next_step }}" />
           <input type="hidden" name="task_name" value="{{ data.task_name }}" />
           <input type="hidden" name="task_container_uuid" value="{{ data.task_container.uuid }}" />
           <input type="hidden" name="task_computing_uuid" value="{{ data.task_computing.uuid }}" />
@@ -180,7 +179,7 @@
             </td>
            </tr>  -->
            
-           {% if data.task_computing.type == 'slurm' %}
+           {% if data.task_computing.access_method == 'slurm+ssh' %}
            <tr>
             <td><b>Computing options</b></td>
             <td>
@@ -207,6 +206,7 @@
            </tr>
           </table>
           </form>
+          </div>
 
 
       {% else %}
diff --git a/services/webapp/code/rosetta/core_app/templates/main.html b/services/webapp/code/rosetta/core_app/templates/main.html
index 6f9656e..a18904a 100644
--- a/services/webapp/code/rosetta/core_app/templates/main.html
+++ b/services/webapp/code/rosetta/core_app/templates/main.html
@@ -24,18 +24,7 @@
                 </div>
                 {% endif %}
             </div>
-        </div>
-
-        <div style="display:table-row">
-            <div class="text-vertical-bottom">
-                {% if user.is_authenticated %}
-                <a href="/tasks" class="btn btn-dark btn-lg">Tasks</a>
-                <a href="/containers" class="btn btn-dark btn-lg">Containers</a>
-                {% else %}
-                <a href="/login" class="btn btn-dark btn-lg">Log In</a>
-                {% endif %}
-            </div>
-        </div>
+        </div>          
     </header>
 
 {% include "footer.html" %}
diff --git a/services/webapp/code/rosetta/core_app/templates/tasks.html b/services/webapp/code/rosetta/core_app/templates/tasks.html
index 2f25d24..6217979 100644
--- a/services/webapp/code/rosetta/core_app/templates/tasks.html
+++ b/services/webapp/code/rosetta/core_app/templates/tasks.html
@@ -28,7 +28,7 @@
 
       {% if not data.task and not data.tasks %}
       <div class="row" style="padding:10px; padding-left:15px">
-      <i>Nothing here. To create a new task, choose a container from the "containers" menu entry and run it by hitting the play button.</i>
+      <a href="/create_task">New task...</a>
       </div>
       {% endif %}
 
diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py
index cedc871..067bd8e 100644
--- a/services/webapp/code/rosetta/core_app/views.py
+++ b/services/webapp/code/rosetta/core_app/views.py
@@ -402,35 +402,12 @@ def create_task(request):
     data['profile'] = Profile.objects.get(user=request.user)
     data['title']   = 'New Task'
 
-    # Step if any
-    step = request.POST.get('step', None)
-
-    # Container uuid if any
-    container_uuid = request.GET.get('task_container_uuid', None)
-    if container_uuid:
-        try:
-            data['task_container'] = Container.objects.get(uuid=container_uuid, user=request.user)
-        except Container.DoesNotExist:
-            data['task_container'] = Container.objects.get(uuid=container_uuid, user=None)
-    else:
-        # Get containers
-        data['containers'] = list(Container.objects.filter(user=None)) + list(Container.objects.filter(user=request.user))
-    
-    # Get computings 
-    data['computings'] = list(Computing.objects.filter(user=None)) + list(Computing.objects.filter(user=request.user))
-
-
-    # Handle step
-    if step:
-
-        # Task name
-        task_name = request.POST.get('task_name', None)
-        if not task_name:
-            raise ErrorMessage('Missing task name')
-        data['task_name'] = task_name
-
-        # Task container
+    # Get task container helper function
+    def get_task_container(request):
         task_container_uuid = request.POST.get('task_container_uuid', None)
+        if not task_container_uuid:
+            # At the second step the task uuid is set via a GET request 
+            task_container_uuid = request.GET.get('task_container_uuid', None)
         try:
             task_container = Container.objects.get(uuid=task_container_uuid, user=None)
         except Container.DoesNotExist:
@@ -438,9 +415,10 @@ def create_task(request):
                 task_container =  Container.objects.get(uuid=task_container_uuid, user=request.user)
             except Container.DoesNotExist:
                 raise Exception('Consistency error, container with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_container_uuid, request.user.email))
-        data['task_container'] = task_container
+        return task_container
 
-        # Task computing
+    # Get task computing helper function
+    def get_task_computing(request):
         task_computing_uuid = request.POST.get('task_computing_uuid', None)
         try:
             task_computing = Computing.objects.get(uuid=task_computing_uuid, user=None)
@@ -450,94 +428,139 @@ def create_task(request):
             except Computing.DoesNotExist:
                 raise Exception('Consistency error, computing with uuid "{}" does not exists or user "{}" does not have access rights'.format(task_computing_uuid, request.user.email))        
         task_computing.attach_user_conf(request.user)
-        data['task_computing'] = task_computing
-            
-        # Handle step one/two
-        if step == 'one':
-    
-            # Set step and task uuid
-            data['step'] = 'two'
-            
-        elif step == 'two':
-
-            # Generate the task uuid
-            task_uuid = str(uuid.uuid4())
-
-            # Create the task object
-            task = Task(uuid      = task_uuid,
-                        user      = request.user,
-                        name      = task_name,
-                        status    = TaskStatuses.created,
-                        container = task_container,
-                        computing = task_computing)
-
-            # Add auth
-            task.auth_user     = request.POST.get('auth_user', None)
-            task.auth_pass     = request.POST.get('auth_password', None)
-            task.access_method = request.POST.get('access_method', None)
-            task_base_port     = request.POST.get('task_base_port', None)
-            
-            if task_base_port:
-                task.port = task_base_port
-            
-            # Checks
-            if task.auth_pass and len(task.auth_pass) < 6:
-                raise ErrorMessage('Task password must be at least 6 chars') 
-            
-            # Computing options # TODO: This is hardcoded thinking about Slurm and Singularity
-            computing_cpus = request.POST.get('computing_cpus', None)
-            computing_memory = request.POST.get('computing_memory', None)
-            computing_partition = request.POST.get('computing_partition', None)
-            extra_binds = request.POST.get('extra_binds', None)
-            
-            computing_options = {}
-            if computing_cpus:
-                try:
-                    int(computing_cpus)
-                except:
-                    raise Exception('Cannot convert computing_cpus to int')
-                computing_options['cpus'] = int(computing_cpus)
-    
-            if computing_memory:
-                computing_options['memory'] = computing_memory
+        return task_computing
+
+    # Get task name helper function
+    def get_task_name(request):
+        task_name = request.POST.get('task_name', None)
+        if not task_name:
+            raise ErrorMessage('Missing task name')
+        return task_name
+
+    # Get step if any, check both POST and GET
+    step = request.POST.get('step', None)
+    if not step:
+        step = request.GET.get('step', None)
     
-            if computing_partition:
-                computing_options['partition'] = computing_partition        
-            
-            if computing_options:
-                task.computing_options = computing_options
-                        
-            # Attach user config to computing
-            task.computing.attach_user_conf(task.user)
 
-            # Set port if not dynamic ports
-            if not task.container.supports_dynamic_ports:
-                if task.container.ports:
-                    task.port = task.container.port
+    # Handle the various steps
+    if not step:
+        
+        # Step one is assumed: chose software container
+        return HttpResponseRedirect('/containers/?mode=new_task')
+        
+    elif step == 'two':
         
-            # Set extra binds if any:
-            task.extra_binds = extra_binds
+        # Get software container
+        data['task_container'] = get_task_container(request)
 
-            # Save the task before starting it, or the computing manager will not be able to work properly
-            task.save()
-    
-            # Start the task
+        # List all computing resources 
+        data['computings'] = list(Computing.objects.filter(user=None)) + list(Computing.objects.filter(user=request.user))
+            
+        data['step'] = 'two'
+        data['next_step'] = 'three'
+
+    elif step == 'three':
+
+        # Get software container
+        data['task_container'] = get_task_container(request)
+
+        # Get computing resource
+        data['task_computing'] = get_task_computing(request)
+
+        # Get task name
+        data['task_name'] = get_task_name(request)
+
+        # Set current and next step
+        data['step'] = 'three'
+        data['next_step'] = 'last'
+
+
+    elif step == 'last':
+
+        # Get software container
+        data['task_container'] = get_task_container(request)
+
+        # Get computing resource
+        data['task_computing'] = get_task_computing(request)
+
+        # Get task name
+        data['task_name'] = get_task_name(request)
+
+        # Generate the task uuid
+        task_uuid = str(uuid.uuid4())
+
+        # Create the task object
+        task = Task(uuid      = task_uuid,
+                    user      = request.user,
+                    name      = data['task_name'],
+                    status    = TaskStatuses.created,
+                    container = data['task_container'],
+                    computing = data['task_computing'])
+
+        # Add auth
+        task.auth_user     = request.POST.get('auth_user', None)
+        task.auth_pass     = request.POST.get('auth_password', None)
+        task.access_method = request.POST.get('access_method', None)
+        task_base_port     = request.POST.get('task_base_port', None)
+        
+        if task_base_port:
+            task.port = task_base_port
+        
+        # Checks
+        if task.auth_pass and len(task.auth_pass) < 6:
+            raise ErrorMessage('Task password must be at least 6 chars') 
+        
+        # Computing options # TODO: This is hardcoded thinking about Slurm and Singularity
+        computing_cpus = request.POST.get('computing_cpus', None)
+        computing_memory = request.POST.get('computing_memory', None)
+        computing_partition = request.POST.get('computing_partition', None)
+        extra_binds = request.POST.get('extra_binds', None)
+        
+        computing_options = {}
+        if computing_cpus:
             try:
-                task.computing.manager.start_task(task)
+                int(computing_cpus)
             except:
-                # Delete the task if could not start it
-                task.delete()
-                
-                # ..and re-raise
-                raise
+                raise Exception('Cannot convert computing_cpus to int')
+            computing_options['cpus'] = int(computing_cpus)
 
-            # Set step        
-            data['step'] = 'created'
+        if computing_memory:
+            computing_options['memory'] = computing_memory
 
-    else:
+        if computing_partition:
+            computing_options['partition'] = computing_partition        
         
-        # Set step
-        data['step'] = 'one'
+        if computing_options:
+            task.computing_options = computing_options
+                    
+        # Attach user config to computing
+        task.computing.attach_user_conf(task.user)
+
+        # Set port if not dynamic ports
+        if not task.container.supports_dynamic_ports:
+            if task.container.ports:
+                task.port = task.container.port
+    
+        # Set extra binds if any:
+        task.extra_binds = extra_binds
+
+        # Save the task before starting it, or the computing manager will not be able to work properly
+        task.save()
+
+        # Start the task
+        try:
+            task.computing.manager.start_task(task)
+        except:
+            # Delete the task if could not start it
+            task.delete()
+            
+            # ..and re-raise
+            raise
+
+        # Set step        
+        data['step'] = 'created'
+
 
     return render(request, 'create_task.html', {'data': data})
 
@@ -608,10 +631,15 @@ def containers(request):
     search_text   = request.POST.get('search_text', '')
     search_type = request.POST.get('search_type', 'All')
 
-    # Set bak to page data
+    # Set back to page data
     data['search_type'] = search_type
     data['search_text'] = search_text
 
+    # Are we using this page as first step of a new task?
+    data['mode'] = request.GET.get('mode', None)
+    if not data['mode']:
+        data['mode'] = request.POST.get('mode', None)
+
 
     # Do we have to operate on a specific container?
     if uuid:
-- 
GitLab