branch: master
commit 675cd04a8530fdc16f68758a410b91ce10d46b18
Author: TSholokhova <tanja201...@gmail.com>
Date:   Sat Jul 21 15:39:10 2018 +0200

    Add a web interface.
    
    * Makefile.am (dist_sql_DATA): Add static files.
    * src/cuirass/database.scm (assqx-ref): Export it.
    (db-get-builds): Add 'evaluation' filter and filters for pagination.
    (db-get-evaluations-build-summary, db-get-evaluations-id-min,
    db-get-evaluations-id-max, db-get-builds-min, db-get-builds-max): New 
exported
    procedures.
    * src/cuirass/http.scm (%static-directory): New parameter.
    (%page-size, %file-mime-types, %file-white-list): New variables.
    (handle-build-request, handle-builds-request): Move the 
WITH-CRITICAL-SECTION
    call out.
    (url-handler): Add RESPOND-HTML, RESPOND-STATIC-FILE and RESPOND-NOT-FOUND
    procedures.  Call WITH-CRITICAL-SECTION sooner for the '/build',
    '/api/latestbuilds' and '/api/queue' routes.  Add '/', '/jobset/<name>',
    '/eval/<id>', '/static/<path>' routes.  Use RESPOND-NOT-FOUND when the route
    isn't found.
    * src/cuirass/templates.scm: New file.
    (html-page, specifications-table, evaluation-info-table,
    build-eval-table): New exported procedures.
    (pagination): New procedure.
    
    Signed-off-by: Clément Lassieur <clem...@lassieur.org>
---
 Makefile.am               |  20 ++++-
 src/cuirass/database.scm  | 174 +++++++++++++++++++++++++++---------
 src/cuirass/http.scm      | 158 +++++++++++++++++++++++++++------
 src/cuirass/templates.scm | 222 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 505 insertions(+), 69 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index 4f6c089..ac22601 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,6 +4,7 @@
 # Copyright © 2016, 2017 Mathieu Lirzin <m...@gnu.org>
 # Copyright © 2018 Ludovic Courtès <l...@gnu.org>
 # Copyright © 2018 Clément Lassieur <clem...@lassieur.org>
+# Copyright © 2018 Tatiana Sholokhova <tanja201...@gmail.com>
 #
 # This file is part of Cuirass.
 #
@@ -34,6 +35,10 @@ pkgobjectdir = $(guileobjectdir)/$(PACKAGE)
 webmoduledir = $(guilesitedir)/web/server
 webobjectdir = $(guileobjectdir)/web/server
 sqldir = $(pkgdatadir)/sql
+staticdir = $(pkgdatadir)/static
+cssdir = $(staticdir)/css
+fontsdir = $(staticdir)/fonts
+imagesdir = $(staticdir)/images
 
 dist_pkgmodule_DATA =                          \
   src/cuirass/base.scm                         \
@@ -41,7 +46,8 @@ dist_pkgmodule_DATA =                         \
   src/cuirass/http.scm                         \
   src/cuirass/logging.scm                      \
   src/cuirass/ui.scm                           \
-  src/cuirass/utils.scm
+  src/cuirass/utils.scm             \
+  src/cuirass/templates.scm
 
 nodist_pkgmodule_DATA = \
   src/cuirass/config.scm
@@ -61,6 +67,18 @@ dist_pkgdata_DATA = src/schema.sql
 dist_sql_DATA =                                \
   src/sql/upgrade-1.sql
 
+dist_css_DATA =                                        \
+  src/static/css/bootstrap.css                 \
+  src/static/css/open-iconic-bootstrap.css
+dist_fonts_DATA =                              \
+  src/static/fonts/open-iconic.eot             \
+  src/static/fonts/open-iconic.otf             \
+  src/static/fonts/open-iconic.svg             \
+  src/static/fonts/open-iconic.ttf             \
+  src/static/fonts/open-iconic.woff
+dist_images_DATA =                             \
+  src/static/images/logo.png
+
 TEST_EXTENSIONS = .scm .sh
 AM_TESTS_ENVIRONMENT = \
   env GUILE_AUTO_COMPILE='0' \
