Added results overview and quick configuration links

Change-Id: If890f1a03e338f8488575f57311159bc9e4f4248
Signed-off-by: Dirk Fauth <Dirk.Fauth@de.bosch.com>
diff --git a/docker-compose.yml b/docker-compose.yml
index c214311..c153d97 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,5 +19,5 @@
       context: ./manager
     container_name: app4mc_cloud_manager
     ports:
-      - "9090:9090"
+      - "9090:8080"
       
\ No newline at end of file
diff --git a/manager/Dockerfile b/manager/Dockerfile
index c980683..cdef499 100644
--- a/manager/Dockerfile
+++ b/manager/Dockerfile
@@ -1,5 +1,6 @@
 FROM amazoncorretto:8u265
 
 COPY target/manager-0.0.1-SNAPSHOT.jar app.jar
+COPY src/main/resources/services_online.txt services.txt
 
-ENTRYPOINT ["java","-jar","/app.jar"]
\ No newline at end of file
+ENTRYPOINT ["java","-Dservice.configuration=services.txt","-jar","/app.jar"]
\ No newline at end of file
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
index e0c175c..591cbea 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/ServiceConfiguration.java
@@ -18,9 +18,13 @@
 
 public class ServiceConfiguration {
 
-	private final String serviceName;
+	private String serviceName;
 	private ArrayList<ServiceConfigurationParameter> parameter = new ArrayList<>();
 	
+	public ServiceConfiguration() {
+		// empty constructor needed for JSON serialization 
+	}
+	
 	public ServiceConfiguration(String serviceName) {
 		this.serviceName = serviceName;
 	}
@@ -29,6 +33,10 @@
 		return serviceName;
 	}
 	
+	public void setServiceName(String serviceName) {
+		this.serviceName = serviceName;
+	}
+
 	public void addParameter(ServiceConfigurationParameter param) {
 		this.parameter.add(param);
 	}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WebSecurityConfig.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WebSecurityConfig.java
index bbfced6..f40db1c 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WebSecurityConfig.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WebSecurityConfig.java
@@ -32,6 +32,7 @@
 		http
 		.authorizeRequests()
 			.antMatchers("/admin").authenticated()
+			.antMatchers("/workflowResults").authenticated()
 			.anyRequest().permitAll()
 			.and()
 		.formLogin()
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
index 0378eba..d183bcc 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowController.java
@@ -16,10 +16,13 @@
 import java.io.File;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
@@ -66,6 +69,11 @@
 
 	private HashMap<String, WorkflowStatus> workflowStatusMap = new HashMap<>();
 	
+	private DateFormat formatter = DateFormat.getDateTimeInstance(
+            DateFormat.SHORT, 
+            DateFormat.SHORT, 
+            Locale.ENGLISH);
+	
 	@javax.annotation.Resource(name = "cloudServiceDefinitions")
 	List<CloudServiceDefinition> cloudServiceDefinitions;
 
@@ -82,10 +90,22 @@
 			Model model,
 			@RequestParam(name = "uuid", required = false) String uuid) {
 		
-		if (!StringUtils.isEmpty(uuid) && this.workflowStatusMap.containsKey(uuid)) {
-			model.addAttribute("workflowStatus", this.workflowStatusMap.get(uuid));
+		if (!StringUtils.isEmpty(uuid)) {
+			
+			WorkflowStatus existing = this.workflowStatusMap.get(uuid);
+			if (existing == null) {
+				existing = WorkflowStatusHelper.loadWorkflowStatus(storageService.load(uuid, "workflowstatus.json").toFile());
+				if (existing != null) {
+					// we could load, so we store it in the local map to avoid loading it again
+					this.workflowStatusMap.put(uuid, existing);
+				} else {
+					existing = new WorkflowStatus();
+				}
+			}
+			model.addAttribute("workflowStatus", existing);
 		}
 		
+		
 		// render the form view
 		return "workflow";
 	}
@@ -111,6 +131,7 @@
 		String uuid = storageService.store(file);
 		
 		workflowStatus.setUuid(uuid);
+		workflowStatus.setName(file.getOriginalFilename() + " - " + formatter.format(new Date()));
 		workflowStatus.addMessage(file.getOriginalFilename() + " successfully uploaded!");
 
 		this.workflowStatusMap.put(uuid, workflowStatus);
@@ -137,6 +158,33 @@
 		return "workflow";
 	}
 	
