├── .gitignore
├── README.md
├── src
├── index.js
├── index.html
├── Msg.elm
├── Main.elm
├── Components
│ ├── Help.elm
│ ├── Breadcrumbs.elm
│ ├── Navbar.elm
│ └── LiveSearch.elm
├── Urls.elm
├── Update.elm
├── View.elm
├── Pages
│ ├── Jobset.elm
│ └── Project.elm
├── Utils.elm
└── Models.elm
├── default.nix
├── package.json
├── elm-package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | elm-stuff/
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Superseeded by https://github.com/hercules-ci/hercules
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Require index.html so it gets copied to dist
4 | require('./index.html');
5 |
6 | var app = require('./Main.elm').Main.fullscreen();
7 |
8 | app.ports.title.subscribe(function(title) {
9 | document.title = "Hydra - " + title;
10 | });
11 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Msg.elm:
--------------------------------------------------------------------------------
1 | module Msg exposing (..)
2 |
3 | import Material
4 | import Http
5 |
6 | import Components.LiveSearch as LiveSearch
7 | import Urls exposing (Page)
8 |
9 |
10 | type LoginType
11 | = Hydra
12 | | Google
13 |
14 |
15 | type Msg
16 | = Mdl (Material.Msg Msg)
17 | | FetchSucceed String
18 | | FetchFail Http.Error
19 | | LoginUserClick LoginType
20 | | LogoutUserClick
21 | | PreferencesClick
22 | | LiveSearchMsg LiveSearch.Msg
23 | | NewPage Page
24 | | ClickCreateProject
25 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | { backend ? (import ./../backend {})}:
2 |
3 | with import (fetchTarball https://github.com/NixOS/nixpkgs/archive/e725c927d4a09ee116fe18f2f0718364678a321f.tar.gz) {};
4 |
5 | stdenv.mkDerivation {
6 | name = "hydra-frontend";
7 |
8 | src = ./.;
9 |
10 | buildInputs = [ elmPackages.elm nodejs ];
11 |
12 | patchPhase = ''
13 | patchShebangs node_modules/webpack
14 | '';
15 |
16 | buildPhase = ''
17 | ${backend}/bin/gen-elm src
18 |
19 | # https://github.com/NixHercules/hercules/issues/3
20 | sed -i "s@'@@g" src/Hercules.elm
21 |
22 | npm run build
23 | '';
24 |
25 | installPhase = ''
26 | mkdir $out
27 | cp dist/* $out/
28 | '';
29 | }
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "1.0.0",
4 | "description": "Hydra frontend",
5 | "main": "index.js",
6 | "dependencies": {
7 | "elm-hot-loader": "^0.3.2",
8 | "elm-webpack-loader": "3.0.0",
9 | "file-loader": "^0.8.5",
10 | "webpack": "^1.13.0",
11 | "webpack-dev-middleware": "^1.6.1",
12 | "webpack-dev-server": "^1.14.1"
13 | },
14 | "devDependencies": {},
15 | "scripts": {
16 | "test": "echo \"Error: no test specified\" && exit 1",
17 | "build": "webpack --optimize-minimize --bail",
18 | "watch": "webpack --watch",
19 | "dev": "webpack-dev-server --watch --hot --port 3000"
20 | },
21 | "author": "Domen Kozar",
22 | "license": "ISC"
23 | }
24 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "src"
8 | ],
9 | "exposed-modules": [],
10 | "dependencies": {
11 | "NoRedInk/elm-decode-pipeline": "2.0.0 <= v < 3.0.0",
12 | "debois/elm-mdl": "7.4.0 <= v < 8.0.0",
13 | "elm-lang/core": "4.0.0 <= v < 5.0.0",
14 | "elm-lang/html": "1.0.0 <= v < 2.0.0",
15 | "elm-lang/navigation": "1.0.0 <= v < 2.0.0",
16 | "evancz/elm-http": "3.0.1 <= v < 4.0.0",
17 | "evancz/url-parser": "1.0.0 <= v < 2.0.0"
18 | },
19 | "elm-version": "0.17.0 <= v < 0.18.0"
20 | }
21 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path"),
2 | webpack = require('webpack');
3 |
4 | module.exports = {
5 | entry: {
6 | app: [
7 | './src/index.js'
8 | ]
9 | },
10 |
11 | output: {
12 | path: path.resolve(__dirname + '/dist'),
13 | filename: 'app.js',
14 | },
15 |
16 | module: {
17 | loaders: [
18 | {
19 | test: /\.html$/,
20 | include: /src/,
21 | loader: 'file?name=[name].[ext]',
22 | },
23 | {
24 | test: /\.elm$/,
25 | include: /src/,
26 | loader: 'elm-hot!elm-webpack?warn=true',
27 | }
28 | ],
29 |
30 | noParse: /\.elm$/,
31 | },
32 |
33 | devServer: {
34 | inline: true,
35 | stats: 'errors-only',
36 | historyApiFallback: true
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/src/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (..)
2 |
3 | import Http
4 | import Task
5 | import Json.Decode as Json
6 | import Json.Decode exposing ((:=))
7 | import Material
8 | import Navigation
9 |
10 | import Msg exposing (..)
11 | import Models exposing (..)
12 | import Update exposing (..)
13 | import View exposing (..)
14 | import Urls exposing (..)
15 |
16 |
17 | init : Result String Page -> ( AppModel, Cmd Msg )
18 | init result =
19 | let
20 | ( model, cmds ) =
21 | urlUpdate result initialModel
22 | in
23 | model ! [ doInit, Material.init Mdl, cmds ]
24 |
25 |
26 | doInit : Cmd Msg
27 | doInit =
28 | Task.perform FetchFail FetchSucceed (Http.get decodeInit "/api/init")
29 |
30 |
31 | decodeInit : Json.Decoder (String)
32 | decodeInit =
33 | Json.succeed "a"
34 |
35 |
36 |
37 | -- Json.list (Json.succeed Event
38 | -- |: ("id" := Json.int)
39 | -- |: ("title" := Json.string)
40 | -- )
41 |
42 |
43 | subscriptions : AppModel -> Sub Msg
44 | subscriptions model =
45 | Sub.none
46 |
47 |
48 | main : Program Never
49 | main =
50 | Navigation.program (Navigation.makeParser urlParser)
51 | { init = init
52 | , update = update
53 | , view = view
54 | , urlUpdate = urlUpdate
55 | , subscriptions = Material.subscriptions Mdl
56 | }
57 |
--------------------------------------------------------------------------------
/src/Components/Help.elm:
--------------------------------------------------------------------------------
1 | module Components.Help exposing (..)
2 |
3 | import Html exposing (..)
4 | import Material.Icon as Icon
5 | import Material.Options as Options
6 | import Material.Tooltip as Tooltip
7 |
8 | import Msg exposing (..)
9 | import Models exposing (..)
10 |
11 |
12 | {-| Uses ports to communicate with jQuery and initialize twitter
13 | bootstrap popover plugin. Each help element is a questionmark
14 | icon which on popover shows some help text.
15 | -}
16 | popoverHelp : AppModel -> List (Html Msg) -> Html Msg
17 | popoverHelp model html =
18 | span
19 | []
20 | [ Icon.view "help"
21 | [ Options.css "margin" "0 6px"
22 | , Options.css "color" "#0088CC"
23 | , Options.css "cursor" "help"
24 | , Tooltip.attach Mdl [3]
25 | ]
26 | , Tooltip.render Mdl [3] model.mdl
27 | [ Tooltip.large ]
28 | html
29 | ]
30 |
31 | projectHelp : AppModel -> Html Msg
32 | projectHelp model =
33 | popoverHelp model [ text "TODO" ]
34 |
35 | jobsetHelp : AppModel -> Html Msg
36 | jobsetHelp model =
37 | popoverHelp
38 | model
39 | [ text "Jobsets evaluate a Nix expression and provide an overview of successful/failed builds." ]
40 |
41 |
42 | evaluationHelp : AppModel -> Html Msg
43 | evaluationHelp model =
44 | popoverHelp model [ text "" ]
45 |
46 |
47 | buildHelp : AppModel -> Html Msg
48 | buildHelp model =
49 | popoverHelp model [ text "" ]
50 |
51 |
52 | buildStepHelp : AppModel -> Html Msg
53 | buildStepHelp model =
54 | popoverHelp model [ text ""]
55 |
--------------------------------------------------------------------------------
/src/Urls.elm:
--------------------------------------------------------------------------------
1 | module Urls exposing (..)
2 |
3 | import Debug
4 | import Navigation
5 | import String
6 | import UrlParser exposing (Parser, (>), format, int, oneOf, s, string)
7 |
8 |
9 | {-| Main type representing current url/page
10 | -}
11 | type Page
12 | = Home
13 | | Project String
14 | | NewProject
15 | | Jobset String String
16 |
17 |
18 | urlParser : Navigation.Location -> Result String Page
19 | urlParser location =
20 | String.dropLeft 1 location.pathname
21 | |> Debug.log "pathname"
22 | |> UrlParser.parse identity pageParser
23 |
24 |
25 | pageParser : Parser (Page -> a) a
26 | pageParser =
27 | oneOf
28 | [ format Home (s "")
29 | , format Project (s "project" > string)
30 | , format NewProject (s "create-project")
31 | , format Jobset (s "jobset" > string > string)
32 | ]
33 |
34 |
35 | pageToURL : Page -> String
36 | pageToURL page =
37 | case page of
38 | Home ->
39 | "/"
40 |
41 | Project name ->
42 | "/project/" ++ name
43 |
44 | NewProject ->
45 | "/create-project"
46 |
47 | Jobset project jobset ->
48 | "/jobset/" ++ project ++ "/" ++ jobset
49 |
50 |
51 | pageToTitle : Page -> String
52 | pageToTitle page =
53 | case page of
54 | Home ->
55 | "Projects"
56 |
57 | Project name ->
58 | "Project " ++ name
59 |
60 | NewProject ->
61 | "New Project"
62 |
63 | Jobset project jobset ->
64 | "Jobset " ++ jobset ++ " of project " ++ project
65 |
--------------------------------------------------------------------------------
/src/Components/Breadcrumbs.elm:
--------------------------------------------------------------------------------
1 | module Components.Breadcrumbs exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import List
6 | import Material.Icon as Icon
7 | import Material.Options as Options
8 |
9 | import Msg exposing (..)
10 | import Models exposing (..)
11 | import Urls as Urls exposing (..)
12 | import Utils exposing (onClickPage)
13 |
14 |
15 | type alias Breadcrumb =
16 | { name : String
17 | , page : Maybe Page
18 | }
19 |
20 |
21 | renderBreadcrumbs : List Breadcrumb -> List (Html Msg)
22 | renderBreadcrumbs breadcrumbs =
23 | let
24 | home = a (onClickPage Home) [ text "Hydra"]
25 | render breadcrumb =
26 | case breadcrumb.page of
27 | Just page ->
28 | a (onClickPage page)
29 | [ text breadcrumb.name ]
30 |
31 | Nothing ->
32 | span [ class "active" ]
33 | [ text breadcrumb.name ]
34 | in List.intersperse
35 | (Icon.view "keyboard_arrow_right"
36 | [ Icon.size36
37 | , Options.css "top" "10px"
38 | , Options.css "position" "relative"
39 | ])
40 | (home :: List.map render breadcrumbs)
41 |
42 |
43 | breadCrumbs : AppModel -> List (Html Msg)
44 | breadCrumbs model =
45 | let
46 | breadcrumbs =
47 | case model.currentPage of
48 | Home ->
49 | []
50 |
51 | NewProject ->
52 | [ Breadcrumb "New Project" Nothing ]
53 |
54 | Project project ->
55 | [ Breadcrumb project Nothing ]
56 |
57 | Jobset project jobset ->
58 | [ Breadcrumb project (Just (Urls.Project project))
59 | , Breadcrumb jobset Nothing
60 | ]
61 | in renderBreadcrumbs breadcrumbs
62 |
--------------------------------------------------------------------------------
/src/Update.elm:
--------------------------------------------------------------------------------
1 | port module Update exposing (..)
2 |
3 | import Material
4 | import Navigation
5 | import Models exposing (..)
6 | import Msg exposing (..)
7 | import Components.LiveSearch as LiveSearch
8 | import Urls exposing (..)
9 |
10 |
11 | update : Msg -> AppModel -> ( AppModel, Cmd Msg )
12 | update msg model =
13 | case msg of
14 | Mdl msg' ->
15 | Material.update msg' model
16 |
17 | FetchSucceed init ->
18 | ( model, Cmd.none )
19 |
20 | FetchFail msg ->
21 | ( model, Cmd.none )
22 |
23 | LoginUserClick loginType ->
24 | let
25 | -- TODO: well, actually do the login proceedure
26 | user =
27 | { id = "domenkozar"
28 | , name = "Domen Kožar"
29 | , email = "domen@dev.si"
30 | , roles = []
31 | , recieveEvaluationErrors = False
32 | }
33 | in
34 | case loginType of
35 | Hydra ->
36 | ( { model | user = Just user }, Cmd.none )
37 |
38 | Google ->
39 | ( { model | user = Just user }, Cmd.none )
40 |
41 | LogoutUserClick ->
42 | -- TODO: well, should we cleanup something?
43 | ( { model | user = Nothing }, Cmd.none )
44 |
45 | PreferencesClick ->
46 | ( model, Cmd.none )
47 |
48 | LiveSearchMsg searchmsg ->
49 | let
50 | ( newmodel, cmds ) =
51 | LiveSearch.update searchmsg model
52 | in
53 | ( newmodel, Cmd.map LiveSearchMsg cmds )
54 |
55 | NewPage page ->
56 | ( model, Navigation.newUrl (pageToURL page) )
57 |
58 | ClickCreateProject ->
59 | -- TODO: http
60 | ( model, Cmd.none )
61 |
62 |
63 | urlUpdate : Result String Page -> AppModel -> ( AppModel, Cmd b )
64 | urlUpdate result model =
65 | case result of
66 | Err msg ->
67 | let
68 | msg =
69 | (Debug.log "urlUpdate:" msg)
70 |
71 | alert =
72 | { kind = Danger
73 | , msg = "Given URL returned 404."
74 | }
75 | in
76 | { model | alert = Just alert } ! []
77 |
78 | Ok page ->
79 | ( { model
80 | | currentPage = page
81 | , alert = Nothing
82 | }
83 | , title (pageToTitle page)
84 | )
85 |
86 |
87 |
88 | -- Ports
89 |
90 |
91 | port title : String -> Cmd msg
92 |
--------------------------------------------------------------------------------
/src/View.elm:
--------------------------------------------------------------------------------
1 | module View exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Maybe
6 | import List
7 | import Material.Scheme
8 | import Material.Color as Color
9 | import Material.Layout as Layout
10 | import Material.Options as Options
11 | import Material.Footer as Footer
12 |
13 | import Components.Navbar as Navbar
14 | import Pages.Project exposing (..)
15 | import Pages.Jobset as Jobset exposing (..)
16 | import Msg exposing (..)
17 | import Models exposing (..)
18 | import Utils exposing (..)
19 | import Urls exposing (..)
20 |
21 |
22 | view : AppModel -> Html Msg
23 | view model =
24 | Options.div
25 | [ ]
26 | [ Material.Scheme.topWithScheme Color.BlueGrey Color.LightBlue <|
27 | Layout.render Mdl model.mdl
28 | [ Layout.fixedHeader ]
29 | { header = Navbar.view model
30 | , drawer = []
31 | , tabs = ( Navbar.tabs model, [ Color.background (Color.color Color.LightBlue Color.A700) ] )
32 | , main = viewBody model
33 | }
34 | ]
35 |
36 |
37 | viewBody : AppModel -> List (Html Msg)
38 | viewBody model =
39 | let
40 | softwareLink name url version =
41 | p []
42 | [ a [ href url ]
43 | [ text name ]
44 | , span []
45 | [ text " @ "
46 | , text version ]
47 | ]
48 | in Options.div
49 | [ Options.css "margin" "30px"
50 | , Options.css "min-height" "100%"
51 | ]
52 | (pageToView model)
53 | :: [ Footer.mini [ Options.css "position" "absolute"
54 | , Options.css "bottom" "-70px"
55 | , Options.css "width" "100%"]
56 | { left = Footer.left []
57 | [ Footer.logo
58 | []
59 | []
60 | ]
61 | , right = Footer.right []
62 | [ Footer.logo
63 | []
64 | [ Footer.html <| softwareLink "Nix" "http://nixos.org/nix/" model.hydraConfig.nixVersion
65 | , Footer.html <| softwareLink "Hydra" "http://nixos.org/hydra/" model.hydraConfig.hydraVersion
66 | ]
67 | ]
68 | }
69 | ]
70 |
71 |
72 | pageToView : AppModel -> List (Html Msg)
73 | pageToView model =
74 | case model.currentPage of
75 | Home ->
76 | Pages.Project.view model model.currentPage
77 |
78 | Project name ->
79 | Pages.Project.view model model.currentPage
80 |
81 | NewProject ->
82 | Pages.Project.view model model.currentPage
83 |
84 | Jobset projectName jobsetName ->
85 | case List.head (List.filter (\p -> p.name == projectName) model.projects) of
86 | Just project ->
87 | case List.head (List.filter (\j -> j.name == jobsetName) project.jobsets) of
88 | Just jobset ->
89 | Jobset.view model
90 |
91 | Nothing ->
92 | render404 ("Jobset " ++ jobsetName ++ " does not exist.")
93 |
94 | Nothing ->
95 | render404 ("Project " ++ projectName ++ " does not exist.")
96 |
--------------------------------------------------------------------------------
/src/Components/Navbar.elm:
--------------------------------------------------------------------------------
1 | module Components.Navbar exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.App as App
5 | import Html.Attributes exposing (..)
6 | import Maybe
7 | import Material.Layout as Layout
8 | import Material.Icon as Icon
9 | import Material.Menu as Menu
10 | import Material.Options as Options
11 |
12 | import Msg exposing (..)
13 | import Models exposing (AppModel)
14 | import Components.Breadcrumbs exposing (breadCrumbs)
15 | import Components.LiveSearch as LiveSearch
16 | import Urls exposing (..)
17 | import Utils exposing (..)
18 |
19 |
20 | tabs : AppModel -> List (Html Msg)
21 | tabs model =
22 | [ span [] [ whiteBadge [] [ text (toString model.queueStats.numBuilding)], text " in progress"]
23 | , span [] [ whiteBadge [] [ text (toString model.queueStats.numWaiting)], text " in queue" ]
24 | , span [] [ whiteBadge [] [ text (toString model.queueStats.numMachines) ], text " machines" ]
25 | , text "evaluations"
26 | , text "builds"
27 | , text "steps"
28 | ]
29 |
30 | view : AppModel -> List (Html Msg)
31 | view model =
32 | let
33 | menuItems =
34 | case model.user of
35 | Nothing ->
36 | [ Menu.item
37 | [ Menu.onSelect <| LoginUserClick Google ]
38 | [ menuIcon "input"
39 | , text "Sign in with Google" ]
40 | , Menu.item
41 | [ Menu.onSelect <| LoginUserClick Hydra ]
42 | [ menuIcon "input"
43 | , text "Sign in with a password" ]
44 | ]
45 |
46 | Just user ->
47 | [ Menu.item
48 | [ Menu.onSelect <| PreferencesClick ]
49 | [ menuIcon "settings"
50 | , text "Preferences" ]
51 | , Menu.item
52 | [ Menu.onSelect <| LogoutUserClick ]
53 | [ Icon.view "power_settings_new" [ Options.css "width" "40px"
54 | , Options.css "color" "red" ]
55 | , text "Sign out" ]
56 | ]
57 |
58 | in [ Layout.row []
59 | [ Layout.title
60 | [ ]
61 | ([ if model.hydraConfig.logo == "" then
62 | text ""
63 | else
64 | img
65 | ([ src model.hydraConfig.logo
66 | , alt "Hydra Logo"
67 | , class "logo"
68 | , style [ ( "height", "37px" ), ( "margin", "5px" ) ]
69 | ] ++ (onClickPage Home))
70 | []
71 | ] ++ (breadCrumbs model))
72 | , Layout.spacer
73 | , Layout.navigation []
74 | [ App.map LiveSearchMsg (LiveSearch.view model)
75 | , span [] (Maybe.withDefault [] (Maybe.map (\user -> [ text user.name ]) model.user))
76 | , Menu.render Mdl [1] model.mdl
77 | [ Menu.ripple
78 | , Menu.bottomRight
79 | , Menu.icon "account_circle" ]
80 | menuItems
81 | ]
82 | ]
83 | ]
84 |
--------------------------------------------------------------------------------
/src/Pages/Jobset.elm:
--------------------------------------------------------------------------------
1 | module Pages.Jobset exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Material.Menu as Menu
6 | import Material.List as List
7 | import Material.Options as Options
8 | import Material.Tabs as Tabs
9 | import Material.Table as Table
10 |
11 | import Models exposing (..)
12 | import Msg exposing (..)
13 | import Utils exposing (..)
14 | import Urls as Urls exposing(..)
15 |
16 |
17 | view : AppModel -> List (Html Msg)
18 | view model =
19 | case model.jobsetPage of
20 | Err _ -> [p [] [ text "TODO"]]
21 | Ok jobset ->
22 | renderHeader model "Jobset" (Just jobset.name) Nothing
23 | ++
24 | [ Tabs.render Mdl [4] model.mdl
25 | [ Tabs.ripple
26 | --, Tabs.onSelectTab SelectTab
27 | --, Tabs.activeTab model.tab
28 | ]
29 | [ Tabs.label
30 | [ Options.center ]
31 | [ Options.span [ Options.css "width" "4px" ] []
32 | , text "Evaluations"
33 | ]
34 | , Tabs.label
35 | [ Options.center ]
36 | [ Options.span [ Options.css "width" "4px" ] []
37 | , text "Jobs"
38 | ]
39 | , Tabs.label
40 | [ Options.center ]
41 | [ Options.span [ Options.css "width" "4px" ] []
42 | , text "Channels"
43 | ]
44 | ]
45 | (if List.isEmpty [ 1 ]
46 | then render404 "No evaluations yet."
47 | else [ List.ul []
48 | [ List.li [ List.withSubtitle ]
49 | [ List.content []
50 | [ text "Lastest check"
51 | , List.subtitle [] [ text "2016-08-06 12:38:01" ]
52 | ]
53 | ]
54 | , List.li [ List.withSubtitle ]
55 | [ List.content []
56 | [ text "Lastest evaluation"
57 | , List.subtitle [] [ a
58 | [ href "TODO" ]
59 | [ text "2016-08-06 17:45:55" ]
60 | ]
61 | ]
62 | ]
63 | , List.li [ List.withSubtitle ]
64 | [ List.content []
65 | [ text "Lastest finished evaluation"
66 | , List.subtitle [] [ a
67 | [ href "TODO" ]
68 | [ text "2016-08-06 17:45:55" ] ]
69 | ]
70 | ]
71 | ]
72 | , Table.table [ Options.css "width" "100%" ]
73 | [ Table.thead []
74 | [ Table.tr []
75 | [ Table.th [] [ text "#"]
76 | , Table.th [ ] [ text "Input chages" ]
77 | , Table.th [ ] [ text "Job status" ]
78 | , Table.th [ ] [ text "Time" ]
79 | ]
80 | ]
81 | , Table.tbody []
82 | (jobset.evaluations |> List.map (\evaluation ->
83 | Table.tr []
84 | [ Table.td [] [ a
85 | (onClickPage (Urls.Jobset "123" "foo"))
86 | [ text "123" ] ]
87 | , Table.td [] [ text "snabbBsrc → e1fdc74" ]
88 | , Table.td [] (statusLabels 145 62 23)
89 | , Table.td [] [ text "2016-08-05 13:43:40" ]
90 | ]
91 | )
92 | )
93 | ]
94 | ]
95 | )
96 | ]
97 |
--------------------------------------------------------------------------------
/src/Utils.elm:
--------------------------------------------------------------------------------
1 | module Utils exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Html.Events exposing (..)
6 | import Json.Decode as Json
7 | import Material.Elevation as Elevation
8 | import Material.Button as Button
9 | import Material.Color as Color
10 | import Material.Icon as Icon
11 | import Material.Options as Options
12 | import Msg exposing (..)
13 | import Urls exposing (..)
14 | import Models exposing (..)
15 |
16 |
17 | menuIcon : String -> Html Msg
18 | menuIcon name =
19 | Icon.view name [ Options.css "width" "40px" ]
20 |
21 |
22 | onPreventDefaultClick : msg -> Attribute msg
23 | onPreventDefaultClick message =
24 | onWithOptions "click" { defaultOptions | preventDefault = True } (Json.succeed message)
25 |
26 |
27 | onClickPage : Page -> List (Attribute Msg)
28 | onClickPage page =
29 | [ style [("pointer", "cursor")]
30 | , href (pageToURL page)
31 | , onPreventDefaultClick (NewPage page)
32 | ]
33 |
34 |
35 | optionalTag : Bool -> Html Msg -> Html Msg
36 | optionalTag doInclude html =
37 | if doInclude then
38 | html
39 | else
40 | text ""
41 |
42 |
43 | statusLabels : Int -> Int -> Int -> List (Html Msg)
44 | statusLabels succeeded failed queued =
45 | [ optionalTag (succeeded > 0)
46 | (badge
47 | (Color.color Color.Green Color.S500)
48 | [ Options.attribute <| title "Jobs succeeded" ]
49 | [ text (toString succeeded) ]
50 | )
51 | , optionalTag (failed > 0)
52 | (badge
53 | (Color.color Color.Red Color.S500)
54 | [ Options.attribute <| title "Jobs failed" ]
55 | [ text (toString failed) ]
56 | )
57 | , optionalTag (queued > 0)
58 | (badge
59 | (Color.color Color.Grey Color.S500)
60 | [ Options.attribute <| title "Jobs in queue" ]
61 | [ text (toString queued) ]
62 | )
63 | ]
64 |
65 |
66 | badge : Color.Color -> List (Options.Property c Msg) -> List (Html Msg) -> Html Msg
67 | badge color properties content =
68 | Options.span
69 | ([ Options.css "border-radius" "9px"
70 | , Options.css "padding" "3px 5px"
71 | , Options.css "line-height" "14px"
72 | , Options.css "white-space" "nowrap"
73 | , Options.css "font-weight" "bold"
74 | , Options.css "font-size" "12px"
75 | , Options.css "margin" "0 3px"
76 | , Options.css "cursor" "help"
77 | , Options.css "color" (if color == Color.white then "#000" else "#FFF")
78 | , Color.background color
79 | ] ++ properties)
80 | content
81 |
82 |
83 | whiteBadge : List (Options.Property c Msg) -> List (Html Msg) -> Html Msg
84 | whiteBadge properties content =
85 | badge Color.white properties content
86 |
87 |
88 | render404 : String -> List (Html Msg)
89 | render404 reason =
90 | [ Options.div
91 | [ Elevation.e2
92 | , Options.css "padding" "40px"
93 | , Options.center
94 | ]
95 | [ text reason ]
96 | ]
97 |
98 |
99 | renderHeader : AppModel -> String -> Maybe String -> Maybe Page -> List (Html Msg)
100 | renderHeader model name subname page =
101 | let
102 | subnameHtml = case subname of
103 | Nothing -> []
104 | Just s -> [ small [ style [ ( "margin-left", "10px" ) ]]
105 | [ text s ]]
106 | pageHtml = case page of
107 | Nothing -> []
108 | Just p -> [
109 | Button.render Mdl [2] model.mdl
110 | [ Button.fab
111 | , Button.colored
112 | , Button.onClick (NewPage p)
113 | , Options.css "margin-left" "20px"
114 | ]
115 | [ Icon.i "add"]
116 | ]
117 | in
118 | [ h1
119 | [ style [ ( "margin-bottom", "30px" ) ] ]
120 | ([ text name ] ++ subnameHtml ++ pageHtml)
121 | ]
122 |
--------------------------------------------------------------------------------
/src/Components/LiveSearch.elm:
--------------------------------------------------------------------------------
1 | module Components.LiveSearch exposing (update, view, search, Msg)
2 |
3 | import Html exposing (..)
4 | import Html.Events exposing (..)
5 | import Json.Decode as Json
6 | import String
7 | import Material
8 | import Material.Textfield as Textfield
9 | import Material.Color as Color
10 | import Material.Icon as Icon
11 | import Material.Options as Options
12 |
13 | import Models exposing (..)
14 |
15 |
16 | type Msg
17 | = SearchInput String
18 | | SearchEscape
19 | | Mdl (Material.Msg Msg)
20 |
21 |
22 | compareCaseInsensitve : String -> String -> Bool
23 | compareCaseInsensitve s1 s2 =
24 | String.contains (String.toLower s1) (String.toLower s2)
25 |
26 |
27 | {-| Filter project by Project name or Jobset name
28 | -}
29 | searchProject : String -> Project -> Project
30 | searchProject searchstring project =
31 | let
32 | projectFilteredJobsets =
33 | { project | jobsets = List.map (filterByName searchstring) project.jobsets }
34 |
35 | hasJobsets =
36 | List.any (\j -> j.isShown) projectFilteredJobsets.jobsets
37 |
38 | newproject =
39 | filterByName searchstring project
40 | in
41 | if
42 | newproject.isShown
43 | -- if project matches, display all jobsets
44 | then
45 | { newproject | jobsets = List.map (\j -> { j | isShown = True }) newproject.jobsets }
46 | else if
47 | hasJobsets
48 | -- if project doesn't match, only display if any of jobsets match
49 | then
50 | { projectFilteredJobsets | isShown = True }
51 | else
52 | newproject
53 |
54 |
55 | filterByName : String -> { b | name : String, isShown : Bool } -> { b | name : String, isShown : Bool }
56 | filterByName searchstring project =
57 | if compareCaseInsensitve searchstring project.name then
58 | { project | isShown = True }
59 | else
60 | { project | isShown = False }
61 |
62 |
63 | {-| Filter any record by isShown field
64 |
65 | TODO: recursively apply all lists in the structure
66 | -}
67 | search : List { a | isShown : Bool } -> List { a | isShown : Bool }
68 | search projects =
69 | List.filter (\x -> x.isShown) projects
70 |
71 |
72 | update : Msg -> AppModel -> ( AppModel, Cmd Msg )
73 | update msg model =
74 | case msg of
75 | SearchInput searchstring ->
76 | let
77 | newprojects =
78 | List.map (searchProject searchstring) model.projects
79 | in
80 | ( { model
81 | | projects = newprojects
82 | , searchString = searchstring
83 | }
84 | , Cmd.none
85 | )
86 |
87 | -- on Escape, clear search bar and return all projects/jobsets
88 | SearchEscape ->
89 | ( { model
90 | | searchString = ""
91 | , projects = List.map (searchProject "") model.projects
92 | }
93 | , Cmd.none
94 | )
95 | Mdl msg' ->
96 | Material.update msg' model
97 |
98 |
99 | view : AppModel -> Html Msg
100 | view model =
101 | span
102 | []
103 | [ Textfield.render Mdl [0] model.mdl
104 | [ Textfield.label "Search"
105 | , Textfield.floatingLabel
106 | , Textfield.text'
107 | , Textfield.onInput SearchInput
108 | , onEscape SearchEscape
109 | , Textfield.value model.searchString
110 | , Textfield.style [ Options.css "border-radius" "0.5em"
111 | , Color.background Color.primaryDark ]
112 | ]
113 | , Icon.view
114 | "search"
115 | [ Icon.onClick (SearchInput model.searchString) -- TODO: trigger a proper search page
116 | , Options.css "position" "relative"
117 | , Options.css "top" "8px"
118 | , Options.css "right" "28px"
119 | , Options.css "z-index" "100"
120 | , Options.css "cursor" "pointer"
121 | ]
122 | ]
123 |
124 | onEscape : msg -> Textfield.Property msg
125 | onEscape msg =
126 | Textfield.on "keydown" (Json.map (always msg) (Json.customDecoder keyCode isEscape))
127 |
128 |
129 | isEscape : Int -> Result String ()
130 | isEscape code =
131 | case code of
132 | 27 ->
133 | Ok ()
134 |
135 | _ ->
136 | Err "not the right key code"
137 |
--------------------------------------------------------------------------------
/src/Models.elm:
--------------------------------------------------------------------------------
1 | module Models exposing (..)
2 |
3 | import Material
4 | import Date
5 |
6 | import Urls exposing (..)
7 |
8 |
9 | type AlertType
10 | = Danger
11 | | Info
12 | | Warning
13 | | Success
14 |
15 |
16 | type alias Alert =
17 | { kind : AlertType
18 | , msg : String
19 | }
20 |
21 |
22 | type alias User =
23 | { id : String
24 | , name : String
25 | , email : String
26 | , roles : List String
27 | , recieveEvaluationErrors : Bool
28 | }
29 |
30 |
31 | type alias Jobset =
32 | { id : String
33 | , name : String
34 | , description : String
35 | , queued : Int
36 | , failed : Int
37 | , succeeded : Int
38 | , lastEvaluation : String
39 | , isShown : Bool
40 | }
41 |
42 |
43 | type alias Project =
44 | { id : String
45 | , name : String
46 | , description : String
47 | , jobsets : List Jobset
48 | , isShown : Bool
49 | }
50 |
51 |
52 | type alias HydraConfig =
53 | { logo : String
54 | , hydraVersion : String
55 | , nixVersion : String
56 | }
57 |
58 |
59 | type alias QueueStats =
60 | { numBuilding : Int
61 | , numWaiting : Int
62 | , numMachines : Int
63 | }
64 |
65 | type alias JobSummary =
66 | { succeeded : Int
67 | , failed : Int
68 | , inQueue : Int
69 | }
70 |
71 | type alias Evaluation =
72 | { id : Int
73 | , inputChanges : String
74 | , jobSummary : JobSummary
75 | , evaluatedAt : Result String Date.Date
76 | }
77 |
78 | type alias JobsetPage =
79 | { latestCheckTime : Result String Date.Date
80 | , latestEvaluationTime : Result String Date.Date
81 | , latestFinishedEvaluationTime : Result String Date.Date
82 | , evaluations : List Evaluation
83 | , name : String
84 | }
85 |
86 | type AjaxError msg =
87 | AjaxFail msg |
88 | Loading
89 |
90 | type alias AppModel =
91 | { alert : Maybe Alert
92 | , hydraConfig : HydraConfig
93 | , projects : List Project
94 | , jobsets : Result AjaxError (List Jobset)
95 | , jobsetPage : Result AjaxError JobsetPage
96 | , user : Maybe User
97 | , mdl : Material.Model
98 | , queueStats : QueueStats
99 | , searchString : String
100 | , currentPage : Page
101 | }
102 |
103 |
104 | initialModel : AppModel
105 | initialModel =
106 | let
107 | jobsets = [ { id = "release-16.03"
108 | , name = "release-16.03"
109 | , description = "NixOS 16.03 release branch"
110 | , queued = 5
111 | , failed = 275
112 | , succeeded = 24315
113 | , lastEvaluation = "2016-05-21 13:57:13"
114 | , isShown = True
115 | }
116 | , { id = "trunk-combined"
117 | , name = "trunk-combined"
118 | , description = "Combined NixOS/Nixpkgs unstable"
119 | , queued = 1
120 | , failed = 406
121 | , succeeded = 24243
122 | , lastEvaluation = "2016-05-21 13:57:03"
123 | , isShown = True
124 | }
125 | ]
126 | in
127 | { alert = Nothing
128 | , user = Nothing
129 | , mdl = Material.model
130 | , currentPage = Home
131 | , searchString = ""
132 | , hydraConfig =
133 | -- TODO: downsize logo, serve it with webpack
134 | { logo = "http://nixos.org/logo/nixos-logo-only-hires.png"
135 | , hydraVersion = "0.1.1234.abcdef"
136 | , nixVersion = "1.12pre1234_abcdef"
137 | }
138 | , queueStats = QueueStats 124 32345 19
139 | -- Pages
140 | , jobsetPage = Ok
141 | { latestCheckTime = Date.fromString "2016-08-06 12:38:01"
142 | , latestEvaluationTime = Date.fromString "2016-08-06 17:45:55"
143 | , latestFinishedEvaluationTime = Date.fromString "2016-08-06 17:45:55"
144 | , name = "Hardcodedfoobar"
145 | , evaluations =
146 | [ { id = 123
147 | , inputChanges = "snabbBsrc → e1fdc74"
148 | , jobSummary = { succeeded = 145, failed = 62, inQueue = 23 }
149 | , evaluatedAt = Date.fromString "2016-08-05 13:43:40"
150 | }
151 |
152 | ]
153 | }
154 | , jobsets = Ok []
155 | , projects =
156 | [ { id = "nixos"
157 | , name = "NixOS"
158 | , description = "the purely functional Linux distribution"
159 | , isShown = True
160 | , jobsets = jobsets
161 | }
162 | , { id = "nix"
163 | , name = "Nix"
164 | , description = "the purely functional package manager"
165 | , isShown = True
166 | , jobsets =
167 | [ { id = "master"
168 | , name = "master"
169 | , description = "Master branch"
170 | , queued = 0
171 | , failed = 33
172 | , succeeded = 1
173 | , isShown = True
174 | , lastEvaluation = "2016-05-21 13:57:13"
175 | }
176 | ]
177 | }
178 | , { id = "nixpkgs"
179 | , name = "Nixpkgs"
180 | , description = "Nix Packages collection"
181 | , isShown = True
182 | , jobsets =
183 | [ { id = "trunk"
184 | , name = "trunk"
185 | , description = "Trunk"
186 | , isShown = True
187 | , queued = 0
188 | , failed = 7798
189 | , succeeded = 24006
190 | , lastEvaluation = "2016-05-21 13:57:13"
191 | }
192 | , { id = "staging"
193 | , name = "staging"
194 | , description = "Staging"
195 | , isShown = True
196 | , queued = 0
197 | , failed = 31604
198 | , succeeded = 63
199 | , lastEvaluation = "2016-05-21 13:57:03"
200 | }
201 | ]
202 | }
203 | , { id = "nixops"
204 | , name = "NixOps"
205 | , description = "Deploying NixOS machines"
206 | , isShown = True
207 | , jobsets = []
208 | }
209 | ]
210 | }
211 |
--------------------------------------------------------------------------------
/src/Pages/Project.elm:
--------------------------------------------------------------------------------
1 | module Pages.Project exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Maybe
6 | import List
7 | import Material.Button as Button
8 | import Material.Options as Options
9 | import Material.Elevation as Elevation
10 | import Material.Menu as Menu
11 | import Material.Table as Table
12 | import Material.Textfield as Textfield
13 | import Material.Toggles as Toggles
14 |
15 | import Components.LiveSearch exposing (search)
16 | import Components.Help exposing (..)
17 | import Msg exposing (..)
18 | import Models exposing (Project, AppModel)
19 | import Urls exposing (..)
20 | import Utils exposing (..)
21 |
22 |
23 |
24 | view : AppModel -> Page -> List (Html Msg)
25 | view model page =
26 | case page of
27 | Home ->
28 | projectsView model model.projects
29 |
30 | Project name ->
31 | case List.head (List.filter (\p -> p.name == name) model.projects) of
32 | Just project ->
33 | [ renderProject model 0 project ]
34 |
35 | Nothing ->
36 | render404 ("Project " ++ name ++ " does not exist.")
37 | NewProject ->
38 | newProjectView model
39 |
40 | -- TODO: get rid of this
41 | _ -> []
42 |
43 |
44 | projectsView : AppModel -> List Project -> List (Html Msg)
45 | projectsView model projects =
46 | let
47 | newprojects =
48 | List.indexedMap (renderProject model) (search projects)
49 | in
50 | renderHeader model "Projects" Nothing (Just NewProject)
51 | ++ if List.isEmpty newprojects then
52 | render404 "Zero projects. Maybe add one?"
53 | else
54 | newprojects
55 |
56 |
57 | newProjectView : AppModel -> List (Html Msg)
58 | newProjectView model =
59 | renderHeader model "Add a new project" Nothing Nothing
60 | ++
61 | [ Html.form []
62 | [ Textfield.render Mdl [5] model.mdl
63 | [ Textfield.label "Identifier (e.g. hydra)"
64 | , Textfield.floatingLabel
65 | , Textfield.text'
66 | , Options.css "display" "block"
67 | ]
68 | , Textfield.render Mdl [6] model.mdl
69 | [ Textfield.label "Display name (e.g. Hydra)"
70 | , Textfield.floatingLabel
71 | , Textfield.text'
72 | , Options.css "display" "block"
73 | ]
74 | , Textfield.render Mdl [7] model.mdl
75 | [ Textfield.label "Description (e.g. Builds Nix expressions and provides insight about the process)"
76 | , Textfield.floatingLabel
77 | , Textfield.text'
78 | , Options.css "display" "block"
79 | ]
80 | , Textfield.render Mdl [8] model.mdl
81 | [ Textfield.label "URL (e.g. https://github.com/NixOS/hydra)"
82 | , Textfield.floatingLabel
83 | , Textfield.text'
84 | , Options.css "display" "block"
85 | ]
86 | ]
87 | , Toggles.checkbox Mdl [9] model.mdl
88 | [ Toggles.ripple
89 | --, Toggles.onClick MyToggleMsg
90 | ]
91 | [ text "Is visible on the project list?" ]
92 | , Toggles.checkbox Mdl [10] model.mdl
93 | [ Toggles.ripple
94 | --, Toggles.onClick MyToggleMsg
95 | ]
96 | [ text "Is enabled?" ]
97 | , Textfield.render Mdl [11] model.mdl
98 | [ Textfield.label "Owner"
99 | , Textfield.floatingLabel
100 | , Textfield.text'
101 | , Options.css "display" "block"
102 | , Textfield.value (Maybe.withDefault "" (Maybe.map (\u -> u.id) model.user))
103 | ]
104 | , Button.render Mdl [12] model.mdl
105 | [ Button.raised
106 | , Button.colored
107 | --, Button.onClick MyClickMsg
108 | ]
109 | [ text "Create project"]
110 | ]
111 |
112 |
113 | renderProject : AppModel -> Int -> Project -> Html Msg
114 | renderProject model i project =
115 | Options.div
116 | [ Elevation.e2
117 | , Options.css "margin" "30px"
118 | , Options.css "padding" "8px"
119 | ]
120 | [ h3
121 | []
122 | [ a (onClickPage (Urls.Project project.name))
123 | [ Options.span
124 | [ Options.css "margin" "16px" ]
125 | [ text (project.name) ]
126 | ]
127 | , small
128 | [ class "hidden-xs" ]
129 | [ text ("(" ++ project.description ++ ")") ]
130 | -- TODO: correct index
131 | , Menu.render Mdl [i + 10] model.mdl
132 | [ Menu.ripple
133 | , Menu.bottomRight
134 | , Options.css "float" "right"
135 | ]
136 | [ Menu.item []
137 | [ menuIcon "add"
138 | , text "Add a jobset"
139 | ]
140 | , Menu.item []
141 | [ menuIcon "settings"
142 | , text "Configuration"
143 | ]
144 | , Menu.item []
145 | [ menuIcon "delete"
146 | , text "Delete the project"
147 | ]
148 | ]
149 | ]
150 | , if List.isEmpty project.jobsets
151 | then Options.span
152 | [ Options.center
153 | , Options.css "margin" "30px"
154 | ]
155 | [ text "No Jobsets configured yet." ]
156 | else Table.table [ Options.css "width" "100%" ]
157 | [ Table.thead []
158 | [ Table.tr []
159 | [ Table.th [] [ text "Jobset", jobsetHelp model]
160 | , Table.th [ ] [ text "Description" ]
161 | , Table.th [ ] [ text "Job status" ]
162 | , Table.th [ ] [ text "Last evaluation" ]
163 | ]
164 | ]
165 | , Table.tbody []
166 | (search project.jobsets |> List.map (\jobset ->
167 | Table.tr []
168 | [ Table.td [] [ a
169 | (onClickPage (Urls.Jobset project.name jobset.id))
170 | [ text jobset.name ] ]
171 | , Table.td [] [ text jobset.description ]
172 | , Table.td [] (statusLabels jobset.succeeded jobset.failed jobset.queued)
173 | , Table.td [] [ text jobset.lastEvaluation ]
174 | ]
175 |
176 | )
177 | )
178 | ]
179 | ]
180 |
--------------------------------------------------------------------------------