🎇 Gitlab pipelines monitor – in Elm

Posted October 2020

ℹ️ This is adapted from a talk I gave last year at Elm London Finally got round to bloggifying… You can also just check out the project on Gitlab.

screenshot

Background

What?

  • It’s a CI visualisation for Gitlab, now hosted at https://gitlab.com/declension
  • Uses Elm 0.19, ParcelJS, Gitlab v4 API
  • Designed for always-on bigscreens in the office (EDIT: remember them?)
  • Pretty alpha right now EDIT: seems to be stable, some missing things…

Why?

  • Elm! Good reasons! Elm!
  • 🆙 Exciting FP use-cases
  • 🤔 Also: learning experience on complex REST APIs, mutable data, and asynchronicity
  • 🎨 A focus on smooth animations and dark UIs
  • ⛙ Need to get default and other merge request branches, too

Existing Apps

Let’s face it – this is not new territory #### gitlab-monitor timoschwarzer/gitlab-monitor

gitlab-ci-monitor

globocom/gitlab-ci-monitor

Issues

  • Not registered as apps
  • “Not made here”, let’s be honest.
  • 😱 Dangerous lack of emoji on existing products
  • ⛔ This has to stop!

Challenges

The Gitlab API

Everything’s Changing

  • Projects’ metadata gets updated
  • Ordering is (currently) on last update, so order changes…
  • New branches appear…
  • New pipelines appear in various branches…
  • Previously become ✔️ or ✖️

Data fan-out

  • Each group has many projects
  • …each project has many pipelines
  • …each pipeline can have many stages
  • …each stage has many jobs…
  • …each job has many steps…
  • 😱

A word about GraphQL

Back to authentication

oAuth 2 Flows

  • 🔐 Web application – server side
  • 🌐 Implicit grant – SPAs etc
  • 😞 Resource owner password credentials
    – secured host

Getting this to work

Elm setup

  • Elm! Simple Elm project
  • 🎁 Yarn to build, Parcel.JS to do all the magic
  • 💅 SASS for styling

Current Data Model

type alias Model =
    { config : Flags
    , key : Nav.Key
    , token : Maybe Token
    , data : GitlabData
    , url : Url.Url
    }

Data structure itself

type alias ProjectId =
    Int

type alias GitRef =
    String

type alias GitlabData =
    { projects : List Project
    , pipelines : Dict ProjectId (List Pipeline)
    }

Records

type alias Pipeline =
    { ref : String
    , id : Int
    , status : Status
    , url : String
    }

type alias Project =
    { id : Int
    , namespace : String
    , name : String
    , description : Maybe String
    , url : String
    , lastActivity : Posix
    }

type alias PipelineStore =
    Dict ProjectId (Dict GitRef (List Pipeline))

Implicit Grant Flow

  • One needs to register an app first:
  • Gitlab applications

Implicit Grant URL

  • Then redirect user to a URL like
  • https://gitlab.example.com/oauth/authorize ?client_id=APP_ID &redirect_uri=REDIRECT_URI &state=UNIQUE_STATE_HASH &scope=REQUESTED_SCOPES
  • They’ll come back at REDIRECT_URI, with a token in the URL

HTTP with auth

getUrl : Token -> Url -> (Result Http.Error a -> Msg) -> Decoder a -> Cmd Msg
getUrl token url msg decoder =
    Http.request
        { method = "GET"
        , url = Url.toString url
        , body = emptyBody
        , expect = expectJson msg decoder
        , headers = [ header "Authorization" ("Bearer " ++ token) ]
        , timeout = Just (httpTimeout * 1000.0)
        , tracker = Nothing
        }

Parse that token!

  • This… works

    extractToken : Url -> Maybe Token
    extractToken url =
        url.fragment
            |> Maybe.map (String.append "http://DUMMY?")
            |> Maybe.andThen Url.fromString
            |> Maybe.andThen (Url.Parser.parse (query toToken))
            |> Maybe.withDefault Nothing

Simple Subscriptions

subscriptions : Model -> Sub Msg
subscriptions _ =
    Time.every (10 * 1000) Tick
  • We’ll use chained messages though, so this isn’t everything…

Less Simple Messages

type Msg
    = Tick Posix
    | LinkClicked Browser.UrlRequest
    | GotPipelinesFor ProjectId (Result Http.Error (List Pipeline))
    | GotProjects (Result Http.Error (List Project))
    | UrlChanged Url.Url

Lessons learned

Nested mutable data is hard

  • Incomplete data makes for some challenges of its own in FP
  • Caching is key (arguably the whole data structure is a cache)
  • This is a case study for GraphQL in many ways

Parcel.JS

  • …is insanely fast 🚀
  • Elm integration “just works”
  • Supports .env, SASS out of the box
  • I / we won’t be using Webpack much now 😁 (EDIT: I haven’t and that’s great)

Obvious notes to self

  • Don’t leave slides to the day before itself
  • Especially if you also custom-build the slide framework…
  • Especially if that’s in Haskell 😆

But wait, there’s more

  • Mutable nested data can be… hard
  • Lenses aren’t really a thing in Elm: Elm Packages on “lens”
  • Which is probably good, I guess..
  • (though elm-monocle looks interesting)

Tags