aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2013-01-19 10:20:43 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2013-01-19 10:20:43 +0100
commitfd953370ba0daea03c5de58aac30e097f86826c6 (patch)
treeb41143ecd8e65bfeb1ceceb2500c23146ca7780e
parenteb6cbd28992cec9025a8e95f8f03ae3839699e0b (diff)
downloadesper-testing-fd953370ba0daea03c5de58aac30e097f86826c6.tar.gz
esper-testing-fd953370ba0daea03c5de58aac30e097f86826c6.tar.bz2
esper-testing-fd953370ba0daea03c5de58aac30e097f86826c6.tar.xz
esper-testing-fd953370ba0daea03c5de58aac30e097f86826c6.zip
o Making sure every resource that supports paging has a default ordering.
o Fixing mis-mapped JenkinsJob. o Better navigation in the Jenkins app, <a> links are now proper links.
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/Util.java10
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/core/db/BuildDao.java4
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/core/db/PersonDao.java10
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/jenkins/JenkinsDao.java11
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/jenkins/JenkinsJobDto.java4
-rwxr-xr-x[-rw-r--r--]src/main/java/io/trygvis/esper/testing/util/sql/PageRequest.java6
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/web/JerseyApplication.java4
-rwxr-xr-xsrc/main/java/io/trygvis/esper/testing/web/resource/JenkinsResource.java22
-rwxr-xr-xsrc/main/resources/webapp/apps/jenkinsApp/build.html6
-rwxr-xr-x[-rw-r--r--]src/main/resources/webapp/apps/jenkinsApp/jenkinsApp.js39
-rwxr-xr-xsrc/main/resources/webapp/apps/jenkinsApp/job.html8
-rwxr-xr-x[-rw-r--r--]src/main/resources/webapp/apps/jenkinsApp/server-list.html2
-rwxr-xr-x[-rw-r--r--]src/main/resources/webapp/apps/jenkinsApp/server.html14
13 files changed, 71 insertions, 69 deletions
diff --git a/src/main/java/io/trygvis/esper/testing/Util.java b/src/main/java/io/trygvis/esper/testing/Util.java
index 849a6f9..1bba36c 100755
--- a/src/main/java/io/trygvis/esper/testing/Util.java
+++ b/src/main/java/io/trygvis/esper/testing/Util.java
@@ -77,11 +77,19 @@ public class Util {
// SQL
// -----------------------------------------------------------------------
+ public static List<String> ifEmpty(List<String> inputs, String defaultValue) {
+ if (inputs.isEmpty()) {
+ return inputs;
+ }
+
+ return Collections.singletonList(defaultValue);
+ }
+
public static String orderBy(String[] inputs, String... allowed) {
return orderBy(Arrays.asList(inputs), allowed);
}
- public static String orderBy(Iterable<String> inputs, String... allowed) {
+ public static String orderBy(List<String> inputs, String... allowed) {
StringBuilder buffer = new StringBuilder();
for (String input : inputs) {
diff --git a/src/main/java/io/trygvis/esper/testing/core/db/BuildDao.java b/src/main/java/io/trygvis/esper/testing/core/db/BuildDao.java
index c6a46c6..7c9ccb7 100755
--- a/src/main/java/io/trygvis/esper/testing/core/db/BuildDao.java
+++ b/src/main/java/io/trygvis/esper/testing/core/db/BuildDao.java
@@ -92,7 +92,7 @@ public class BuildDao {
public List<BuildDto> selectBuildsByPerson(Uuid person, PageRequest page) throws SQLException {
String sql = "SELECT " + BUILD + " FROM build b, build_participant bp WHERE bp.person=? AND b.uuid = bp.build";
- sql += orderBy(page.orderBy, "created_date", "timestamp");
+ sql += orderBy(ifEmpty(page.orderBy, "created_date-"), "created_date", "timestamp");
sql += " LIMIT ? OFFSET ?";
try (PreparedStatement s = c.prepareStatement(sql)) {
@@ -106,7 +106,7 @@ public class BuildDao {
public List<BuildDto> selectBuilds(PageRequest page) throws SQLException {
String sql = "SELECT " + BUILD + " FROM build";
- sql += orderBy(page.orderBy, "created_date", "timestamp");
+ sql += orderBy(ifEmpty(page.orderBy, "created_date-"), "created_date", "timestamp");
sql += " LIMIT ? OFFSET ?";
try (PreparedStatement s = c.prepareStatement(sql)) {
diff --git a/src/main/java/io/trygvis/esper/testing/core/db/PersonDao.java b/src/main/java/io/trygvis/esper/testing/core/db/PersonDao.java
index 290d6d5..2cfb16d 100755
--- a/src/main/java/io/trygvis/esper/testing/core/db/PersonDao.java
+++ b/src/main/java/io/trygvis/esper/testing/core/db/PersonDao.java
@@ -97,14 +97,14 @@ public class PersonDao {
}
}
- public List<PersonDto> selectPersons(PageRequest pageRequest, Option<String> query) throws SQLException {
+ public List<PersonDto> selectPersons(PageRequest page, Option<String> query) throws SQLException {
String sql = "SELECT " + PERSON + " FROM person";
if (query.isSome()) {
sql += " WHERE lower(name) LIKE '%' || ? || '%'";
}
- sql += orderBy(pageRequest.orderBy, "name", "created_date");
+ sql += orderBy(ifEmpty(page.orderBy, "created_date-"), "name", "created_date");
sql += " LIMIT ? OFFSET ?";
@@ -115,8 +115,8 @@ public class PersonDao {
s.setString(i++, query.some());
}
- s.setInt(i++, pageRequest.count.orSome(10));
- s.setInt(i, pageRequest.startIndex.orSome(0));
+ s.setInt(i++, page.count.orSome(10));
+ s.setInt(i, page.startIndex.orSome(0));
return toList(s, person);
}
}
@@ -216,7 +216,7 @@ public class PersonDao {
sql += " AND level=?";
}
- sql += orderBy(page.orderBy, "name", "created_date");
+ sql += orderBy(ifEmpty(page.orderBy, "created_date-"), "name", "created_date");
sql += " LIMIT ? OFFSET ?";
diff --git a/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsDao.java b/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsDao.java
index 781df13..d21118d 100755
--- a/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsDao.java
+++ b/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsDao.java
@@ -22,8 +22,6 @@ public class JenkinsDao {
public static final String JENKINS_SERVER = "uuid, created_date, name, url, enabled";
- public static final String JENKINS_JOB = "uuid, created_date, server, file, url, job_type, display_name";
-
public static final String JENKINS_USER = "uuid, created_date, server, absolute_url";
public JenkinsDao(Connection c) {
@@ -42,6 +40,8 @@ public class JenkinsDao {
}
};
+ public static final String JENKINS_JOB = "uuid, created_date, server, file, url, job_type, display_name";
+
public static final SqlF<ResultSet, JenkinsJobDto> jenkinsJob = new SqlF<ResultSet, JenkinsJobDto>() {
public JenkinsJobDto apply(ResultSet rs) throws SQLException {
int i = 1;
@@ -51,6 +51,7 @@ public class JenkinsDao {
UUID.fromString(rs.getString(i++)),
UUID.fromString(rs.getString(i++)),
URI.create(rs.getString(i++)),
+ rs.getString(i++),
fromNull(rs.getString(i)));
}
};
@@ -172,7 +173,11 @@ public class JenkinsDao {
}
public List<JenkinsBuildDto> selectBuildByJob(UUID job, PageRequest page) throws SQLException {
- try (PreparedStatement s = c.prepareStatement("SELECT " + JENKINS_BUILD + " FROM jenkins_build WHERE job=? ORDER BY created_date DESC LIMIT ? OFFSET ?")) {
+ String sql = "SELECT " + JENKINS_BUILD + " FROM jenkins_build WHERE job=?";
+ sql += orderBy(ifEmpty(page.orderBy, "created_date-"), "created_date", "timestamp");
+ sql += " LIMIT ? OFFSET ?";
+
+ try (PreparedStatement s = c.prepareStatement(sql)) {
int i = 1;
s.setString(i++, job.toString());
s.setInt(i++, page.count.orSome(10));
diff --git a/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsJobDto.java b/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsJobDto.java
index e540a2a..2092d89 100755
--- a/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsJobDto.java
+++ b/src/main/java/io/trygvis/esper/testing/jenkins/JenkinsJobDto.java
@@ -11,13 +11,15 @@ public class JenkinsJobDto extends AbstractEntity {
public final UUID server;
public final UUID file;
public final URI url;
+ public final String jobType;
public final Option<String> displayName;
- public JenkinsJobDto(UUID uuid, DateTime createdDate, UUID server, UUID file, URI url, Option<String> displayName) {
+ public JenkinsJobDto(UUID uuid, DateTime createdDate, UUID server, UUID file, URI url, String jobType, Option<String> displayName) {
super(uuid, createdDate);
this.server = server;
this.file = file;
this.url = url;
+ this.jobType = jobType;
this.displayName = displayName;
}
}
diff --git a/src/main/java/io/trygvis/esper/testing/util/sql/PageRequest.java b/src/main/java/io/trygvis/esper/testing/util/sql/PageRequest.java
index b7c6750..7773e50 100644..100755
--- a/src/main/java/io/trygvis/esper/testing/util/sql/PageRequest.java
+++ b/src/main/java/io/trygvis/esper/testing/util/sql/PageRequest.java
@@ -1,12 +1,14 @@
package io.trygvis.esper.testing.util.sql;
-import fj.data.*;
+import fj.data.Option;
+
+import java.util.*;
public class PageRequest {
public final Option<Integer> startIndex;
public final Option<Integer> count;
public final List<String> orderBy;
- public static final PageRequest FIRST_PAGE = new PageRequest(Option.<Integer>none(), Option.<Integer>none(), List.<String>nil());
+ public static final PageRequest FIRST_PAGE = new PageRequest(Option.<Integer>none(), Option.<Integer>none(), Collections.<String>emptyList());
public PageRequest(Option<Integer> startIndex, Option<Integer> count, List<String> orderBy) {
this.startIndex = startIndex;
diff --git a/src/main/java/io/trygvis/esper/testing/web/JerseyApplication.java b/src/main/java/io/trygvis/esper/testing/web/JerseyApplication.java
index 38cfd9d..b85dd7a 100755
--- a/src/main/java/io/trygvis/esper/testing/web/JerseyApplication.java
+++ b/src/main/java/io/trygvis/esper/testing/web/JerseyApplication.java
@@ -66,10 +66,10 @@ public class JerseyApplication extends Application {
List<String> list = queryParameters.get("orderBy");
- fj.data.List<String> orderBy = nil();
+ List<String> orderBy = Collections.emptyList();
if (list != null) {
- orderBy = iterableList(list);
+ orderBy = list;
}
return new PageRequest(
diff --git a/src/main/java/io/trygvis/esper/testing/web/resource/JenkinsResource.java b/src/main/java/io/trygvis/esper/testing/web/resource/JenkinsResource.java
index 0c0c6a9..a6d8e8e 100755
--- a/src/main/java/io/trygvis/esper/testing/web/resource/JenkinsResource.java
+++ b/src/main/java/io/trygvis/esper/testing/web/resource/JenkinsResource.java
@@ -69,9 +69,9 @@ public class JenkinsResource extends AbstractResource {
@GET
@Path("/job/{uuid}")
@Produces(MediaType.APPLICATION_JSON)
- public JenkinsJobJsonDetail getJob(@MagicParam final UUID uuid) throws Exception {
- return sql(new JenkinsDaosCallback<SqlOption<JenkinsJobJsonDetail>>() {
- protected SqlOption<JenkinsJobJsonDetail> run() throws SQLException {
+ public JenkinsJobDetailJson getJob(@MagicParam final UUID uuid) throws Exception {
+ return sql(new JenkinsDaosCallback<SqlOption<JenkinsJobDetailJson>>() {
+ protected SqlOption<JenkinsJobDetailJson> run() throws SQLException {
return daos.jenkinsDao.selectJob(uuid).map(getJenkinsJobJsonDetail);
}
});
@@ -149,13 +149,13 @@ public class JenkinsResource extends AbstractResource {
protected SqlF<JenkinsJobDto, JenkinsJobJson> getJenkinsJobJson = new SqlF<JenkinsJobDto, JenkinsJobJson>() {
public JenkinsJobJson apply(JenkinsJobDto job) throws SQLException {
- return new JenkinsJobJson(job.uuid, job.createdDate, job.server, job.displayName.toNull());
+ return new JenkinsJobJson(job.uuid, job.createdDate, job.server, job.url, job.displayName.toNull());
}
};
- protected SqlF<JenkinsJobDto,JenkinsJobJsonDetail> getJenkinsJobJsonDetail = new SqlF<JenkinsJobDto, JenkinsJobJsonDetail>() {
- public JenkinsJobJsonDetail apply(JenkinsJobDto dto) throws SQLException {
- return new JenkinsJobJsonDetail(
+ protected SqlF<JenkinsJobDto,JenkinsJobDetailJson> getJenkinsJobJsonDetail = new SqlF<JenkinsJobDto, JenkinsJobDetailJson>() {
+ public JenkinsJobDetailJson apply(JenkinsJobDto dto) throws SQLException {
+ return new JenkinsJobDetailJson(
getJenkinsJobJson.apply(dto),
daos.jenkinsDao.selectBuildCountByJob(dto.uuid));
}
@@ -223,21 +223,23 @@ class JenkinsJobJson {
public final UUID uuid;
public final DateTime createdDate;
public final UUID server;
+ public final URI url;
public final String displayName;
- JenkinsJobJson(UUID uuid, DateTime createdDate, UUID server, String displayName) {
+ JenkinsJobJson(UUID uuid, DateTime createdDate, UUID server, URI url, String displayName) {
this.uuid = uuid;
this.createdDate = createdDate;
this.server = server;
+ this.url = url;
this.displayName = displayName;
}
}
-class JenkinsJobJsonDetail {
+class JenkinsJobDetailJson {
public final JenkinsJobJson job;
public final Integer buildCount;
- JenkinsJobJsonDetail(JenkinsJobJson job, Integer buildCount) {
+ JenkinsJobDetailJson(JenkinsJobJson job, Integer buildCount) {
this.job = job;
this.buildCount = buildCount;
}
diff --git a/src/main/resources/webapp/apps/jenkinsApp/build.html b/src/main/resources/webapp/apps/jenkinsApp/build.html
index 7239c90..13e3b8b 100755
--- a/src/main/resources/webapp/apps/jenkinsApp/build.html
+++ b/src/main/resources/webapp/apps/jenkinsApp/build.html
@@ -7,9 +7,9 @@
</div>
<ul class="breadcrumb">
- <li><a ng-click="showServers()">All Servers</a> <span class="divider">/</span></li>
- <li><a ng-click="showServer()">Servers</a> <span class="divider">/</span></li>
- <li><a ng-click="showJob()">Job</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/">All Servers</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/server/{{serverUuid}}">Server</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/server/{{serverUuid}}/job/{{jobUuid}}">Job</a> <span class="divider">/</span></li>
<li class="active">Build</li>
</ul>
diff --git a/src/main/resources/webapp/apps/jenkinsApp/jenkinsApp.js b/src/main/resources/webapp/apps/jenkinsApp/jenkinsApp.js
index e42c67b..68344cd 100644..100755
--- a/src/main/resources/webapp/apps/jenkinsApp/jenkinsApp.js
+++ b/src/main/resources/webapp/apps/jenkinsApp/jenkinsApp.js
@@ -3,55 +3,44 @@
var jenkinsApp = angular.module('jenkinsApp', ['jenkinsServer', 'jenkinsJob', 'jenkinsBuild', 'core.directives', 'pagingTableService']).config(function ($routeProvider) {
$routeProvider.
when('/', {controller: ServerListCtrl, templateUrl: '/apps/jenkinsApp/server-list.html?noCache=' + noCache}).
- when('/server/:uuid', {controller: ServerCtrl, templateUrl: '/apps/jenkinsApp/server.html?noCache=' + noCache}).
- when('/job/:uuid', {controller: JobCtrl, templateUrl: '/apps/jenkinsApp/job.html?noCache=' + noCache}).
- when('/build/:uuid', {controller: BuildCtrl, templateUrl: '/apps/jenkinsApp/build.html?noCache=' + noCache});
+ when('/server/:serverUuid', {controller: ServerCtrl, templateUrl: '/apps/jenkinsApp/server.html?noCache=' + noCache}).
+ when('/server/:serverUuid/job/:jobUuid', {controller: JobCtrl, templateUrl: '/apps/jenkinsApp/job.html?noCache=' + noCache}).
+ when('/server/:serverUuid/job/:jobUuid/build/:buildUuid', {controller: BuildCtrl, templateUrl: '/apps/jenkinsApp/build.html?noCache=' + noCache});
});
function ServerListCtrl($scope, $location, JenkinsServer) {
JenkinsServer.query(function (servers) {
$scope.servers = servers;
});
-
- $scope.showServers = function () { $location.path('/'); };
- $scope.showServer = function (uuid) { $location.path('/server/' + uuid); };
}
function ServerCtrl($scope, $location, $routeParams, JenkinsServer, JenkinsJob, PagingTableService) {
- var serverUuid = $routeParams.uuid;
+ $scope.serverUuid = $routeParams.serverUuid;
- JenkinsServer.get({uuid: serverUuid}, function (server) {
+ JenkinsServer.get({uuid: $scope.serverUuid}, function (server) {
$scope.server = server;
});
- $scope.jobs = PagingTableService.create($scope, PagingTableService.defaultCallback(JenkinsJob, {server: serverUuid}));
-
- $scope.showServers = function () { $location.path('/'); };
- $scope.showJob = function (uuid) { $location.path('/job/' + uuid); };
+ $scope.jobs = PagingTableService.create($scope, PagingTableService.defaultCallback(JenkinsJob, {server: $scope.serverUuid}));
}
function JobCtrl($scope, $location, $routeParams, JenkinsJob, JenkinsBuild, PagingTableService) {
- var jobUuid = $routeParams.uuid;
+ $scope.serverUuid = $routeParams.serverUuid;
+ $scope.jobUuid = $routeParams.jobUuid;
- JenkinsJob.get({uuid: jobUuid}, function (details) {
+ JenkinsJob.get({uuid: $scope.jobUuid}, function (details) {
$scope.details = details;
});
- $scope.builds = PagingTableService.create($scope, PagingTableService.defaultCallback(JenkinsBuild, {job: jobUuid}));
-
- $scope.showServers = function () { $location.path('/'); };
- $scope.showServer = function () { $location.path('/server/' + $scope.job.server); };
- $scope.showBuild = function (uuid) { $location.path('/build/' + uuid); };
+ $scope.builds = PagingTableService.create($scope, PagingTableService.defaultCallback(JenkinsBuild, {job: $scope.jobUuid, orderBy: "timestamp-"}));
}
function BuildCtrl($scope, $location, $routeParams, JenkinsBuild) {
- var buildUuid = $routeParams.uuid;
+ $scope.serverUuid = $routeParams.serverUuid;
+ $scope.jobUuid = $routeParams.jobUuid;
+ $scope.buildUuid = $routeParams.buildUuid;
- JenkinsBuild.get({uuid: buildUuid}, function (details) {
+ JenkinsBuild.get({uuid: $scope.buildUuid}, function (details) {
$scope.details = details;
});
-
- $scope.showServers = function () { $location.path('/'); };
- $scope.showServer = function (uuid) { $location.path('/server/' + $scope.server.uuid); };
- $scope.showJob = function (uuid) { $location.path('/job/' + $scope.build.job); };
}
diff --git a/src/main/resources/webapp/apps/jenkinsApp/job.html b/src/main/resources/webapp/apps/jenkinsApp/job.html
index 8942ab7..4f21fad 100755
--- a/src/main/resources/webapp/apps/jenkinsApp/job.html
+++ b/src/main/resources/webapp/apps/jenkinsApp/job.html
@@ -7,8 +7,8 @@
</div>
<ul class="breadcrumb">
- <li><a ng-click="showServers()">All Servers</a> <span class="divider">/</span></li>
- <li><a ng-click="showServer()">Servers</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/">All Servers</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/server/{{serverUuid}}">Server</a> <span class="divider">/</span></li>
<li class="active">Job</li>
</ul>
@@ -18,7 +18,7 @@
<tbody>
<tr>
<th>URL</th>
- <td><a href="{{details.job.displayName}}">{{details.job.displayName}}</a></td>
+ <td><a href="{{details.job.url}}">{{details.job.displayName}}</a></td>
</tr>
<tr>
<th>Build count</th>
@@ -40,7 +40,7 @@
<tr ng-repeat="build in builds.rows" class="{{{true: 'success', false: 'error'}[build.success]}}">
<td>{{build.timestamp | date:'medium'}}</td>
<td>{{build.result}}</td>
- <td><a class="btn" ng-click="showBuild(build.uuid)"><i class="icon-chevron-right"></i></a></td>
+ <td><a class="btn" href="/jenkins/#/server/{{serverUuid}}/job/{{jobUuid}}/build/{{build.uuid}}"><i class="icon-chevron-right"></i></a></td>
</tr>
</tbody>
<tfoot>
diff --git a/src/main/resources/webapp/apps/jenkinsApp/server-list.html b/src/main/resources/webapp/apps/jenkinsApp/server-list.html
index 9e297e6..93e5e9e 100644..100755
--- a/src/main/resources/webapp/apps/jenkinsApp/server-list.html
+++ b/src/main/resources/webapp/apps/jenkinsApp/server-list.html
@@ -24,7 +24,7 @@
<td>{{server.url}}</td>
<td>{{server.enabled}}</td>
<td><a href="{{server.url}}">Visit</a></td>
- <td><a class="btn" ng-click="showServer(server.uuid)"><i class="icon-chevron-right"></i></a>
+ <td><a class="btn" href="/jenkins/#/server/{{server.uuid}}"><i class="icon-chevron-right"></i></a>
</td>
</tr>
</tbody>
diff --git a/src/main/resources/webapp/apps/jenkinsApp/server.html b/src/main/resources/webapp/apps/jenkinsApp/server.html
index 7fbd9f5..fb821d3 100644..100755
--- a/src/main/resources/webapp/apps/jenkinsApp/server.html
+++ b/src/main/resources/webapp/apps/jenkinsApp/server.html
@@ -8,7 +8,7 @@
</div>
<ul class="breadcrumb">
- <li><a ng-click="showServers()">All Servers</a> <span class="divider">/</span></li>
+ <li><a href="/jenkins/#/">All Servers</a> <span class="divider">/</span></li>
<li class="active">Server</li>
</ul>
@@ -25,24 +25,18 @@
</tr>
<tr>
<th>Stats</th>
- <td>{{server.jobCount}} jobs, {{server.buildCount}} builds</td>
+ <td>{{server.jobCount}} jobs</td>
</tr>
</tbody>
</table>
<h3>Recent Jobs</h3>
<table class="table">
- <thead>
- <tr>
- <th>Job</th>
- </tr>
- </thead>
<tbody>
<tr ng-repeat="job in jobs.rows">
- <td>{{job.createdDate | date:'medium'}}</td>
<td>{{job.displayName}}</td>
- <td>{{job.uuid}}</td>
- <td><a class="btn" ng-click="showJob(job.uuid)"><i class="icon-chevron-right"></i></a></td>
+ <td>{{job.createdDate | date:'medium'}}</td>
+ <td><a class="btn" href="/jenkins/#/server/{{server.uuid}}/job/{{job.uuid}}"><i class="icon-chevron-right"></i></a></td>
</tr>
</tbody>
<tfoot>