+	@PostMapping("/profile/{selected}")
+	public String selectServiceProfile(
+			@PathVariable(name = "selected") String selected,
+			@ModelAttribute WorkflowStatus ws) {
+		
+		if ("systemc".equals(selected)) {
+			selectService("Migration", ws);
+			selectService("Validation", ws);
+			selectService("Amalthea 2 SystemC", ws);
+			selectService("APP4MC.Sim", ws);
+			selectService("INCHRON BTF Trace Visualization", ws);
+		} else if ("rtc".equals(selected)) {
+			selectService("Migration", ws);
+			selectService("Validation", ws);
+			selectService("RTC Analysis", ws);
+			selectService("RTC Interpreter", ws);
+			selectService("Chart Visualizer", ws);
+			
+			// add default configuration to ignore over utilization
+			ServiceConfiguration configuration = ws.getConfiguration("RTC Analysis");
+			ServiceConfigurationParameter parameter = configuration.getParameter("analysis-ignore-overutilization");
+			parameter.setValue("true");
+		}
+		
+		return "workflow";
+	}
+	
 	@PostMapping("/select/{selected}")
 	public String selectService(
 			@PathVariable(name = "selected") String selected,
@@ -379,6 +427,7 @@
 				}
 			} finally {
 				workflowStatus.done();
+				WorkflowStatusHelper.saveWorkflowStatus(workflowStatus, storageService.load(this.uuid, "workflowstatus.json").toFile());
 				messagingTemplate.convertAndSend("/topic/process-updates", new ProcessLog(Action.DONE));
 				// TODO send to session and not broadcast
 //				messagingTemplate.convertAndSendToUser(
@@ -513,8 +562,6 @@
 						boolean error = false;
 						for (String resultUrl : resultUrls) {
 							
-							// TODO check if link or download
-							
 							// download file
 							Path migrationSubDir = storageService.load(workflowStatus.getUuid(), "_" + serviceName.toLowerCase());
 							Files.createDirectories(migrationSubDir);
@@ -522,9 +569,6 @@
 							HttpResponse<File> resultResponse = Unirest.get(resultUrl)
 									.asFile(migrationSubDir.resolve("result").toString());
 							
-							// TODO check http status 302
-							// check the response result code
-								
 							Path migrationResult = resultResponse.getBody().toPath();
 							String filename = HeaderHelper.getFilenameFromHeader(resultResponse.getHeaders().getFirst("Content-Disposition"));
 							if (filename != null) {
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowResultController.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowResultController.java
new file mode 100644
index 0000000..3b0418d
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowResultController.java
@@ -0,0 +1,61 @@
+/*********************************************************************************
+ * Copyright (c) 2020 Robert Bosch GmbH and others.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Robert Bosch GmbH - initial API and implementation
+ ********************************************************************************
+ */
+package org.eclipse.app4mc.cloud.manager;
+
+import java.util.stream.Collectors;
+
+import org.eclipse.app4mc.cloud.manager.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
+
+@Controller
+public class WorkflowResultController {
+
+	@Autowired
+	private StorageService storageService;
+
+	@GetMapping("/workflowResults")
+	public String workflowResults(Model model) {
+		
+		model.addAttribute("workflows", storageService.loadAll()
+				.map(path -> path.toString().substring(path.toString().lastIndexOf('_') + 1))
+				.collect(Collectors.toMap(
+						uuid -> {
+							WorkflowStatus status = WorkflowStatusHelper.loadWorkflowStatus(storageService.load(uuid, "workflowstatus.json").toFile());
+							return (status != null && status.getName() != null) ? status.getName() : uuid;
+						}, 
+						uuid -> MvcUriComponentsBuilder.fromMethodName(WorkflowController.class,
+							"workflow",
+							model,
+							uuid).build().toUri().toString()))
+		);
+
+		// render the form view
+		return "workflowResults";
+	}
+
+	@GetMapping("/deleteAllResults")
+	public String delete(Model model) {
+		
+		storageService.deleteAll();
+		// remove possible status from model
+		model.addAttribute("workflowStatus", new WorkflowStatus());
+
+		return "redirect:/workflowResults";
+	}
+
+}
\ No newline at end of file
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
index 3c585d7..551c5a7 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatus.java
@@ -19,6 +19,7 @@
 
 public class WorkflowStatus {
 
+	private String name;
 	private String uuid;
 	private ArrayList<String> selectedServices = new ArrayList<>();
 	private HashMap<String, ServiceConfiguration> serviceConfigurations = new LinkedHashMap<>();
@@ -29,6 +30,14 @@
 	private boolean cancelled = false;
 	private boolean done = false;
 	
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
 	public String getUuid() {
 		return uuid;
 	}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java
new file mode 100644
index 0000000..3a02bc8
--- /dev/null
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/WorkflowStatusHelper.java
@@ -0,0 +1,52 @@
+/*********************************************************************************
+ * Copyright (c) 2020 Robert Bosch GmbH and others.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Robert Bosch GmbH - initial API and implementation
+ ********************************************************************************
+ */
+package org.eclipse.app4mc.cloud.manager;
+
+import java.io.File;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public final class WorkflowStatusHelper {
+
+	private static Logger LOG = LoggerFactory.getLogger(WorkflowStatusHelper.class);
+	
+	private WorkflowStatusHelper() {
+		// private constructor for helper class
+	}
+	
+	public static void saveWorkflowStatus(WorkflowStatus ws, File file) {
+		try {
+			ObjectMapper mapper = new ObjectMapper();
+			mapper.writerWithDefaultPrettyPrinter().writeValue(file, ws);
+		} catch (Exception e) {
+			LOG.error("Failed to serialize workflow status", e);
+		}
+		
+	}
+	
+	public static WorkflowStatus loadWorkflowStatus(File file) {
+		try {
+			if (file.exists()) {
+				ObjectMapper mapper = new ObjectMapper();
+				return mapper.readValue(file, WorkflowStatus.class);
+			}
+		} catch (Exception e) {
+			LOG.error("Failed to load workflow status", e);
+		}
+		return null;
+	}
+}
diff --git a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/storage/FileSystemStorageService.java b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/storage/FileSystemStorageService.java
index 110d340..397a134 100644
--- a/manager/src/main/java/org/eclipse/app4mc/cloud/manager/storage/FileSystemStorageService.java
+++ b/manager/src/main/java/org/eclipse/app4mc/cloud/manager/storage/FileSystemStorageService.java
@@ -22,14 +22,19 @@
 import java.util.Comparator;
 import java.util.stream.Stream;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.UrlResource;
 import org.springframework.stereotype.Service;
+import org.springframework.util.FileSystemUtils;
 import org.springframework.web.multipart.MultipartFile;
 
 @Service
 public class FileSystemStorageService implements StorageService {
 
+	private static final Logger LOG = LoggerFactory.getLogger(FileSystemStorageService.class);
+	
 	private static final String TEMP_DIR_PREFIX = "app4mc_mgr_";
 	
 	private final String defaultBaseDir = System.getProperty("java.io.tmpdir");
@@ -59,8 +64,7 @@
 		try {
 			return Files.walk(this.tempLocation, 1)
 					.filter(path -> !path.equals(this.tempLocation))
-					.filter(path -> path.getFileName().toString().startsWith(TEMP_DIR_PREFIX))
-					.map(path -> this.tempLocation.relativize(path));
+					.filter(path -> path.getFileName().toString().startsWith(TEMP_DIR_PREFIX));
 		} catch (IOException e) {
 			throw new StorageException("Failed to read stored files", e);
 		}
@@ -107,7 +111,12 @@
 	
 	@Override
 	public void deleteAll() {
-//		loadAll().forEach(path -> FileSystemUtils.deleteRecursively(path.toFile()));
-		loadAll().forEach(System.out::println);
+		loadAll().forEach(path -> {
+			try {
+				FileSystemUtils.deleteRecursively(path);
+			} catch (IOException e) {
+				LOG.error("Could not delete: {}", path, e);
+			}
+		});
 	}
 }
diff --git a/manager/src/main/resources/services.txt b/manager/src/main/resources/services.txt
index 33cd2a8..076f805 100644
--- a/manager/src/main/resources/services.txt
+++ b/manager/src/main/resources/services.txt
@@ -1,7 +1,7 @@
 Migration;http://localhost:8080/app4mc/converter/;Model Migration Service
 Validation;http://localhost:8181/app4mc/validation/;Model Validation Service
 RTC Analysis;http://localhost:8081/app4mc/analysis/;RTC Analysis Service
-RTC Interpreter;http://localhost:8082/app4mc/interpreter/;RTC Interpreter Service
+RTC Interpreter;http://localhost:8082/app4mc/interpreter/rtc/;RTC Interpreter Service
 Label per Core Interpreter;http://localhost:8084/app4mc/interpreter/label-per-core/;Label per Core Interpreter Service
 Label per Memory Interpreter;http://localhost:8084/app4mc/interpreter/label-per-memory/;Label per Memory Interpreter Service
 Label per Task Interpreter;http://localhost:8084/app4mc/interpreter/label-per-task/;Label per Task Interpreter Service
@@ -10,4 +10,4 @@
 Amalthea 2 INCHRON;https://am2inc.dev1.inchron.de/projects;Amalthea to INCHRON Transformation Service
 Amalthea 2 SystemC;http://localhost:8282/app4mc/amlt2systemc/;Amalthea to SystemC Transformation Service
 APP4MC.Sim;http://139.30.201.29:2323/app4mc/simulation/;APP4MC SystemC Simulation Service
-INCHRON BTF Trace Visualization;https://trace.dev1.inchron.de/traces/;INCHRON BTF Trace Visualization Service
+INCHRON BTF Trace Visualization;https://trace.dev1.inchron.de/traces/;INCHRON BTF Trace Visualization Service
\ No newline at end of file
diff --git a/manager/src/main/resources/services_online.txt b/manager/src/main/resources/services_online.txt
new file mode 100644
index 0000000..a7649d3
--- /dev/null
+++ b/manager/src/main/resources/services_online.txt
@@ -0,0 +1,13 @@
+Migration;https://app4mc.eclipseprojects.io/app4mc/converter/;Model Migration Service
+Validation;https://app4mc.eclipseprojects.io/app4mc/validation/;Model Validation Service
+RTC Analysis;https://app4mc.eclipseprojects.io/app4mc/analysis/;RTC Analysis Service
+RTC Interpreter;https://app4mc.eclipseprojects.io/app4mc/interpreter/rtc/;RTC Interpreter Service
+Label per Core Interpreter;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-per-core/;Label per Core Interpreter Service
+Label per Memory Interpreter;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-per-memory/;Label per Memory Interpreter Service
+Label per Task Interpreter;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-per-task/;Label per Task Interpreter Service
+Label Size Interpreter;https://app4mc.eclipseprojects.io/app4mc/interpreter/label-size/;Label Size Interpreter Service
+Chart Visualizer;https://app4mc.eclipseprojects.io/app4mc/visualization/barchart/;Chart Visualizer Service
+Amalthea 2 INCHRON;https://am2inc.dev1.inchron.de/projects;Amalthea to INCHRON Transformation Service
+Amalthea 2 SystemC;https://app4mc.eclipseprojects.io/app4mc/amlt2systemc/;Amalthea to SystemC Transformation Service
+APP4MC.Sim;http://139.30.201.29:2323/app4mc/simulation/;APP4MC SystemC Simulation Service
+INCHRON BTF Trace Visualization;https://trace.dev1.inchron.de/traces/;INCHRON BTF Trace Visualization Service
\ No newline at end of file
diff --git a/manager/src/main/resources/static/js/workflow.js b/manager/src/main/resources/static/js/workflow.js
index 11e100a..585fa27 100644
--- a/manager/src/main/resources/static/js/workflow.js
+++ b/manager/src/main/resources/static/js/workflow.js
@@ -28,6 +28,16 @@
 	});
 }
 
+function selectServiceProfile(profile) {
+	$.ajax({
+		type: 'POST',
+		url: '/profile/' + profile,
+		success: function(result) {
+			$('#selectedServicesBlock').load('/selectedServices');
+		}
+	});
+}
+
 function removeSelectedServices(service) {
 	$.ajax({
 		type: 'POST',
@@ -52,6 +62,7 @@
 	// initialize and bind the form for ajax submission
 	var options = {
 		beforeSubmit: function() {
+				$('#profiles').hide();
 				$('#workflowSubmit').hide();
 				$('#workflowCancel').show();
 				$('#customFile').prop('disabled', true);
@@ -80,11 +91,26 @@
 		var fileName = $(this).val().split('\\').pop();
 		//replace the "Choose a file" label
 		$(this).next('.custom-file-label').html(fileName);
+        $('#profiles').show();
+        $('#buttons').show();
         $('#workflowSubmit').show();
 		clearWorkflow();
 	});
+	
 });
 
+function copy(link) {
+	var copyLink = link.href;
+	var handler = function(event) {
+		event.clipboardData.setData('text/plain', copyLink);
+		event.preventDefault();
+		document.removeEventListener('copy', handler, true);
+	}
+	document.addEventListener('copy', handler, true);
+	document.execCommand('copy');
+	$('.toast').toast('show');
+}
+
 //websocket
 
 var stompClient = null;
@@ -104,7 +130,7 @@
 					$('#errorsBlock').load('/errors');
 		     	} else if (action == 'DONE') {
 		     		disconnect();
-		     		location.reload(true);
+		     		window.location.href = window.location.href.split("?")[0];
 		 	    }
 			}
 		);
