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>