open Tyxml.Html
let pp_ptime = Ptime.pp_human ()
let txtf fmt = Fmt.kstrf txt fmt
let a_titlef fmt = Fmt.kstrf a_title fmt
let form ~csrf_token ?a children =
form ?a
(input ~a:[a_name "dream.csrf"; a_input_type `Hidden; a_value csrf_token] () ::
children)
let check_icon result =
match result with
| Builder.Exited 0 ->
span ~a:[
a_style "color: green; cursor: pointer;";
a_titlef "%a" Builder.pp_execution_result result;
]
[txt "☑"]
| _ ->
span ~a:[
a_style "color: red; cursor: pointer;";
a_titlef "%a" Builder.pp_execution_result result;
]
[txt "☒"]
let layout ~title:title_ body_ =
html
(head (title (txt title_))
[style ~a:[a_mime_type "text/css"]
[
txt "body {\
margin: 40px auto;\
line-height: 1.6;\
color: #444;\
padding: 0 10px;\
}";
txt "h1,h2,h3{line-height:1.2}";
txt ".output-ts {\
white-space: nowrap;\
cursor: pointer;\
user-select: none;\
}";
txt ".output-ts a {text-decoration: none;}";
txt ".output-ts a:hover {text-decoration: underline;}";
txt ".output-code {\
overflow: visible;\
white-space: pre;\
}";
txt ".toggleable {\
display: none;\
}";
txt ".toggleable-descr {\
cursor: pointer;\
text-decoration: underline;\
user-select: none;\
}";
txt ":checked + .toggleable {\
display: block;\
}";
]])
(body body_)
let toggleable ?(hidden=true) id description content =
let checked = if hidden then [] else [a_checked ()] in
div [
label
~a:[
a_label_for id;
a_class ["toggleable-descr"];
]
[txt description];
input
~a:(checked @ [
a_input_type `Checkbox;
a_id id;
a_style "display: none;";
]) ();
div
~a:[
a_class ["toggleable"]
]
content;
]
let artifact ?(basename=false) job_name build { Builder_db.filepath; localpath = _; sha256; size } =
[
a ~a:[a_href (Fmt.strf "/job/%s/build/%a/f/%a"
job_name
Uuidm.pp build.Builder_db.Build.Meta.uuid
Fpath.pp filepath)]
[if basename
then txt (Fpath.basename filepath)
else txtf "%a" Fpath.pp filepath];
txt " ";
code [txtf "SHA256:%a" Hex.pp (Hex.of_cstruct sha256)];
txtf " (%a)" Fmt.byte_size size;
]
let builder username csrf_token jobs =
layout ~title:"Builder Web"
[ h1 [txt "Builder web"];
begin match username with
| Some username ->
form ~csrf_token ~a:[a_method `Post; a_action "/logout"]
[
p [
txtf "Logged in as %s." username;
];
input ~a:[a_input_type `Submit; a_value "Log out!"] ();
];
| None -> txt ""
end;
form ~csrf_token ~a:[a_action "/hash"; a_method `Get]
[
label [
txt "Search artifact by SHA256";
br ();
input ~a:[
a_input_type `Search;
a_id "sha256";
a_name "sha256";
] ();
];
input ~a:[
a_input_type `Submit;
a_value "Search";
] ();
];
p [
txtf "We have currently %d jobs."
(List.length jobs);
];
ul (List.map (fun (job_name, latest_build, latest_artifact) ->
li ([
a ~a:[a_href ("job/" ^ job_name ^ "/")]
[txt job_name];
txt " ";
check_icon latest_build.Builder_db.Build.Meta.result;
br ();
a ~a:[a_href (Fmt.strf "job/%s/build/%a/" job_name Uuidm.pp
latest_build.Builder_db.Build.Meta.uuid)]
[txtf "%a" (Ptime.pp_human ()) latest_build.Builder_db.Build.Meta.start];
txt " ";
] @ match latest_artifact with
| Some main_binary ->
artifact ~basename:true job_name latest_build main_binary
| None ->
[
txtf "Build failed";
]))
jobs);
]
let job name builds =
layout ~title:(Printf.sprintf "Job %s" name)
[ h1 [txtf "Job %s" name];
p [
txtf "Currently %d builds."
(List.length builds)
];
ul (List.map (fun (build, main_binary) ->
li ([
a ~a:[a_href Fpath.(to_string (v "build" / Uuidm.to_string build.Builder_db.Build.Meta.uuid / ""))]
[
txtf "%a" (Ptime.pp_human ()) build.Builder_db.Build.Meta.start;
];
txt " ";
check_icon build.result;
br ();
] @ match main_binary with
| Some main_binary ->
artifact ~basename:true name build main_binary
| None ->
[
txtf "Build failed";
]))
builds);
]
let job_build
name
{ Builder_db.Build.uuid; start; finish; result; console; script; _ }
artifacts
latest_uuid
previous_build
=
let delta = Ptime.diff finish start in
let successful_build = match result with Builder.Exited 0 -> true | _ -> false in
layout ~title:(Fmt.strf "Job build %s %a" name pp_ptime start)
[ h1 [txtf "Job build %s %a" name pp_ptime start];
p [txtf "Build took %a." Ptime.Span.pp delta ];
p [txtf "Execution result: %a." Builder.pp_execution_result result];
(match latest_uuid with
| Some latest_uuid when successful_build && not (Uuidm.equal latest_uuid uuid) ->
p [
a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch"
Uuidm.pp uuid Uuidm.pp latest_uuid]
[txt "Compare opam-switch with latest build"];
]
| _ -> txt "");
(match previous_build with
| Some previous_build when successful_build ->
p [
a ~a:[Fmt.kstr a_href "/compare/%a/%a/opam-switch"
Uuidm.pp uuid Uuidm.pp previous_build.Builder_db.Build.Meta.uuid]
[txt "Compare opam-switch with previous build"];
]
| _ -> txt "");
h3 [txt "Digests of build artifacts"];
dl (List.concat_map
(fun { Builder_db.filepath; localpath=_; sha256; size } ->
let (`Hex sha256_hex) = Hex.of_cstruct sha256 in
[
dt [a
~a:[Fmt.kstr a_href "f/%a" Fpath.pp filepath]
[code [txtf "%a" Fpath.pp filepath]]];
dd [
code [txt "SHA256:"; txt sha256_hex];
txtf " (%a)" Fmt.byte_size size;
];
])
artifacts);
h3 [txt "Job script"];
toggleable "job-script" "Show/hide"
[ pre [txt script] ];
h3 [txt "Build log"];
toggleable ~hidden:false "build-log" "Show/hide build log"
[
table
(List.mapi (fun idx (ts, line) ->
let ts_id = "L" ^ string_of_int idx in
tr [
td ~a:[
a_class ["output-ts"];
a_id ts_id;
]
[a ~a:[a_href ("#"^ts_id); a_class ["output-ts-anchor"]]
[code [txtf "%#d ms" (Duration.to_ms (Int64.of_int ts))]]];
td ~a:[a_class ["output-code"]]
[code [txt line]];
])
(List.rev console));
];
]
let packages packages =
OpamPackage.Set.elements packages
|> List.concat_map (fun p -> [
txtf "%a" Opamdiff.pp_opampackage p;
br ();
])
let package_diffs diffs =
List.concat_map (fun pd -> [
txtf "%a" Opamdiff.pp_version_diff pd;
br ();
])
diffs
let compare_opam job_left job_right
(build_left : Builder_db.Build.t) (build_right : Builder_db.Build.t)
(same, opam_diff, version_diff, left, right) =
layout ~title:(Fmt.strf "Comparing opam switches between builds %a and %a"
Uuidm.pp build_left.uuid Uuidm.pp build_right.uuid)
[
h1 [txt "Comparing opam switches"];
h2 [
txt "Builds ";
a ~a:[a_href
(Fmt.strf "/job/%s/build/%a/"
job_left
Uuidm.pp build_left.uuid)]
[txtf "%a" pp_ptime build_left.start];
txt " and ";
a ~a:[a_href
(Fmt.strf "/job/%s/build/%a/"
job_right
Uuidm.pp build_right.uuid)]
[txtf "%a" pp_ptime build_right.start];
];
ul [
li [
a ~a:[a_href "#packages-removed"]
[txtf "%d packages removed" (OpamPackage.Set.cardinal left)]
];
li [
a ~a:[a_href "#packages-installed"]
[txtf "%d new packages installed" (OpamPackage.Set.cardinal right)]
];
li [
a ~a:[a_href "#packages-version-diff"]
[txtf "%d packages with version changes" (List.length version_diff)]
];
li [
a ~a:[a_href "#packages-opam-diff"]
[txtf "%d packages with changes in their opam file" (OpamPackage.Set.cardinal opam_diff)]
];
li [
a ~a:[a_href "#packages-unchanged"]
[txtf "%d packages unchanged" (OpamPackage.Set.cardinal same)]
];
];
h3 ~a:[a_id "packages-removed"]
[txt "Packages removed"];
code (packages left);
h3 ~a:[a_id "packages-installed"]
[txt "New packages installed"];
code (packages right);
h3 ~a:[a_id "packages-version-diff"]
[txt "Packages with version changes"];
code (package_diffs version_diff);
h3 ~a:[a_id "packages-opam-diff"]
[txt "Packages with changes in their opam file"];
code (packages opam_diff);
h3 ~a:[a_id "packages-unchanged"]
[txt "Unchanged packages"];
code (packages same);
]
let login _username csrf_token =
layout ~title:"Login" [
h1 [txt "Please login"];
form ~csrf_token ~a:[a_method `Post; a_action "/login"]
[
label ~a:[a_label_for "user"] [txt "User name"];
input ~a:[a_name "user"] ();
label ~a:[a_label_for "password"] [txt "Password"];
input ~a:[a_input_type `Password; a_name "password"] ();
input ~a:[a_input_type `Submit; a_value "Log in!"] ();
]
]