diff --git a/manager/src/main/resources/templates/fragment.html b/manager/src/main/resources/templates/fragment.html
index 061370b..d47b425 100644
--- a/manager/src/main/resources/templates/fragment.html
+++ b/manager/src/main/resources/templates/fragment.html
@@ -21,6 +21,7 @@
 				src="/images/Logo_Panorama_small.png" /></a>
 			<ul class="navbar-nav col-md-10 justify-content-end">
 				<li class="nav-item"><a class="nav-link" th:href="@{/}">Home</a></li>
+				<li class="nav-item"><a class="nav-link" th:href="@{/workflowResults}">Results</a></li>
 				<li class="nav-item"><a class="nav-link" th:href="@{/admin}">Admin</a></li>
 				<li class="nav-item" sec:authorize="isAuthenticated()"><a class="nav-link" th:href="@{/logout}">Logout</a></li>
 				<li class="nav-item" sec:authorize="isAnonymous()"><a class="nav-link" th:href="@{/login}">Login</a></li>
diff --git a/manager/src/main/resources/templates/status.html b/manager/src/main/resources/templates/status.html
index 87e4794..07ac487 100644
--- a/manager/src/main/resources/templates/status.html
+++ b/manager/src/main/resources/templates/status.html
@@ -33,8 +33,21 @@
 						th:href="@{/{uuid}/files/{resultValue}?download=true(uuid=${workflowStatus.uuid},resultValue=${result.value})}"><i class="fas fa-download"></i></a>
 				</li>
 			</ul>
