First version

This commit is contained in:
Andreas Svanberg 2017-04-27 12:42:40 +02:00
commit c9d885d94e
5 changed files with 348 additions and 0 deletions

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
.idea/
elm-stuff/
target/

16
elm-package.json Normal file

@ -0,0 +1,16 @@
{
"version": "1.0.0",
"summary": "Lightweight Jenkins walldisplay",
"repository": "https://github.com/asvanberg/better-walldisplay.git",
"license": "BSD3",
"source-directories": [
"src/main/elm"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "5.1.1 <= v < 6.0.0",
"elm-lang/html": "2.0.0 <= v < 3.0.0",
"elm-lang/http": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.18.0 <= v < 0.19.0"
}

64
pom.xml Normal file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.26</version>
</parent>
<groupId>io.github.asvanberg</groupId>
<artifactId>better-walldisplay</artifactId>
<version>1.0.0</version>
<packaging>hpi</packaging>
<properties>
<jenkins.version>2.56</jenkins.version>
</properties>
<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>http://repo.jenkins-ci.org/public/</url>
</pluginRepository>
<pluginRepository>
<id>stil4m-releases</id>
<name>stil4m-releases</name>
<url>https://github.com/stil4m/maven-repository/raw/master/releases/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>nl.stil4m</groupId>
<artifactId>elm-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>Make Elm Source</id>
<phase>generate-sources</phase>
<goals>
<goal>make</goal>
</goals>
<configuration>
<executablePath>elm-make</executablePath>
<inputFiles>src/main/elm/Main.elm</inputFiles>
<outputFile>${project.build.directory}/${project.build.finalName}/app.js</outputFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

194
src/main/elm/Main.elm Normal file

@ -0,0 +1,194 @@
module Main exposing (main)
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (..)
import Http
import Json.Decode as Decode exposing (Decoder, field, int, list, string)
import Task
import Time exposing (Time, second)
type Message
= Update Time
| JobListRetrieved (Result Http.Error (List Job))
| JobDetailsRetrieved String (Result Http.Error JobDetails)
type alias Model =
{ viewName : String
, state : State
, now : Time
, jobs : Dict String (Result () JobDetails)
}
type State
= Working
| Error
type alias Job =
{ url : String
, name : String
, color : String
}
type alias JobDetails =
{ color : String
, name : String
, lastBuild : Maybe Build
, lastSuccessfulBuild : Maybe Build
}
type alias Build =
{ duration : Int
, timestamp : Int
}
main : Program String Model Message
main =
programWithFlags
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
init : String -> ( Model, Cmd Message )
init jenkinsViewName =
({ viewName = jenkinsViewName, state = Working, jobs = Dict.empty, now = 0 }, Task.perform Update Time.now)
update : Message -> Model -> ( Model, Cmd Message )
update msg model =
case msg of
Update now ->
let
request =
Http.get ("/view/" ++ model.viewName ++ "/api/json") (field "jobs" <| Decode.list decodeJob)
command =
Http.send JobListRetrieved request
in
({ model | now = now }, command)
JobListRetrieved (Err e) ->
({ model | state = Error}, Cmd.none)
JobListRetrieved (Ok jobList) ->
let
toDetails job =
{ name = job.name
, color = job.color
, lastBuild = Nothing
, lastSuccessfulBuild = Nothing
}
details =
List.map toDetails jobList
|> List.map (\details -> (details.name, details))
|> Dict.fromList
addJob name jobDetail =
Dict.insert name (Ok jobDetail)
updateColor name jobDetail oldJobDetail =
case oldJobDetail of
Ok f ->
Dict.insert name (Ok { f | color = jobDetail.color })
Err () ->
Dict.insert name (Ok jobDetail)
removeJob name oldJobDetail =
identity
newModel =
{ model
| jobs = Dict.merge addJob updateColor removeJob details model.jobs Dict.empty
}
requestJobDetails job =
Http.send (JobDetailsRetrieved job.name)
<| Http.get (job.url ++ "api/json?tree=" ++ treeParameter) decodeJobDetails
in
(newModel, Cmd.batch <| List.map requestJobDetails <| List.filter isBuilding jobList)
JobDetailsRetrieved jobName (Err _) ->
let
newJobs =
Dict.insert jobName (Err ()) model.jobs
in
({ model | jobs = newJobs }, Cmd.none)
JobDetailsRetrieved jobName (Ok details) ->
let
newJobs =
Dict.insert jobName (Ok details) model.jobs
in
({ model | jobs = newJobs }, Cmd.none)
isBuilding : { c | color : String } -> Bool
isBuilding =
.color >> String.endsWith "_anime"
treeParameter : String
treeParameter = "color,displayName,lastBuild[timestamp,duration],lastSuccessfulBuild[duration,timestamp]"
decodeJob : Decoder Job
decodeJob =
Decode.map3 Job
(field "url" string)
(field "name" string)
(field "color" string)
decodeJobDetails : Decoder JobDetails
decodeJobDetails =
Decode.map4 JobDetails
(field "color" string)
(field "displayName" string)
(field "lastBuild" (Decode.maybe decodeBuild))
(field "lastSuccessfulBuild" (Decode.maybe decodeBuild))
decodeBuild : Decoder Build
decodeBuild =
Decode.map2 Build
(field "duration" int)
(field "timestamp" int)
subscriptions : Model -> Sub Message
subscriptions {state} =
let
updateDelay =
case state of
Working ->
5 * second
Error ->
60 * second
in
Time.every updateDelay Update
view : Model -> Html msg
view model =
let
fontSize =
clamp 0 30 <| round <| 100 / (toFloat <| Dict.size model.jobs) * 0.70
viewJob (jobName, jobDetails) =
case jobDetails of
Ok {name, color, lastBuild, lastSuccessfulBuild} ->
div [ class color, style [ ("font-size", (toString fontSize) ++ "vmin") ] ]
[ if isBuilding { color = color } then
progressBar lastBuild lastSuccessfulBuild
else
div [ class "progress", style [ ("width", "0") ] ] []
, div [ style [ ("z-index", "1000") ] ] [ text name ]
]
Err () ->
div [ class "red" ] [ text jobName ]
progressBar lastBuild lastSuccessfulBuild =
case (lastBuild, lastSuccessfulBuild) of
(Just lb, Just lbs) ->
let
elapsed = (round model.now) - lb.timestamp
progress = clamp 0 100 <| elapsed * 100 // lbs.duration
in
div [ class "progress", style [ ("width", (toString progress) ++ "%") ] ] []
_ ->
div [ class "progress", style [ ("width", "0") ] ] []
in
div [ class "job-container" ] (List.map viewJob <| Dict.toList model.jobs)

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html>
<head>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: #000;
color: #fff;
font-weight: bold;
font-family: sans-serif;
text-shadow: 1px 1px 2px black;
}
div.job-container {
height: 100%;
display: flex;
flex-direction: column;
}
div.job-container > div {
flex: 1;
margin: 8px;
display: flex;
flex-direction: column;
justify-content: center;
border-radius: 10px;
text-align: center;
position: relative;
}
div.job-container > div + div {
margin-top: 0;
}
div.job-container > div.red, div.job-container > div.red_anime {
background-color: #800;
}
div.job-container > div.blue, div.job-container > div.blue_anime {
background-color: #080;
}
.blue .progress, .red .progress, .yellow .progress, .aborted .progress {
display: none;
}
div.job-container div.progress {
background-image: linear-gradient(to left, rgba(255, 255, 255, .5) 50%, transparent);
}
div.job-container > div.yellow, div.job-container > div.yellow_anime {
background-color: #880;
}
div.job-container > div.aborted, div.job-container > div.aborted_anime {
background-color: #a8a6a6;
}
div.progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
transition: width 5s linear;
box-sizing: border-box;
border-radius: 10px;
border-right: 10px solid yellow;
}
</style>
</head>
<body>
<script src="app.js"></script>
<script>
Elm.Main.fullscreen(window.location.search.substring(1))
</script>
</body>
</html>