diff --git a/src/cuirass/database.scm b/src/cuirass/database.scm
index df41d75..9b442c1 100644
--- a/src/cuirass/database.scm
+++ b/src/cuirass/database.scm
@@ -3,6 +3,7 @@
 ;;; Copyright © 2017 Mathieu Othacehe <m.othac...@gmail.com>
 ;;; Copyright © 2018 Ludovic Courtès <l...@gnu.org>
 ;;; Copyright © 2018 Clément Lassieur <clem...@lassieur.org>
+;;; Copyright © 2018 Tatiana Sholokhova <tanja201...@gmail.com>
 ;;;
 ;;; This file is part of Cuirass.
 ;;;
@@ -48,10 +49,16 @@
             db-update-build-status!
             db-get-build
             db-get-builds
+            db-get-builds-min
+            db-get-builds-max
             db-get-evaluations
+            db-get-evaluations-build-summary
+            db-get-evaluations-id-min
+            db-get-evaluations-id-max
             read-sql-file
             read-quoted-string
             sqlite-exec
+            assqx-ref
             ;; Parameters.
             %package-database
             %package-schema-file
@@ -454,20 +461,20 @@ log file for DRV."
        (#:repo-name  . ,repo-name)
        (#:outputs    . ,(db-get-outputs db id))))))
 
+;; XXX Change caller and remove
+(define (assqx-ref filters key)
+  (match filters
+    (()
+     #f)
+    (((xkey xvalue) rest ...)
+     (if (eq? key xkey)
+         xvalue
+         (assqx-ref rest key)))))
+
 (define (db-get-builds db filters)
   "Retrieve all builds in database DB which are matched by given FILTERS.
-FILTERS is an assoc list which possible keys are 'jobset | 'job | 'system |
-'nr | 'order | 'status."
-
-  ;; XXX Change caller and remove
-  (define (assqx-ref filters key)
-    (match filters
-      (()
-       #f)
-      (((xkey xvalue) rest ...)
-       (if (eq? key xkey)
-           xvalue
-           (assqx-ref rest key)))))
+FILTERS is an assoc list whose possible keys are 'id | 'jobset | 'job |
+'system | 'nr | 'order | 'status | 'evaluation."
 
   (define (format-output name path)
     `(,name . ((#:path . ,path))))
@@ -540,41 +547,57 @@ Assumes that if group id stays the same the group headers 
stay the same."
          (collect-outputs x-builds-id x-repeated-row '() rows)))))
 
   (let* ((order (match (assq 'order filters)
-                  (('order 'build-id) "Builds.id ASC")
-                  (('order 'decreasing-build-id) "Builds.id DESC")
-                  (('order 'finish-time) "Builds.stoptime DESC")
-                  (('order 'start-time) "Builds.starttime DESC")
-                  (('order 'submission-time) "Builds.timestamp DESC")
+                  (('order 'build-id) "id ASC")
+                  (('order 'decreasing-build-id) "id DESC")
+                  (('order 'finish-time) "stoptime DESC")
+                  (('order 'finish-time+build-id) "stoptime DESC, id DESC")
+                  (('order 'start-time) "starttime DESC")
+                  (('order 'submission-time) "timestamp DESC")
                   (('order 'status+submission-time)
                    ;; With this order, builds in 'running' state (-1) appear
                    ;; before those in 'scheduled' state (-2).
-                   "Builds.status DESC, Builds.timestamp DESC")
-                  (_ "Builds.id DESC")))
-         (stmt-text (format #f "\
-SELECT Builds.id, Outputs.name, Outputs.path, Builds.timestamp, 
Builds.starttime, Builds.stoptime, Builds.log, Builds.status, 
Builds.derivation,\
-Derivations.job_name, Derivations.system, Derivations.nix_name,\
-Specifications.name \
-FROM Builds \
-INNER JOIN Derivations ON Builds.derivation = Derivations.derivation AND 
Builds.evaluation = Derivations.evaluation \
-INNER JOIN Evaluations ON Derivations.evaluation = Evaluations.id \
-INNER JOIN Specifications ON Evaluations.specification = Specifications.name \
-LEFT JOIN Outputs ON Outputs.build = Builds.id \
-WHERE (:id IS NULL OR (:id = Builds.id)) \
-AND (:jobset IS NULL OR (:jobset = Specifications.name)) \
-AND (:job IS NULL OR (:job = Derivations.job_name)) \
-AND (:system IS NULL OR (:system = Derivations.system)) \
-AND (:status IS NULL OR (:status = 'done' AND Builds.status >= 0) OR (:status 
= 'pending' AND Builds.status < 0)) \
-ORDER BY ~a, Builds.id ASC LIMIT :nr;" order))
+                   "status DESC, timestamp DESC")
+                  (_ "id DESC")))
+         (stmt-text (format #f "SELECT * FROM (
+SELECT Builds.id, Outputs.name, Outputs.path, Builds.timestamp,
+Builds.starttime, Builds.stoptime, Builds.log, Builds.status,
+Builds.derivation, Derivations.job_name, Derivations.system,
+Derivations.nix_name,Specifications.name
+FROM Builds
+INNER JOIN Derivations ON Builds.derivation = Derivations.derivation
+AND Builds.evaluation = Derivations.evaluation
+INNER JOIN Evaluations ON Derivations.evaluation = Evaluations.id
+INNER JOIN Specifications ON Evaluations.specification = Specifications.name
+LEFT JOIN Outputs ON Outputs.build = Builds.id
+WHERE (:id IS NULL OR (:id = Builds.id))
+AND (:jobset IS NULL OR (:jobset = Specifications.name))
+AND (:job IS NULL OR (:job = Derivations.job_name))
+AND (:system IS NULL OR (:system = Derivations.system))
+AND (:evaluation IS NULL OR (:evaluation = Builds.evaluation))
+AND (:status IS NULL OR (:status = 'done' AND Builds.status >= 0) OR (:status 
= 'pending' AND Builds.status < 0))
+AND (:borderlowtime IS NULL OR :borderlowid IS NULL OR ((:borderlowtime, 
:borderlowid) < (Builds.stoptime, Builds.id)))
+AND (:borderhightime IS NULL OR :borderhighid IS NULL OR ((:borderhightime, 
:borderhighid) > (Builds.stoptime, Builds.id)))
+ORDER BY
+CASE WHEN :borderlowtime IS NULL OR :borderlowid IS NULL THEN Builds.stoptime 
ELSE -Builds.stoptime END DESC,
+CASE WHEN :borderlowtime IS NULL OR :borderlowid IS NULL THEN Builds.id ELSE 
-Builds.id END DESC
+LIMIT :nr)
+ORDER BY ~a, id ASC;" order))
          (stmt (sqlite-prepare db stmt-text #:cache? #t)))
-    (sqlite-bind-arguments stmt #:id (assqx-ref filters 'id)
-                           #:jobset (assqx-ref filters 'jobset)
-                           #:job (assqx-ref filters 'job)
-                           #:system (assqx-ref filters 'system)
-                           #:status (and=> (assqx-ref filters 'status)
-                                           object->string)
-                           #:nr (match (assqx-ref filters 'nr)
-                                  (#f -1)
-                                  (x x)))
+    (sqlite-bind-arguments
+     stmt
+     #:id (assqx-ref filters 'id)
+     #:jobset (assqx-ref filters 'jobset)
+     #:job (assqx-ref filters 'job)
+     #:evaluation (assqx-ref filters 'evaluation)
+     #:system (assqx-ref filters 'system)
+     #:status (and=> (assqx-ref filters 'status) object->string)
+     #:borderlowid (assqx-ref filters 'border-low-id)
+     #:borderhighid (assqx-ref filters 'border-high-id)
+     #:borderlowtime (assqx-ref filters 'border-low-time)
+     #:borderhightime (assqx-ref filters 'border-high-time)
+     #:nr (match (assqx-ref filters 'nr)
+            (#f -1)
+            (x x)))
     (sqlite-reset stmt)
     (group-outputs (sqlite-fold-right cons '() stmt))))
 
@@ -631,3 +654,68 @@ FROM Evaluations ORDER BY id DESC LIMIT " limit ";"))
                      (#:specification . ,specification)
                      (#:commits . ,(string-tokenize commits)))
                    evaluations))))))
+
+(define (db-get-evaluations-build-summary db spec limit border-low border-high)
+  (let loop ((rows (sqlite-exec db "
+SELECT E.id, E.commits, B.succeeded, B.failed, B.scheduled
+FROM (SELECT id, evaluation, SUM(status=0) as succeeded,
+SUM(status>0) as failed, SUM(status<0) as scheduled
+FROM Builds
+GROUP BY evaluation) B
+JOIN
+(SELECT id, commits
+FROM Evaluations
+WHERE (specification=" spec ")
+AND (" border-low "IS NULL OR (id >" border-low "))
+AND (" border-high "IS NULL OR (id <" border-high "))
+ORDER BY CASE WHEN " border-low "IS NULL THEN id ELSE -id END DESC
+LIMIT " limit ") E
+ON B.evaluation=E.id
+ORDER BY E.id ASC;"))
+             (evaluations '()))
+    (match rows
+      (() evaluations)
+      ((#(id commits succeeded failed scheduled) . rest)
+       (loop rest
+             (cons `((#:id . ,id)
+                     (#:commits . ,commits)
+                     (#:succeeded . ,succeeded)
+                     (#:failed . ,failed)
+                     (#:scheduled . ,scheduled))
+                   evaluations))))))
+
+(define (db-get-evaluations-id-min db spec)
+  "Return the min id of evaluations for the given specification SPEC."
+  (let ((rows (sqlite-exec db "
+SELECT MIN(id) FROM Evaluations
+WHERE specification=" spec)))
+    (vector-ref (car rows) 0)))
+
+(define (db-get-evaluations-id-max db spec)
+  "Return the max id of evaluations for the given specification SPEC."
+  (let ((rows (sqlite-exec db "
+SELECT MAX(id) FROM Evaluations
+WHERE specification=" spec)))
+    (vector-ref (car rows) 0)))
+
+(define (db-get-builds-min db eval)
+  "Return the min build (stoptime, id) pair for
+   the given evaluation EVAL."
+  (let ((rows (sqlite-exec db "
+SELECT stoptime, MIN(id) FROM
+(SELECT id, stoptime FROM Builds
+WHERE evaluation=" eval " AND
+stoptime = (SELECT MIN(stoptime)
+FROM Builds WHERE evaluation=" eval "))")))
+    (vector->list (car rows))))
+
+(define (db-get-builds-max db eval)
+  "Return the max build (stoptime, id) pair for
+   the given evaluation EVAL."
+  (let ((rows (sqlite-exec db "
+SELECT stoptime, MAX(id) FROM
+(SELECT id, stoptime FROM Builds
+WHERE evaluation=" eval " AND
+stoptime = (SELECT MAX(stoptime)
+FROM Builds WHERE evaluation=" eval "))")))
+    (vector->list (car rows))))
diff --git a/src/cuirass/http.scm b/src/cuirass/http.scm
index a45e6b1..5a5eb52 100644
--- a/src/cuirass/http.scm
+++ b/src/cuirass/http.scm
@@ -3,6 +3,7 @@
 ;;; Copyright © 2017 Mathieu Othacehe <m.othac...@gmail.com>
 ;;; Copyright © 2018 Ludovic Courtès <l...@gnu.org>
 ;;; Copyright © 2018 Clément Lassieur <clem...@lassieur.org>
+;;; Copyright © 2018 Tatiana Sholokhova <tanja201...@gmail.com>
 ;;;
 ;;; This file is part of Cuirass.
 ;;;
@@ -20,11 +21,14 @@
 ;;; along with Cuirass.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (cuirass http)
+  #:use-module (cuirass config)
   #:use-module (cuirass database)
   #:use-module (cuirass utils)
   #:use-module (cuirass logging)
+  #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-11)
   #:use-module (srfi srfi-26)
+  #:use-module (ice-9 binary-ports)
   #:use-module (ice-9 match)
   #:use-module (json)
   #:use-module (web request)
@@ -33,8 +37,37 @@
   #:use-module (web uri)
   #:use-module (fibers)
   #:use-module (fibers channels)
+  #:use-module (sxml simple)
+  #:use-module (cuirass templates)
+  #:use-module (guix utils)
+  #:use-module (guix build union)
   #:export (run-cuirass-server))
 
+(define %static-directory
+  ;; Define to the static file directory.
+  (make-parameter (string-append
+                   (or (getenv "CUIRASS_DATADIR")
+                       (string-append %datadir "/" %package))
+                   "/static")))
+
+(define %page-size 10)
+
+(define %file-mime-types
+  '(("css" . (text/css))
+    ("otf" . (font/otf))
+    ("woff" . (font/woff))
+    ("js"  . (text/javascript))
+    ("png" . (image/png))
+    ("gif" . (image/gif))
+    ("html" . (text/html))))
+
+(define %file-white-list
+  '("css/bootstrap.css"
+    "css/open-iconic-bootstrap.css"
+    "fonts/open-iconic.otf"
+    "fonts/open-iconic.woff"
+    "images/logo.png"))
+
 (define (build->hydra-build build)
   "Convert BUILD to an assoc list matching hydra API format."
   (define (bool->int bool)
@@ -70,19 +103,17 @@
     (#:releasename . #nil)
     (#:buildinputs_builds . #nil)))
 
-(define (handle-build-request db-channel build-id)
-  "Retrieve build identified by BUILD-ID over DB-CHANNEL and convert it to
-hydra format. Return #f is not build was found."
-  (let ((build (with-critical-section db-channel (db)
-                 (db-get-build db build-id))))
+(define (handle-build-request db build-id)
+  "Retrieve build identified by BUILD-ID over DB and convert it
+  to hydra format. Return #f is not build was found."
+  (let ((build (db-get-build db build-id)))
     (and=> build build->hydra-build)))
 
-(define (handle-builds-request db-channel filters)
-  "Retrieve all builds matched by FILTERS in DB-CHANNEL and convert them to
-Hydra format."
-  (let ((builds (with-critical-section db-channel (db)
-                  (with-time-logging "builds request"
-                                     (db-get-builds db filters)))))
+(define (handle-builds-request db filters)
+  "Retrieve all builds matched by FILTERS in DB and convert them
+  to Hydra format."
+  (let ((builds (with-time-logging "builds request"
+                                   (db-get-builds db filters))))
     (map build->hydra-build builds)))
 
 (define (request-parameters request)
@@ -136,6 +167,28 @@ Hydra format."
      (object->json-string
       `((error . ,message)))))
 
+  (define (respond-html body)
+    (respond '((content-type . (application/xhtml+xml)))
+             #:body
+             (lambda (port)
+               (format
+                port "<!DOCTYPE html PUBLIC ~s ~s>"
+                "-//W3C//DTD XHTML 1.0 Transitional//EN"
+                "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";)
+               (sxml->xml body port))))
+
+  (define (respond-static-file path)
+    ;; PATH is a list of path components
+    (let ((file-name (string-join path "/"))
+          (file-path (string-join (cons* (%static-directory) path) "/")))
+      (if (and (member file-name %file-white-list)
+               (file-exists? file-path)
+               (not (file-is-directory? file-path)))
+          (respond `((content-type . ,(assoc-ref %file-mime-types
+                                                 (file-extension file-path))))
+           #:body (call-with-input-file file-path get-bytevector-all))
+          (respond-not-found file-name))))
+
   (define (respond-build-not-found build-id)
     (respond-json-with-error
      404
@@ -147,6 +200,11 @@ Hydra format."
        404
        (format #f "The build log of derivation ~a is not available." drv))))
 
+  (define (respond-not-found resource_name)
+    (respond (build-response #:code 404)
+             #:body (string-append "Resource not found: "
+                                   resource_name)))
+
   (log-message "~a ~a" (request-method request)
                (uri-path (request-uri request)))
 
@@ -159,8 +217,9 @@ Hydra format."
                     (with-critical-section db-channel (db)
                       (db-get-specifications db)))))
     (("build" build-id)
-     (let ((hydra-build (handle-build-request db-channel
-                                              (string->number build-id))))
+     (let ((hydra-build
+            (with-critical-section db-channel (db)
+              (handle-build-request db (string->number build-id)))))
        (if hydra-build
            (respond-json (object->json-string hydra-build))
            (respond-build-not-found build-id))))
@@ -203,11 +262,12 @@ Hydra format."
             (valid-params? (assq-ref params 'nr)))
        (if valid-params?
            ;; Limit results to builds that are "done".
-           (respond-json (object->json-string
-                          (handle-builds-request db-channel
-                                                 `((status done)
-                                                   ,@params
-                                                   (order finish-time)))))
+           (respond-json
+            (object->json-string
+             (with-critical-section db-channel (db)
+               (handle-builds-request db `((status done)
+                                           ,@params
+                                           (order finish-time))))))
            (respond-json-with-error 500 "Parameter not defined!"))))
     (("api" "queue")
      (let* ((params (request-parameters request))
@@ -218,18 +278,66 @@ Hydra format."
             (object->json-string
              ;; Use the 'status+submission-time' order so that builds in
              ;; 'running' state appear before builds in 'scheduled' state.
-             (handle-builds-request db-channel
-                                    `((status pending)
-                                      ,@params
-                                      (order status+submission-time)))))
+             (with-critical-section db-channel (db)
+               (handle-builds-request db `((status pending)
+                                           ,@params
+                                           (order status+submission-time))))))
            (respond-json-with-error 500 "Parameter not defined!"))))
+    ('()
+     (respond-html (html-page
+                    "Cuirass"
+                    (specifications-table
+                     (with-critical-section db-channel (db)
+                       (db-get-specifications db))))))
+
+    (("jobset" name)
+     (respond-html
+      (with-critical-section db-channel (db)
+        (let* ((evaluation-id-max (db-get-evaluations-id-max db name))
+               (evaluation-id-min (db-get-evaluations-id-min db name))
+               (params (request-parameters request))
+               (border-high (assqx-ref params 'border-high))
+               (border-low (assqx-ref params 'border-low))
+               (evaluations (db-get-evaluations-build-summary db
+                                                              name
+                                                              %page-size
+                                                              border-low
+                                                              border-high)))
+          (html-page name (evaluation-info-table name
+                                                 evaluations
+                                                 evaluation-id-min
+                                                 evaluation-id-max))))))
+
+    (("eval" id)
+     (respond-html
+      (with-critical-section db-channel (db)
+        (let* ((builds-id-max (db-get-builds-max db id))
+               (builds-id-min (db-get-builds-min db id))
+               (params (request-parameters request))
+               (border-high-time (assqx-ref params 'border-high-time))
+               (border-low-time (assqx-ref params 'border-low-time))
+               (border-high-id (assqx-ref params 'border-high-id))
+               (border-low-id (assqx-ref params 'border-low-id)))
+          (html-page
+           "Evaluation"
+           (build-eval-table
+            (handle-builds-request db `((evaluation ,id)
+                                        (nr ,%page-size)
+                                        (order finish-time+build-id)
+                                        (border-high-time ,border-high-time)
+                                        (border-low-time ,border-low-time)
+                                        (border-high-id ,border-high-id)
+                                        (border-low-id ,border-low-id)))
+            builds-id-min
+            builds-id-max))))))
+
+    (("static" path ...)
+     (respond-static-file path))
     ('method-not-allowed
      ;; 405 "Method Not Allowed"
      (values (build-response #:code 405) #f db-channel))
     (_
-     (respond (build-response #:code 404)
-              #:body (string-append "Resource not found: "
-                                    (uri->string (request-uri request)))))))
+     (respond-not-found (uri->string (request-uri request))))))
 
 (define* (run-cuirass-server db #:key (host "localhost") (port 8080))
   (let* ((host-info  (gethostbyname host))
diff --git a/src/cuirass/templates.scm b/src/cuirass/templates.scm
new file mode 100644
index 0000000..6ba3a06
--- /dev/null
+++ b/src/cuirass/templates.scm
@@ -0,0 +1,222 @@
+;;; templates.scm -- HTTP API
+;;; Copyright © 2018 Tatiana Sholokhova <tanja201...@gmail.com>
+;;;
+;;; This file is part of Cuirass.
+;;;
+;;; Cuirass is free software: you can redistribute it and/or modify
+;;; it under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation, either version 3 of the License, or
+;;; (at your option) any later version.
+;;;
+;;; Cuirass is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with Cuirass.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (cuirass templates)
+  #:use-module (ice-9 format)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (html-page
+            specifications-table
+            evaluation-info-table
+            build-eval-table))
+
+(define (html-page title body)
+  "Return HTML page with given TITLE and BODY."
+  `(html (@ (xmlns "http://www.w3.org/1999/xhtml";)
+            (xml:lang "en")
+            (lang "en"))
+         (head
+          (meta (@ (charset "utf-8")))
+          (meta (@ (name "viewport")
+                   (content ,(string-join '("width=device-width"
+                                            "initial-scale=1"
+                                            "shrink-to-fit=no")
+                                          ", "))))
+          (link (@ (rel "stylesheet")
+                   (href "/static/css/bootstrap.css")))
+          (link (@ (rel "stylesheet")
+                   (href "/static/css/open-iconic-bootstrap.css")))
+          (title ,title))
+         (body
+          (nav (@ (class "navbar navbar-expand-lg navbar-light bg-light"))
+               (a (@ (class "navbar-brand") (href "/"))
+                  (img (@ (src "/static/images/logo.png")
+                          (alt "logo")
+                          (height "25")))))
+          (main (@ (role "main") (class "container pt-4 px-1"))
+                ,body
+                (hr)))))
+
+(define (specifications-table specs)
+  "Return HTML for the SPECS table."
+  `((p (@ (class "lead")) "Specifications")
+    (table
+     (@ (class "table table-sm table-hover"))
+     ,@(if (null? specs)
+           `((th (@ (scope "col")) "No elements here."))
+           `((thead (tr (th (@ (scope "col")) Name)
+                        (th (@ (scope "col")) Inputs)))
+             (tbody
+              ,@(map
+                 (lambda (spec)
+                   `(tr (td (a (@ (href "/jobset/" ,(assq-ref spec #:name)))
+                               ,(assq-ref spec #:name)))
+                        (td ,(string-join
+                              (map (lambda (input)
+                                     (format #f "~a (on ~a)"
+                                             (assq-ref input #:name)
+                                             (assq-ref input #:branch)))
+                                   (assq-ref spec #:inputs)) ", "))))
+                 specs)))))))
+
+(define (pagination first-link prev-link next-link last-link)
+  "Return html page navigation buttons with LINKS."
+  `(div (@ (class row))
+        (nav
+         (@ (class "mx-auto") (aria-label "Page navigation"))
+         (ul (@ (class "pagination"))
+             (li (@ (class "page-item"))
+                 (a (@ (class "page-link")
+                       (href ,first-link))
+                    "<< First"))
+             (li (@ (class "page-item"
+                      ,(if (string-null? prev-link) " disabled")))
+                 (a (@ (class "page-link")
+                       (href ,prev-link))
+                    "< Previous"))
+             (li (@ (class "page-item"
+                      ,(if (string-null? next-link) " disabled")))
+                 (a (@ (class "page-link")
+                       (href ,next-link))
+                    "Next >"))
+             (li (@ (class "page-item"))
+                 (a (@ (class "page-link")
+                       (href ,last-link))
+                    "Last >>"))))))
+
+(define (evaluation-info-table name evaluations id-min id-max)
+  "Return HTML for the EVALUATION table NAME. ID-MIN and ID-MAX are
+  global minimal and maximal id."
+  `((p (@ (class "lead")) "Evaluations of " ,name)
+    (table
+     (@ (class "table table-sm table-hover table-striped"))
+     ,@(if (null? evaluations)
+           `((th (@ (scope "col")) "No elements here."))
+           `((thead
+              (tr
+               (th (@ (scope "col")) "#")
+               (th (@ (scope "col")) Commits)
+               (th (@ (scope "col")) Success)))
+             (tbody
+              ,@(map
+                 (lambda (row)
+                   `(tr (th (@ (scope "row"))
+                            (a (@ (href "/eval/" ,(assq-ref row #:id)))
+                               ,(assq-ref row #:id)))
+                        (td ,(string-join
+                              (map (cut substring <> 0 7)
+                                   (string-tokenize (assq-ref row #:commits)))
+                              ", "))
+                        (td (a (@ (href "#") (class "badge badge-success"))
+                               ,(assq-ref row #:succeeded))
+                            (a (@ (href "#") (class "badge badge-danger"))
+                               ,(assq-ref row #:failed))
+                            (a (@ (href "#") (class "badge badge-secondary"))
+                               ,(assq-ref row #:scheduled)))))
+                 evaluations)))))
+    ,(if (null? evaluations)
+         (pagination "" "" "" "")
+         (let* ((eval-ids (map (cut assq-ref <> #:id) evaluations))
+                (page-id-min (last eval-ids))
+                (page-id-max (first eval-ids)))
+           (pagination
+            (format #f "?border-high=~d" (1+ id-max))
+            (if (= page-id-max id-max)
+                ""
+                (format #f "?border-low=~d" page-id-max))
+            (if (= page-id-min id-min)
+                ""
+                (format #f "?border-high=~d" page-id-min))
+            (format #f "?border-low=~d" (1- id-min)))))))
+
+(define (build-eval-table builds build-min build-max)
+  "Return HTML for the BUILDS table NAME. BUILD-MIN and BUILD-MAX are
+   global minimal and maximal (stoptime, id) pairs."
+  (define (table-header)
+    `(thead
+      (tr
+       (th (@ (scope "col")) '())
+       (th (@ (scope "col")) ID)
+       (th (@ (scope "col")) Specification)
+       (th (@ (scope "col")) "Finished at")
+       (th (@ (scope "col")) Job)
+       (th (@ (scope "col")) Nixname)
+       (th (@ (scope "col")) System))))
+
+  (define (table-row build)
+    `(tr
+      (td ,(case (assq-ref build #:buildstatus)
+             ((0) `(span (@ (class "oi oi-check text-success")
+                            (title "Succeeded")
+                            (aria-hidden "true"))
+                         ""))
+             ((1 2 3 4) `(span (@ (class "oi oi-x text-danger")
+                                  (title "Failed")
+                                  (aria-hidden "true"))
+                               ""))
+             (else `(span (@ (class "oi oi-clock text-warning")
+                             (title "Scheduled")
+                             (aria-hidden "true"))
+                          ""))))
+      (th (@ (scope "row")),(assq-ref build #:id))
+      (td ,(assq-ref build #:jobset))
+      (td ,(strftime "%c" (localtime (assq-ref build #:stoptime))))
+      (td ,(assq-ref build #:job))
+      (td ,(assq-ref build #:nixname))
+      (td ,(assq-ref build #:system))))
+
+  (define (build-id build)
+    (match build
+      ((stoptime id) id)))
+
+  (define (build-stoptime build)
+    (match build
+      ((stoptime id) stoptime)))
+
+  `((table
+     (@ (class "table table-sm table-hover table-striped"))
+     ,@(if (null? builds)
+           `((th (@ (scope "col")) "No elements here."))
+           `(,(table-header)
+             (tbody ,@(map table-row builds)))))
+    ,(if (null? builds)
+         (pagination "" "" "" "")
+         (let* ((build-time-ids (map (lambda (row)
+                                       (list (assq-ref row #:stoptime)
+                                             (assq-ref row #:id)))
+                                     builds))
+                (page-build-min (last build-time-ids))
+                (page-build-max (first build-time-ids)))
+           (pagination
+            (format #f "?border-high-time=~d&border-high-id=~d"
+                    (build-stoptime build-max)
+                    (1+ (build-id build-max)))
+            (if (equal? page-build-max build-max)
+                ""
+                (format #f "?border-low-time=~d&border-low-id=~d"
+                        (build-stoptime page-build-max)
+                        (build-id page-build-max)))
+            (if (equal? page-build-min build-min)
+                ""
+                (format #f "?border-high-time=~d&border-high-id=~d"
+                        (build-stoptime page-build-min)
+                        (build-id page-build-min)))
+            (format #f "?border-low-time=~d&border-low-id=~d"
+                    (build-stoptime build-min)
+                    (1- (build-id build-min))))))))

Reply via email to