-			<a th:href="@{/{uuid}/delete(uuid=${workflowStatus.uuid})}" class="btn btn-danger mt-4 mb-4">Delete</a>
+			<div class="row mt-4 mb-4">
+				<div class="col">
+					<a th:href="@{/{uuid}/delete(uuid=${workflowStatus.uuid})}" class="btn btn-danger">Delete</a>
+					<a th:href="@{workflow?uuid={uuid}(uuid=${workflowStatus.uuid})}" class="btn btn-info" onclick="copy(this);return false;">Permanent Link</a>
+					<div>
+					    <div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position:relative;bottom:60px;left:200px;">
+							<div class="toast-body">
+								Permanent link copied to clipboard
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
 		</div>
+	    
     </div>
 </body>
 </html>
diff --git a/manager/src/main/resources/templates/workflow.html b/manager/src/main/resources/templates/workflow.html
index da9ab5f..a972246 100644
--- a/manager/src/main/resources/templates/workflow.html
+++ b/manager/src/main/resources/templates/workflow.html
@@ -26,13 +26,17 @@
 					<label class="custom-file-label" for="customFile">Select input file to process</label>
 				</div>
 			</div>
+			<div id="profiles" class="form-row mb-3" th:style="*{!done ? 'display:block' : 'display:none'}">
+				<a class="mr-3" href="" onclick="selectServiceProfile('rtc');return false;">RTC Analysis</a>
+				<a class="mr-3" href="" onclick="selectServiceProfile('systemc');return false;">SystemC Simulation</a>
+			</div>
 			<div class="form-row">
 				<div class="form-group col" id="wf-fm-group">
 					<label>Select service(s) to process</label>
 					<div id="selectedServicesBlock"></div>
 				</div>
 			</div>
