github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/web/elm/src/Build/Build.elm (about)

     1  module Build.Build exposing
     2      ( bodyId
     3      , changeToBuild
     4      , documentTitle
     5      , getScrollBehavior
     6      , getUpdateMessage
     7      , handleCallback
     8      , handleDelivery
     9      , init
    10      , subscriptions
    11      , tooltip
    12      , update
    13      , view
    14      )
    15  
    16  import Api.Endpoints as Endpoints
    17  import Application.Models exposing (Session)
    18  import Assets
    19  import Build.Header.Header as Header
    20  import Build.Header.Models exposing (BuildPageType(..), CurrentOutput(..))
    21  import Build.Models exposing (Model, toMaybe)
    22  import Build.Output.Models exposing (OutputModel)
    23  import Build.Output.Output
    24  import Build.Shortcuts as Shortcuts
    25  import Build.StepTree.Models as STModels
    26  import Build.StepTree.StepTree as StepTree
    27  import Build.Styles as Styles
    28  import Concourse
    29  import Concourse.BuildStatus exposing (BuildStatus(..))
    30  import DateFormat
    31  import Dict exposing (Dict)
    32  import EffectTransformer exposing (ET)
    33  import HoverState
    34  import Html exposing (Html)
    35  import Html.Attributes
    36      exposing
    37          ( attribute
    38          , class
    39          , classList
    40          , href
    41          , id
    42          , style
    43          , tabindex
    44          , title
    45          )
    46  import Html.Lazy
    47  import Http
    48  import List.Extra
    49  import Login.Login as Login
    50  import Maybe.Extra
    51  import Message.Callback exposing (Callback(..))
    52  import Message.Effects as Effects exposing (Effect(..))
    53  import Message.Message exposing (DomID(..), Message(..))
    54  import Message.ScrollDirection as ScrollDirection
    55  import Message.Subscription as Subscription exposing (Delivery(..), Interval(..), Subscription(..))
    56  import Message.TopLevelMessage exposing (TopLevelMessage(..))
    57  import Routes
    58  import SideBar.SideBar as SideBar
    59  import StrictEvents exposing (onScroll)
    60  import String
    61  import Time
    62  import Tooltip
    63  import UpdateMsg exposing (UpdateMsg)
    64  import Views.Icon as Icon
    65  import Views.LoadingIndicator as LoadingIndicator
    66  import Views.NotAuthorized as NotAuthorized
    67  import Views.Spinner as Spinner
    68  import Views.Styles
    69  import Views.TopBar as TopBar
    70  
    71  
    72  bodyId : String
    73  bodyId =
    74      "build-body"
    75  
    76  
    77  type alias Flags =
    78      { highlight : Routes.Highlight
    79      , pageType : BuildPageType
    80      , fromBuildPage : Maybe Build.Header.Models.BuildPageType
    81      }
    82  
    83  
    84  type ScrollBehavior
    85      = ScrollWindow
    86      | ScrollToID String
    87      | NoScroll
    88  
    89  
    90  init : Flags -> ( Model, List Effect )
    91  init flags =
    92      changeToBuild
    93          flags
    94          ( { page = flags.pageType
    95            , id = 0
    96            , name =
    97                  case flags.pageType of
    98                      OneOffBuildPage id ->
    99                          String.fromInt id
   100  
   101                      JobBuildPage { buildName } ->
   102                          buildName
   103            , now = Nothing
   104            , job = Nothing
   105            , disableManualTrigger = False
   106            , history = []
   107            , nextPage = Nothing
   108            , prep = Nothing
   109            , duration = { startedAt = Nothing, finishedAt = Nothing }
   110            , status = BuildStatusPending
   111            , output = Empty
   112            , autoScroll = True
   113            , isScrollToIdInProgress = False
   114            , previousKeyPress = Nothing
   115            , isTriggerBuildKeyDown = False
   116            , showHelp = False
   117            , highlight = flags.highlight
   118            , authorized = True
   119            , fetchingHistory = False
   120            , scrolledToCurrentBuild = False
   121            , shiftDown = False
   122            , isUserMenuExpanded = False
   123            , hasLoadedYet = False
   124            , notFound = False
   125            , reapTime = Nothing
   126            }
   127          , [ GetCurrentTime
   128            , GetCurrentTimeZone
   129            , FetchAllPipelines
   130            ]
   131          )
   132  
   133  
   134  subscriptions : Model -> List Subscription
   135  subscriptions model =
   136      let
   137          buildEventsUrl =
   138              model.output
   139                  |> toMaybe
   140                  |> Maybe.andThen .eventStreamUrlPath
   141      in
   142      [ OnClockTick OneSecond
   143      , OnClockTick FiveSeconds
   144      , OnKeyDown
   145      , OnKeyUp
   146      , OnElementVisible
   147      , OnScrolledToId
   148      ]
   149          ++ (case buildEventsUrl of
   150                  Nothing ->
   151                      []
   152  
   153                  Just url ->
   154                      [ Subscription.FromEventSource ( url, [ "end", "event" ] ) ]
   155             )
   156  
   157  
   158  changeToBuild : Flags -> ET Model
   159  changeToBuild { highlight, pageType, fromBuildPage } ( model, effects ) =
   160      let
   161          newModel =
   162              { model | page = pageType }
   163      in
   164      (if fromBuildPage == Just pageType then
   165          ( newModel, effects )
   166  
   167       else
   168          ( { newModel
   169              | prep = Nothing
   170              , output = Empty
   171              , autoScroll = True
   172              , highlight = highlight
   173            }
   174          , case pageType of
   175              OneOffBuildPage buildId ->
   176                  effects
   177                      ++ [ CloseBuildEventStream, FetchBuild 0 buildId ]
   178  
   179              JobBuildPage jbi ->
   180                  effects
   181                      ++ [ CloseBuildEventStream, FetchJobBuild jbi ]
   182          )
   183      )
   184          |> Header.changeToBuild pageType
   185  
   186  
   187  extractTitle : Model -> String
   188  extractTitle model =
   189      case ( model.hasLoadedYet, model.job, model.page ) of
   190          ( True, Just { jobName }, _ ) ->
   191              jobName ++ " #" ++ model.name
   192  
   193          ( _, _, JobBuildPage { jobName, buildName } ) ->
   194              jobName ++ " #" ++ buildName
   195  
   196          ( _, _, OneOffBuildPage id ) ->
   197              "#" ++ String.fromInt id
   198  
   199  
   200  getUpdateMessage : Model -> UpdateMsg
   201  getUpdateMessage model =
   202      if model.notFound then
   203          UpdateMsg.NotFound
   204  
   205      else
   206          UpdateMsg.AOK
   207  
   208  
   209  handleCallback : Callback -> ET Model
   210  handleCallback action ( model, effects ) =
   211      (case action of
   212          BuildFetched (Ok build) ->
   213              handleBuildFetched build ( model, effects )
   214  
   215          BuildFetched (Err err) ->
   216              case err of
   217                  Http.BadStatus { status } ->
   218                      if status.code == 401 then
   219                          ( model, effects ++ [ RedirectToLogin ] )
   220  
   221                      else if status.code == 404 then
   222                          ( { model
   223                              | prep = Nothing
   224                              , notFound = True
   225                            }
   226                          , effects
   227                          )
   228  
   229                      else
   230                          ( model, effects )
   231  
   232                  _ ->
   233                      ( model, effects )
   234  
   235          BuildAborted (Ok ()) ->
   236              ( model, effects )
   237  
   238          BuildPrepFetched buildId (Ok buildPrep) ->
   239              if buildId == model.id then
   240                  handleBuildPrepFetched buildPrep ( model, effects )
   241  
   242              else
   243                  ( model, effects )
   244  
   245          BuildPrepFetched _ (Err err) ->
   246              case err of
   247                  Http.BadStatus { status } ->
   248                      if status.code == 401 then
   249                          ( { model | authorized = False }, effects )
   250  
   251                      else
   252                          ( model, effects )
   253  
   254                  _ ->
   255                      ( model, effects )
   256  
   257          PlanAndResourcesFetched buildId (Ok planAndResources) ->
   258              updateOutput
   259                  (Build.Output.Output.planAndResourcesFetched
   260                      buildId
   261                      planAndResources
   262                  )
   263                  ( model
   264                  , effects
   265                      ++ [ Effects.OpenBuildEventStream
   266                              { url =
   267                                  Endpoints.BuildEventStream
   268                                      |> Endpoints.Build buildId
   269                                      |> Endpoints.toString []
   270                              , eventTypes = [ "end", "event" ]
   271                              }
   272                         , SyncStickyBuildLogHeaders
   273                         ]
   274                  )
   275  
   276          PlanAndResourcesFetched _ (Err err) ->
   277              case err of
   278                  Http.BadStatus { status } ->
   279                      let
   280                          isAborted =
   281                              model.status == BuildStatusAborted
   282                      in
   283                      if status.code == 404 && isAborted then
   284                          ( { model | output = Cancelled }
   285                          , effects
   286                          )
   287  
   288                      else if status.code == 401 then
   289                          ( { model | authorized = False }, effects )
   290  
   291                      else
   292                          ( model, effects )
   293  
   294                  _ ->
   295                      ( model, effects )
   296  
   297          BuildJobDetailsFetched (Ok job) ->
   298              ( { model | disableManualTrigger = job.disableManualTrigger }
   299              , effects
   300              )
   301  
   302          BuildJobDetailsFetched (Err _) ->
   303              -- https://github.com/concourse/concourse/issues/3201
   304              ( model, effects )
   305  
   306          _ ->
   307              ( model, effects )
   308      )
   309          |> Header.handleCallback action
   310  
   311  
   312  handleDelivery : { a | hovered : HoverState.HoverState } -> Delivery -> ET Model
   313  handleDelivery session delivery ( model, effects ) =
   314      (case delivery of
   315          ClockTicked OneSecond time ->
   316              ( { model | now = Just time }, effects )
   317  
   318          ClockTicked FiveSeconds _ ->
   319              ( model, effects ++ [ Effects.FetchAllPipelines ] )
   320  
   321          WindowResized _ _ ->
   322              ( model, effects ++ [ SyncStickyBuildLogHeaders ] )
   323  
   324          EventsReceived (Ok envelopes) ->
   325              let
   326                  eventSourceClosed =
   327                      model.output
   328                          |> toMaybe
   329                          |> Maybe.map (.eventSourceOpened >> not)
   330                          |> Maybe.withDefault False
   331  
   332                  buildStatus =
   333                      envelopes
   334                          |> List.filterMap
   335                              (\{ data } ->
   336                                  case data of
   337                                      STModels.BuildStatus status date ->
   338                                          Just ( status, date )
   339  
   340                                      _ ->
   341                                          Nothing
   342                              )
   343                          |> List.Extra.last
   344  
   345                  ( newModel, newEffects ) =
   346                      updateOutput
   347                          (Build.Output.Output.handleEnvelopes envelopes)
   348                          (if eventSourceClosed && (envelopes |> List.map .data |> List.member STModels.NetworkError) then
   349                              ( { model | authorized = False }, effects )
   350  
   351                           else
   352                              case getScrollBehavior model of
   353                                  ScrollWindow ->
   354                                      ( model
   355                                      , effects
   356                                          ++ [ Effects.Scroll
   357                                                  ScrollDirection.ToBottom
   358                                                  bodyId
   359                                             ]
   360                                      )
   361  
   362                                  ScrollToID id ->
   363                                      ( { model
   364                                          | highlight = Routes.HighlightNothing
   365                                          , autoScroll = False
   366                                          , isScrollToIdInProgress = True
   367                                        }
   368                                      , effects
   369                                          ++ [ Effects.Scroll
   370                                                  (ScrollDirection.ToId id)
   371                                                  bodyId
   372                                             ]
   373                                      )
   374  
   375                                  NoScroll ->
   376                                      ( model, effects )
   377                          )
   378              in
   379              case ( model.hasLoadedYet, buildStatus ) of
   380                  ( True, Just ( status, _ ) ) ->
   381                      ( newModel
   382                      , if Concourse.BuildStatus.isRunning model.status then
   383                          newEffects ++ [ SetFavIcon (Just status) ]
   384  
   385                        else
   386                          newEffects
   387                      )
   388  
   389                  _ ->
   390                      ( newModel, newEffects )
   391  
   392          ScrolledToId _ ->
   393              ( { model | isScrollToIdInProgress = False }, effects )
   394  
   395          _ ->
   396              ( model, effects )
   397      )
   398          |> Tooltip.handleDelivery session delivery
   399          |> Shortcuts.handleDelivery delivery
   400          |> Header.handleDelivery delivery
   401  
   402  
   403  update : Message -> ET Model
   404  update msg ( model, effects ) =
   405      (case msg of
   406          Click (BuildTab id name) ->
   407              ( model
   408              , effects
   409                  ++ [ NavigateTo <|
   410                          Routes.toString <|
   411                              Routes.buildRoute id name model.job
   412                     ]
   413              )
   414  
   415          Click TriggerBuildButton ->
   416              (model.job
   417                  |> Maybe.map (DoTriggerBuild >> (::) >> Tuple.mapSecond)
   418                  |> Maybe.withDefault identity
   419              )
   420                  ( model, effects )
   421  
   422          Click AbortBuildButton ->
   423              ( model, DoAbortBuild model.id :: effects )
   424  
   425          Click (StepHeader id) ->
   426              updateOutput
   427                  (Build.Output.Output.handleStepTreeMsg <| StepTree.toggleStep id)
   428                  ( model, effects ++ [ SyncStickyBuildLogHeaders ] )
   429  
   430          Click (StepInitialization id) ->
   431              updateOutput
   432                  (Build.Output.Output.handleStepTreeMsg <| StepTree.toggleStepInitialization id)
   433                  ( model, effects ++ [ SyncStickyBuildLogHeaders ] )
   434  
   435          Click (StepSubHeader id i) ->
   436              updateOutput
   437                  (Build.Output.Output.handleStepTreeMsg <| StepTree.toggleStepSubHeader id i)
   438                  ( model, effects ++ [ SyncStickyBuildLogHeaders ] )
   439  
   440          Click (StepTab id tab) ->
   441              updateOutput
   442                  (Build.Output.Output.handleStepTreeMsg <| StepTree.switchTab id tab)
   443                  ( model, effects )
   444  
   445          SetHighlight id line ->
   446              updateOutput
   447                  (Build.Output.Output.handleStepTreeMsg <| StepTree.setHighlight id line)
   448                  ( model, effects )
   449  
   450          ExtendHighlight id line ->
   451              updateOutput
   452                  (Build.Output.Output.handleStepTreeMsg <| StepTree.extendHighlight id line)
   453                  ( model, effects )
   454  
   455          GoToRoute route ->
   456              ( model, effects ++ [ NavigateTo <| Routes.toString <| route ] )
   457  
   458          Scrolled { scrollHeight, scrollTop, clientHeight } ->
   459              ( { model
   460                  | autoScroll =
   461                      (scrollHeight == scrollTop + clientHeight)
   462                          && not model.isScrollToIdInProgress
   463                }
   464              , effects
   465              )
   466  
   467          _ ->
   468              ( model, effects )
   469      )
   470          |> Header.update msg
   471  
   472  
   473  getScrollBehavior : Model -> ScrollBehavior
   474  getScrollBehavior model =
   475      case model.highlight of
   476          Routes.HighlightLine stepID lineNumber ->
   477              ScrollToID <| stepID ++ ":" ++ String.fromInt lineNumber
   478  
   479          Routes.HighlightRange stepID beginning end ->
   480              if beginning <= end then
   481                  ScrollToID <| stepID ++ ":" ++ String.fromInt beginning
   482  
   483              else
   484                  NoScroll
   485  
   486          Routes.HighlightNothing ->
   487              if model.autoScroll then
   488                  if model.hasLoadedYet then
   489                      case model.status of
   490                          BuildStatusSucceeded ->
   491                              NoScroll
   492  
   493                          BuildStatusPending ->
   494                              NoScroll
   495  
   496                          _ ->
   497                              ScrollWindow
   498  
   499                  else
   500                      NoScroll
   501  
   502              else
   503                  NoScroll
   504  
   505  
   506  updateOutput :
   507      (OutputModel -> ( OutputModel, List Effect ))
   508      -> ET Model
   509  updateOutput updater ( model, effects ) =
   510      case model.output of
   511          Output output ->
   512              let
   513                  ( newOutput, outputEffects ) =
   514                      updater output
   515  
   516                  newModel =
   517                      { model
   518                          | output =
   519                              -- model.output must be equal-by-reference
   520                              -- to its previous value when passed
   521                              -- into `Html.Lazy.lazy3` below.
   522                              if newOutput /= output then
   523                                  Output newOutput
   524  
   525                              else
   526                                  model.output
   527                      }
   528              in
   529              ( newModel, effects ++ outputEffects )
   530  
   531          _ ->
   532              ( model, effects )
   533  
   534  
   535  handleBuildFetched : Concourse.Build -> ET Model
   536  handleBuildFetched build ( model, effects ) =
   537      let
   538          withBuild =
   539              { model
   540                  | reapTime = build.reapTime
   541                  , output =
   542                      if model.hasLoadedYet then
   543                          model.output
   544  
   545                      else
   546                          Empty
   547              }
   548  
   549          fetchJobAndHistory =
   550              case ( model.job, build.job ) of
   551                  ( Nothing, Just buildJob ) ->
   552                      [ FetchBuildJobDetails buildJob
   553                      , FetchBuildHistory buildJob Nothing
   554                      ]
   555  
   556                  _ ->
   557                      []
   558  
   559          ( newModel, cmd ) =
   560              if build.status == BuildStatusPending then
   561                  ( withBuild, effects ++ pollUntilStarted build.id )
   562  
   563              else if build.reapTime == Nothing then
   564                  case model.prep of
   565                      Nothing ->
   566                          initBuildOutput build ( withBuild, effects )
   567  
   568                      Just _ ->
   569                          let
   570                              ( newNewModel, newEffects ) =
   571                                  initBuildOutput build ( withBuild, effects )
   572                          in
   573                          ( newNewModel
   574                          , newEffects
   575                              ++ [ FetchBuildPrep 1000 build.id ]
   576                          )
   577  
   578              else
   579                  ( withBuild, effects )
   580      in
   581      if not model.hasLoadedYet || build.id == model.id then
   582          ( newModel
   583          , cmd
   584              ++ fetchJobAndHistory
   585              ++ [ SetFavIcon (Just build.status), Focus bodyId ]
   586          )
   587  
   588      else
   589          ( model, effects )
   590  
   591  
   592  pollUntilStarted : Int -> List Effect
   593  pollUntilStarted buildId =
   594      [ FetchBuild 1000 buildId
   595      , FetchBuildPrep 1000 buildId
   596      ]
   597  
   598  
   599  initBuildOutput : Concourse.Build -> ET Model
   600  initBuildOutput build ( model, effects ) =
   601      let
   602          ( output, outputCmd ) =
   603              Build.Output.Output.init model.highlight build
   604      in
   605      ( { model | output = Output output }
   606      , effects ++ outputCmd
   607      )
   608  
   609  
   610  handleBuildPrepFetched : Concourse.BuildPrep -> ET Model
   611  handleBuildPrepFetched buildPrep ( model, effects ) =
   612      ( { model | prep = Just buildPrep }
   613      , effects
   614      )
   615  
   616  
   617  documentTitle : Model -> String
   618  documentTitle =
   619      extractTitle
   620  
   621  
   622  view : Session -> Model -> Html Message
   623  view session model =
   624      let
   625          route =
   626              case model.page of
   627                  OneOffBuildPage buildId ->
   628                      Routes.OneOffBuild
   629                          { id = buildId
   630                          , highlight = model.highlight
   631                          }
   632  
   633                  JobBuildPage buildId ->
   634                      Routes.Build
   635                          { id = buildId
   636                          , highlight = model.highlight
   637                          }
   638      in
   639      Html.div
   640          (id "page-including-top-bar" :: Views.Styles.pageIncludingTopBar)
   641          [ Html.div
   642              (id "top-bar-app" :: Views.Styles.topBar False)
   643              [ SideBar.hamburgerMenu session
   644              , TopBar.concourseLogo
   645              , breadcrumbs model
   646              , Login.view session.userState model
   647              ]
   648          , Html.div
   649              (id "page-below-top-bar" :: Views.Styles.pageBelowTopBar route)
   650              [ SideBar.view session
   651                  (model.job
   652                      |> Maybe.map
   653                          (\j ->
   654                              { pipelineName = j.pipelineName
   655                              , teamName = j.teamName
   656                              }
   657                          )
   658                  )
   659              , viewBuildPage session model
   660              ]
   661          ]
   662  
   663  
   664  tooltip : Model -> { a | hovered : HoverState.HoverState } -> Maybe Tooltip.Tooltip
   665  tooltip model session =
   666      model.output
   667          |> toMaybe
   668          |> Maybe.andThen .steps
   669          |> Maybe.andThen (\steps -> StepTree.tooltip steps session)
   670  
   671  
   672  breadcrumbs : Model -> Html Message
   673  breadcrumbs model =
   674      case ( model.job, model.page ) of
   675          ( Just jobId, _ ) ->
   676              TopBar.breadcrumbs <|
   677                  Routes.Job
   678                      { id = jobId
   679                      , page = Nothing
   680                      }
   681  
   682          ( _, JobBuildPage buildId ) ->
   683              TopBar.breadcrumbs <|
   684                  Routes.Build
   685                      { id = buildId
   686                      , highlight = model.highlight
   687                      }
   688  
   689          _ ->
   690              Html.text ""
   691  
   692  
   693  viewBuildPage : Session -> Model -> Html Message
   694  viewBuildPage session model =
   695      if model.hasLoadedYet then
   696          Html.div
   697              [ class "with-fixed-header"
   698              , attribute "data-build-name" model.name
   699              , style "flex-grow" "1"
   700              , style "display" "flex"
   701              , style "flex-direction" "column"
   702              , style "overflow" "hidden"
   703              ]
   704              [ Header.view session model
   705              , body session model
   706              ]
   707  
   708      else
   709          LoadingIndicator.view
   710  
   711  
   712  body :
   713      Session
   714      ->
   715          { a
   716              | prep : Maybe Concourse.BuildPrep
   717              , job : Maybe Concourse.JobIdentifier
   718              , status : BuildStatus
   719              , duration : Concourse.BuildDuration
   720              , reapTime : Maybe Time.Posix
   721              , id : Int
   722              , name : String
   723              , output : CurrentOutput
   724              , authorized : Bool
   725              , showHelp : Bool
   726          }
   727      -> Html Message
   728  body session ({ prep, output, authorized, showHelp } as params) =
   729      Html.div
   730          ([ class "scrollable-body build-body"
   731           , id bodyId
   732           , tabindex 0
   733           , onScroll Scrolled
   734           ]
   735              ++ Styles.body
   736          )
   737      <|
   738          if authorized then
   739              [ viewBuildPrep prep
   740              , Html.Lazy.lazy3
   741                  viewBuildOutput
   742                  session.timeZone
   743                  (Build.Output.Output.filterHoverState session.hovered)
   744                  output
   745              , Shortcuts.keyboardHelp showHelp
   746              ]
   747                  ++ tombstone session.timeZone params
   748  
   749          else
   750              [ NotAuthorized.view ]
   751  
   752  
   753  tombstone :
   754      Time.Zone
   755      ->
   756          { a
   757              | job : Maybe Concourse.JobIdentifier
   758              , status : BuildStatus
   759              , duration : Concourse.BuildDuration
   760              , reapTime : Maybe Time.Posix
   761              , id : Int
   762              , name : String
   763          }
   764      -> List (Html Message)
   765  tombstone timeZone model =
   766      let
   767          maybeBirthDate =
   768              Maybe.Extra.or model.duration.startedAt model.duration.finishedAt
   769      in
   770      case ( maybeBirthDate, model.reapTime ) of
   771          ( Just birthDate, Just reapTime ) ->
   772              [ Html.div
   773                  [ class "tombstone" ]
   774                  [ Html.div [ class "heading" ] [ Html.text "RIP" ]
   775                  , Html.div
   776                      [ class "job-name" ]
   777                      [ model.job
   778                          |> Maybe.map .jobName
   779                          |> Maybe.withDefault "one-off build"
   780                          |> Html.text
   781                      ]
   782                  , Html.div
   783                      [ class "build-name" ]
   784                      [ Html.text <| "build #" ++ model.name ]
   785                  , Html.div
   786                      [ class "date" ]
   787                      [ Html.text <|
   788                          mmDDYY timeZone birthDate
   789                              ++ "-"
   790                              ++ mmDDYY timeZone reapTime
   791                      ]
   792                  , Html.div
   793                      [ class "epitaph" ]
   794                      [ Html.text <|
   795                          case model.status of
   796                              BuildStatusSucceeded ->
   797                                  "It passed, and now it has passed on."
   798  
   799                              BuildStatusFailed ->
   800                                  "It failed, and now has been forgotten."
   801  
   802                              BuildStatusErrored ->
   803                                  "It errored, but has found forgiveness."
   804  
   805                              BuildStatusAborted ->
   806                                  "It was never given a chance."
   807  
   808                              _ ->
   809                                  "I'm not dead yet."
   810                      ]
   811                  ]
   812              , Html.div
   813                  [ class "explanation" ]
   814                  [ Html.text "This log has been "
   815                  , Html.a
   816                      [ Html.Attributes.href "https://concourse-ci.org/jobs.html#job-build-log-retention" ]
   817                      [ Html.text "reaped." ]
   818                  ]
   819              ]
   820  
   821          _ ->
   822              []
   823  
   824  
   825  mmDDYY : Time.Zone -> Time.Posix -> String
   826  mmDDYY =
   827      DateFormat.format
   828          [ DateFormat.monthFixed
   829          , DateFormat.text "/"
   830          , DateFormat.dayOfMonthFixed
   831          , DateFormat.text "/"
   832          , DateFormat.yearNumberLastTwo
   833          ]
   834  
   835  
   836  viewBuildOutput : Time.Zone -> HoverState.HoverState -> CurrentOutput -> Html Message
   837  viewBuildOutput timeZone hovered output =
   838      case output of
   839          Output o ->
   840              Build.Output.Output.view
   841                  { timeZone = timeZone, hovered = hovered }
   842                  o
   843  
   844          Cancelled ->
   845              Html.div
   846                  Styles.errorLog
   847                  [ Html.text "build cancelled" ]
   848  
   849          Empty ->
   850              Html.div [] []
   851  
   852  
   853  viewBuildPrep : Maybe Concourse.BuildPrep -> Html Message
   854  viewBuildPrep buildPrep =
   855      case buildPrep of
   856          Just prep ->
   857              Html.div [ class "build-step" ]
   858                  [ Html.div
   859                      [ class "header"
   860                      , style "display" "flex"
   861                      , style "align-items" "center"
   862                      ]
   863                      [ Icon.icon
   864                          { sizePx = 14, image = Assets.CogsIcon }
   865                          [ style "margin" "7px"
   866                          , style "margin-right" "2px"
   867                          , style "background-size" "contain"
   868                          ]
   869                      , Html.h3 [] [ Html.text "preparing build" ]
   870                      ]
   871                  , Html.div []
   872                      [ Html.ul
   873                          [ class "prep-status-list"
   874                          , style "font-size" "14px"
   875                          ]
   876                          ([ viewBuildPrepLi "checking pipeline is not paused" prep.pausedPipeline Dict.empty
   877                           , viewBuildPrepLi "checking job is not paused" prep.pausedJob Dict.empty
   878                           ]
   879                              ++ viewBuildPrepInputs prep.inputs
   880                              ++ [ viewBuildPrepLi "waiting for a suitable set of input versions" prep.inputsSatisfied prep.missingInputReasons
   881                                 , viewBuildPrepLi "checking max-in-flight is not reached" prep.maxRunningBuilds Dict.empty
   882                                 ]
   883                          )
   884                      ]
   885                  ]
   886  
   887          Nothing ->
   888              Html.div [] []
   889  
   890  
   891  viewBuildPrepInputs : Dict String Concourse.BuildPrepStatus -> List (Html Message)
   892  viewBuildPrepInputs inputs =
   893      List.map viewBuildPrepInput (Dict.toList inputs)
   894  
   895  
   896  viewBuildPrepInput : ( String, Concourse.BuildPrepStatus ) -> Html Message
   897  viewBuildPrepInput ( name, status ) =
   898      viewBuildPrepLi ("discovering any new versions of " ++ name) status Dict.empty
   899  
   900  
   901  viewBuildPrepDetails : Dict String String -> Html Message
   902  viewBuildPrepDetails details =
   903      Html.ul [ class "details" ]
   904          (List.map viewDetailItem (Dict.toList details))
   905  
   906  
   907  viewDetailItem : ( String, String ) -> Html Message
   908  viewDetailItem ( name, status ) =
   909      Html.li []
   910          [ Html.text (name ++ " - " ++ status) ]
   911  
   912  
   913  viewBuildPrepLi :
   914      String
   915      -> Concourse.BuildPrepStatus
   916      -> Dict String String
   917      -> Html Message
   918  viewBuildPrepLi text status details =
   919      Html.li
   920          [ classList
   921              [ ( "prep-status", True )
   922              , ( "inactive", status == Concourse.BuildPrepStatusUnknown )
   923              ]
   924          ]
   925          [ Html.div
   926              [ style "align-items" "center"
   927              , style "display" "flex"
   928              ]
   929              [ viewBuildPrepStatus status
   930              , Html.span []
   931                  [ Html.text text ]
   932              ]
   933          , viewBuildPrepDetails details
   934          ]
   935  
   936  
   937  viewBuildPrepStatus : Concourse.BuildPrepStatus -> Html Message
   938  viewBuildPrepStatus status =
   939      case status of
   940          Concourse.BuildPrepStatusUnknown ->
   941              Html.div
   942                  [ title "thinking..." ]
   943                  [ Spinner.spinner
   944                      { sizePx = 12
   945                      , margin = "0 8px 0 0"
   946                      }
   947                  ]
   948  
   949          Concourse.BuildPrepStatusBlocking ->
   950              Html.div
   951                  [ title "blocking" ]
   952                  [ Spinner.spinner
   953                      { sizePx = 12
   954                      , margin = "0 8px 0 0"
   955                      }
   956                  ]
   957  
   958          Concourse.BuildPrepStatusNotBlocking ->
   959              Icon.icon
   960                  { sizePx = 12
   961                  , image = Assets.NotBlockingCheckIcon
   962                  }
   963                  [ style "margin-right" "8px"
   964                  , style "background-size" "contain"
   965                  , title "not blocking"
   966                  ]