-			<div class="form-row" style="margin-top: 20px; margin-bottom: 100px">
+			<div id="buttons" class="form-row" style="margin-top: 20px; margin-bottom: 100px" th:style="*{!done ? 'display:block' : 'display:none'}">
 				<div class="col text-center">
 					<input 
 						id="workflowSubmit" 
diff --git a/manager/src/main/resources/templates/workflowResults.html b/manager/src/main/resources/templates/workflowResults.html
new file mode 100644
index 0000000..46cce37
--- /dev/null
+++ b/manager/src/main/resources/templates/workflowResults.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html xmlns:th="https://www.thymeleaf.org"
+	  xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
+<head th:replace="fragment :: fragment_head">
+</head>
+<body>
+	<div th:replace="fragment :: fragment_nav"></div>
+	<div class="banner"></div>
+	<div class="container col-md-7">
+		<div class="row mb-5" style="margin-top: 100px">
+			<div class="col text-center">
+				<div class="content-heading">
+					<h1>Cloud Service - Workflow Results</h1>
+				</div>
+			</div>
+		</div>
+
+		<div class="row mt-5 mb-5">
+		</div>
+		<div class="row" th:each="workflow : ${workflows}">
+			<div class="col text-center">
+				<a th:href="${workflow.value}" th:text="${workflow.key}"></a>
+			</div>
+		</div>
+		<div sec:authorize="isAuthenticated()" class="row mt-5">
+			<div class="col text-center">
+				<a th:href="@{/deleteAllResults}" class="btn btn-danger mt-4 mb-4">Delete all results</a>
+			</div>
+		</div>
+		
+	</div>
+</body>
+</html>