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

     1  module Job.Job exposing
     2      ( Flags
     3      , Model
     4      , changeToJob
     5      , documentTitle
     6      , getUpdateMessage
     7      , handleCallback
     8      , handleDelivery
     9      , init
    10      , startingPage
    11      , subscriptions
    12      , tooltip
    13      , update
    14      , view
    15      )
    16  
    17  import Application.Models exposing (Session)
    18  import Assets
    19  import Colors
    20  import Concourse
    21  import Concourse.BuildStatus exposing (BuildStatus(..))
    22  import Concourse.Pagination
    23      exposing
    24          ( Page
    25          , Paginated
    26          , chevronContainer
    27          , chevronLeft
    28          , chevronRight
    29          )
    30  import Dict
    31  import EffectTransformer exposing (ET)
    32  import HoverState
    33  import Html exposing (Html)
    34  import Html.Attributes
    35      exposing
    36          ( attribute
    37          , class
    38          , href
    39          , id
    40          , style
    41          )
    42  import Html.Events
    43      exposing
    44          ( onClick
    45          , onMouseEnter
    46          , onMouseLeave
    47          )
    48  import Http
    49  import Job.Styles as Styles
    50  import List.Extra
    51  import Login.Login as Login
    52  import Message.Callback exposing (Callback(..))
    53  import Message.Effects exposing (Effect(..))
    54  import Message.Message exposing (DomID(..), Message(..))
    55  import Message.Subscription exposing (Delivery(..), Interval(..), Subscription(..))
    56  import Message.TopLevelMessage exposing (TopLevelMessage(..))
    57  import RemoteData exposing (WebData)
    58  import Routes
    59  import SideBar.SideBar as SideBar
    60  import StrictEvents exposing (onLeftClick)
    61  import Time
    62  import Tooltip
    63  import UpdateMsg exposing (UpdateMsg)
    64  import Views.BuildDuration as BuildDuration
    65  import Views.DictView as DictView
    66  import Views.Icon as Icon
    67  import Views.LoadingIndicator as LoadingIndicator
    68  import Views.Styles
    69  import Views.TopBar as TopBar
    70  
    71  
    72  type alias Model =
    73      Login.Model
    74          { jobIdentifier : Concourse.JobIdentifier
    75          , job : WebData Concourse.Job
    76          , pausedChanging : Bool
    77          , buildsWithResources : WebData (Paginated BuildWithResources)
    78          , currentPage : Page
    79          , now : Time.Posix
    80          }
    81  
    82  
    83  type alias BuildWithResources =
    84      { build : Concourse.Build
    85      , resources : Maybe Concourse.BuildResources
    86      }
    87  
    88  
    89  pageLimit : Int
    90  pageLimit =
    91      100
    92  
    93  
    94  type alias Flags =
    95      { jobId : Concourse.JobIdentifier
    96      , paging : Maybe Page
    97      }
    98  
    99  
   100  startingPage : Page
   101  startingPage =
   102      { limit = pageLimit
   103      , direction = Concourse.Pagination.ToMostRecent
   104      }
   105  
   106  
   107  init : Flags -> ( Model, List Effect )
   108  init flags =
   109      let
   110          page =
   111              flags.paging |> Maybe.withDefault startingPage
   112  
   113          model =
   114              { jobIdentifier = flags.jobId
   115              , job = RemoteData.NotAsked
   116              , pausedChanging = False
   117              , buildsWithResources = RemoteData.Loading
   118              , now = Time.millisToPosix 0
   119              , currentPage = page
   120              , isUserMenuExpanded = False
   121              }
   122      in
   123      ( model
   124      , [ FetchJob flags.jobId
   125        , FetchJobBuilds flags.jobId page
   126        , GetCurrentTime
   127        , GetCurrentTimeZone
   128        , FetchAllPipelines
   129        ]
   130      )
   131  
   132  
   133  changeToJob : Flags -> ET Model
   134  changeToJob flags ( model, effects ) =
   135      let
   136          page =
   137              flags.paging |> Maybe.withDefault startingPage
   138      in
   139      ( { model
   140          | currentPage = page
   141          , buildsWithResources = RemoteData.Loading
   142        }
   143      , effects ++ [ FetchJobBuilds model.jobIdentifier page ]
   144      )
   145  
   146  
   147  subscriptions : List Subscription
   148  subscriptions =
   149      [ OnClockTick FiveSeconds
   150      , OnClockTick OneSecond
   151      ]
   152  
   153  
   154  getUpdateMessage : Model -> UpdateMsg
   155  getUpdateMessage model =
   156      case model.job of
   157          RemoteData.Failure _ ->
   158              UpdateMsg.NotFound
   159  
   160          _ ->
   161              UpdateMsg.AOK
   162  
   163  
   164  handleCallback : Callback -> ET Model
   165  handleCallback callback ( model, effects ) =
   166      case callback of
   167          BuildTriggered (Ok build) ->
   168              ( model
   169              , case build.job of
   170                  Nothing ->
   171                      effects
   172  
   173                  Just job ->
   174                      effects
   175                          ++ [ NavigateTo <|
   176                                  Routes.toString <|
   177                                      Routes.Build
   178                                          { id =
   179                                              { teamName = job.teamName
   180                                              , pipelineName = job.pipelineName
   181                                              , jobName = job.jobName
   182                                              , buildName = build.name
   183                                              }
   184                                          , highlight = Routes.HighlightNothing
   185                                          }
   186                             ]
   187              )
   188  
   189          JobBuildsFetched (Ok ( requestedPage, builds )) ->
   190              handleJobBuildsFetched requestedPage builds ( model, effects )
   191  
   192          JobFetched (Ok job) ->
   193              ( { model | job = RemoteData.Success job }
   194              , effects
   195              )
   196  
   197          JobFetched (Err err) ->
   198              case err of
   199                  Http.BadStatus { status } ->
   200                      if status.code == 404 then
   201                          ( { model | job = RemoteData.Failure err }, effects )
   202  
   203                      else
   204                          ( model, effects ++ redirectToLoginIfNecessary err )
   205  
   206                  _ ->
   207                      ( model, effects )
   208  
   209          BuildResourcesFetched (Ok ( id, buildResources )) ->
   210              case model.buildsWithResources of
   211                  RemoteData.Success { content, pagination } ->
   212                      ( { model
   213                          | buildsWithResources =
   214                              RemoteData.Success
   215                                  { content =
   216                                      List.Extra.updateIf
   217                                          (\bwr -> bwr.build.id == id)
   218                                          (\bwr -> { bwr | resources = Just buildResources })
   219                                          content
   220                                  , pagination = pagination
   221                                  }
   222                        }
   223                      , effects
   224                      )
   225  
   226                  _ ->
   227                      ( model, effects )
   228  
   229          BuildResourcesFetched (Err _) ->
   230              ( model, effects )
   231  
   232          PausedToggled (Ok ()) ->
   233              ( { model | pausedChanging = False }, effects )
   234  
   235          GotCurrentTime now ->
   236              ( { model | now = now }, effects )
   237  
   238          _ ->
   239              ( model, effects )
   240  
   241  
   242  handleDelivery : Delivery -> ET Model
   243  handleDelivery delivery ( model, effects ) =
   244      case delivery of
   245          ClockTicked OneSecond time ->
   246              ( { model | now = time }, effects )
   247  
   248          ClockTicked FiveSeconds _ ->
   249              ( model
   250              , effects
   251                  ++ [ FetchJobBuilds model.jobIdentifier model.currentPage
   252                     , FetchJob model.jobIdentifier
   253                     , FetchAllPipelines
   254                     ]
   255              )
   256  
   257          _ ->
   258              ( model, effects )
   259  
   260  
   261  update : Message -> ET Model
   262  update action ( model, effects ) =
   263      case action of
   264          Click TriggerBuildButton ->
   265              ( model, effects ++ [ DoTriggerBuild model.jobIdentifier ] )
   266  
   267          Click ToggleJobButton ->
   268              case model.job |> RemoteData.toMaybe of
   269                  Nothing ->
   270                      ( model, effects )
   271  
   272                  Just j ->
   273                      ( { model
   274                          | pausedChanging = True
   275                          , job = RemoteData.Success { j | paused = not j.paused }
   276                        }
   277                      , if j.paused then
   278                          effects ++ [ UnpauseJob model.jobIdentifier ]
   279  
   280                        else
   281                          effects ++ [ PauseJob model.jobIdentifier ]
   282                      )
   283  
   284          _ ->
   285              ( model, effects )
   286  
   287  
   288  redirectToLoginIfNecessary : Http.Error -> List Effect
   289  redirectToLoginIfNecessary err =
   290      case err of
   291          Http.BadStatus { status } ->
   292              if status.code == 401 then
   293                  [ RedirectToLogin ]
   294  
   295              else
   296                  []
   297  
   298          _ ->
   299              []
   300  
   301  
   302  permalink : List Concourse.Build -> Page
   303  permalink builds =
   304      case List.head builds of
   305          Nothing ->
   306              { direction = Concourse.Pagination.ToMostRecent
   307              , limit = pageLimit
   308              }
   309  
   310          Just build ->
   311              { direction = Concourse.Pagination.To build.id
   312              , limit = List.length builds
   313              }
   314  
   315  
   316  paginatedMap : (a -> b) -> Paginated a -> Paginated b
   317  paginatedMap promoter pagA =
   318      { content =
   319          List.map promoter pagA.content
   320      , pagination = pagA.pagination
   321      }
   322  
   323  
   324  setResourcesToOld : Maybe BuildWithResources -> BuildWithResources -> BuildWithResources
   325  setResourcesToOld existingBuildWithResource newBwr =
   326      case existingBuildWithResource of
   327          Nothing ->
   328              newBwr
   329  
   330          Just buildWithResources ->
   331              { newBwr
   332                  | resources = buildWithResources.resources
   333              }
   334  
   335  
   336  existingBuild : Concourse.Build -> BuildWithResources -> Bool
   337  existingBuild build buildWithResources =
   338      build == buildWithResources.build
   339  
   340  
   341  promoteBuild : Model -> Concourse.Build -> BuildWithResources
   342  promoteBuild model build =
   343      let
   344          newBwr =
   345              { build = build
   346              , resources = Nothing
   347              }
   348  
   349          existingBuildWithResource =
   350              case model.buildsWithResources of
   351                  RemoteData.Success bwrs ->
   352                      List.Extra.find (existingBuild build) bwrs.content
   353  
   354                  _ ->
   355                      Nothing
   356      in
   357      setResourcesToOld existingBuildWithResource newBwr
   358  
   359  
   360  setExistingResources : Paginated Concourse.Build -> Model -> Paginated BuildWithResources
   361  setExistingResources paginatedBuilds model =
   362      paginatedMap (promoteBuild model) paginatedBuilds
   363  
   364  
   365  updateResourcesIfNeeded : BuildWithResources -> Maybe Effect
   366  updateResourcesIfNeeded bwr =
   367      case ( bwr.resources, isRunning bwr.build ) of
   368          ( Just _, False ) ->
   369              Nothing
   370  
   371          _ ->
   372              Just <| FetchBuildResources bwr.build.id
   373  
   374  
   375  handleJobBuildsFetched : Page -> Paginated Concourse.Build -> ET Model
   376  handleJobBuildsFetched requestedPage paginatedBuilds ( model, effects ) =
   377      let
   378          newPage =
   379              permalink paginatedBuilds.content
   380  
   381          newBWRs =
   382              setExistingResources paginatedBuilds model
   383      in
   384      if
   385          Concourse.Pagination.isPreviousPage requestedPage
   386              && (List.length paginatedBuilds.content < pageLimit)
   387      then
   388          ( model
   389          , effects
   390              ++ [ FetchJobBuilds model.jobIdentifier startingPage
   391                 , NavigateTo <|
   392                      Routes.toString <|
   393                          Routes.Job
   394                              { id = model.jobIdentifier
   395                              , page = Just startingPage
   396                              }
   397                 ]
   398          )
   399  
   400      else
   401          ( { model
   402              | buildsWithResources = RemoteData.Success newBWRs
   403              , currentPage = newPage
   404            }
   405          , effects ++ List.filterMap updateResourcesIfNeeded newBWRs.content
   406          )
   407  
   408  
   409  isRunning : Concourse.Build -> Bool
   410  isRunning build =
   411      Concourse.BuildStatus.isRunning build.status
   412  
   413  
   414  documentTitle : Model -> String
   415  documentTitle model =
   416      model.jobIdentifier.jobName
   417  
   418  
   419  view : Session -> Model -> Html Message
   420  view session model =
   421      let
   422          route =
   423              Routes.Job
   424                  { id = model.jobIdentifier
   425                  , page = Just model.currentPage
   426                  }
   427      in
   428      Html.div
   429          (id "page-including-top-bar" :: Views.Styles.pageIncludingTopBar)
   430          [ Html.div
   431              (id "top-bar-app" :: Views.Styles.topBar False)
   432              [ SideBar.hamburgerMenu session
   433              , TopBar.concourseLogo
   434              , TopBar.breadcrumbs route
   435              , Login.view session.userState model
   436              ]
   437          , Html.div
   438              (id "page-below-top-bar" :: Views.Styles.pageBelowTopBar route)
   439              [ SideBar.view session
   440                  (Just
   441                      { pipelineName = model.jobIdentifier.pipelineName
   442                      , teamName = model.jobIdentifier.teamName
   443                      }
   444                  )
   445              , viewMainJobsSection session model
   446              ]
   447          ]
   448  
   449  
   450  tooltip : Model -> a -> Maybe Tooltip.Tooltip
   451  tooltip _ _ =
   452      Nothing
   453  
   454  
   455  viewMainJobsSection : Session -> Model -> Html Message
   456  viewMainJobsSection session model =
   457      let
   458          archived =
   459              isPipelineArchived
   460                  session.pipelines
   461                  model.jobIdentifier
   462      in
   463      Html.div
   464          [ class "with-fixed-header"
   465          , style "flex-grow" "1"
   466          , style "display" "flex"
   467          , style "flex-direction" "column"
   468          ]
   469          [ case model.job |> RemoteData.toMaybe of
   470              Nothing ->
   471                  LoadingIndicator.view
   472  
   473              Just job ->
   474                  let
   475                      toggleHovered =
   476                          HoverState.isHovered ToggleJobButton session.hovered
   477  
   478                      triggerHovered =
   479                          HoverState.isHovered TriggerBuildButton session.hovered
   480                  in
   481                  Html.div [ class "fixed-header" ]
   482                      [ Html.div
   483                          [ class "build-header"
   484                          , style "display" "flex"
   485                          , style "justify-content" "space-between"
   486                          , style "background" <|
   487                              Colors.buildStatusColor True <|
   488                                  headerBuildStatus job.finishedBuild
   489                          ]
   490                          [ Html.div
   491                              [ style "display" "flex" ]
   492                              [ if archived then
   493                                  Html.text ""
   494  
   495                                else
   496                                  Html.button
   497                                      ([ id "pause-toggle"
   498                                       , onMouseEnter <| Hover <| Just ToggleJobButton
   499                                       , onMouseLeave <| Hover Nothing
   500                                       , onClick <| Click ToggleJobButton
   501                                       ]
   502                                          ++ (Styles.triggerButton False toggleHovered <|
   503                                                  headerBuildStatus job.finishedBuild
   504                                             )
   505                                      )
   506                                      [ Icon.icon
   507                                          { sizePx = 40
   508                                          , image =
   509                                              Assets.CircleOutlineIcon <|
   510                                                  if job.paused then
   511                                                      Assets.PlayCircleIcon
   512  
   513                                                  else
   514                                                      Assets.PauseCircleIcon
   515                                          }
   516                                          (Styles.icon toggleHovered)
   517                                      ]
   518                              , Html.h1 []
   519                                  [ Html.span
   520                                      [ class "build-name" ]
   521                                      [ Html.text job.name ]
   522                                  ]
   523                              ]
   524                          , if archived then
   525                              Html.text ""
   526  
   527                            else
   528                              Html.button
   529                                  ([ class "trigger-build"
   530                                   , onLeftClick <| Click TriggerBuildButton
   531                                   , attribute "aria-label" "Trigger Build"
   532                                   , attribute "title" "Trigger Build"
   533                                   , onMouseEnter <| Hover <| Just TriggerBuildButton
   534                                   , onMouseLeave <| Hover Nothing
   535                                   ]
   536                                      ++ (Styles.triggerButton job.disableManualTrigger triggerHovered <|
   537                                              headerBuildStatus job.finishedBuild
   538                                         )
   539                                  )
   540                              <|
   541                                  [ Icon.icon
   542                                      { sizePx = 40
   543                                      , image = Assets.AddCircleIcon |> Assets.CircleOutlineIcon
   544                                      }
   545                                      (Styles.icon <|
   546                                          triggerHovered
   547                                              && not job.disableManualTrigger
   548                                      )
   549                                  ]
   550                                      ++ (if job.disableManualTrigger && triggerHovered then
   551                                              [ Html.div
   552                                                  Styles.triggerTooltip
   553                                                  [ Html.text <|
   554                                                      "manual triggering disabled "
   555                                                          ++ "in job config"
   556                                                  ]
   557                                              ]
   558  
   559                                          else
   560                                              []
   561                                         )
   562                          ]
   563                      , Html.div
   564                          [ id "pagination-header"
   565                          , style "display" "flex"
   566                          , style "justify-content" "space-between"
   567                          , style "align-items" "stretch"
   568                          , style "height" "60px"
   569                          , style "background-color" Colors.secondaryTopBar
   570                          ]
   571                          [ Html.h1
   572                              [ style "margin" "0 18px" ]
   573                              [ Html.text "builds" ]
   574                          , viewPaginationBar session model
   575                          ]
   576                      ]
   577          , case model.buildsWithResources of
   578              RemoteData.Success { content } ->
   579                  if List.isEmpty content then
   580                      Html.div Styles.noBuildsMessage
   581                          [ Html.text <|
   582                              "no builds for job “"
   583                                  ++ model.jobIdentifier.jobName
   584                                  ++ "”"
   585                          ]
   586  
   587                  else
   588                      Html.div
   589                          [ class "scrollable-body job-body"
   590                          , style "overflow-y" "auto"
   591                          ]
   592                          [ Html.ul [ class "jobs-builds-list builds-list" ] <|
   593                              List.map (viewBuildWithResources session model) content
   594                          ]
   595  
   596              _ ->
   597                  LoadingIndicator.view
   598          ]
   599  
   600  
   601  isPipelineArchived :
   602      WebData (List Concourse.Pipeline)
   603      -> Concourse.JobIdentifier
   604      -> Bool
   605  isPipelineArchived pipelines { pipelineName, teamName } =
   606      pipelines
   607          |> RemoteData.withDefault []
   608          |> List.Extra.find (\p -> p.name == pipelineName && p.teamName == teamName)
   609          |> Maybe.map .archived
   610          |> Maybe.withDefault False
   611  
   612  
   613  headerBuildStatus : Maybe Concourse.Build -> BuildStatus
   614  headerBuildStatus finishedBuild =
   615      case finishedBuild of
   616          Nothing ->
   617              BuildStatusPending
   618  
   619          Just build ->
   620              build.status
   621  
   622  
   623  viewPaginationBar : { a | hovered : HoverState.HoverState } -> Model -> Html Message
   624  viewPaginationBar session model =
   625      Html.div
   626          [ id "pagination"
   627          , style "display" "flex"
   628          , style "align-items" "stretch"
   629          ]
   630          (case model.buildsWithResources of
   631              RemoteData.Success { pagination } ->
   632                  [ case pagination.previousPage of
   633                      Nothing ->
   634                          Html.div
   635                              chevronContainer
   636                              [ Html.div
   637                                  (chevronLeft
   638                                      { enabled = False
   639                                      , hovered = False
   640                                      }
   641                                  )
   642                                  []
   643                              ]
   644  
   645                      Just page ->
   646                          let
   647                              jobRoute =
   648                                  Routes.Job { id = model.jobIdentifier, page = Just page }
   649                          in
   650                          Html.div
   651                              ([ onMouseEnter <| Hover <| Just PreviousPageButton
   652                               , onMouseLeave <| Hover Nothing
   653                               ]
   654                                  ++ chevronContainer
   655                              )
   656                              [ Html.a
   657                                  ([ StrictEvents.onLeftClick <| GoToRoute jobRoute
   658                                   , href <| Routes.toString <| jobRoute
   659                                   , attribute "aria-label" "Previous Page"
   660                                   ]
   661                                      ++ chevronLeft
   662                                          { enabled = True
   663                                          , hovered =
   664                                              HoverState.isHovered
   665                                                  PreviousPageButton
   666                                                  session.hovered
   667                                          }
   668                                  )
   669                                  []
   670                              ]
   671                  , case pagination.nextPage of
   672                      Nothing ->
   673                          Html.div
   674                              chevronContainer
   675                              [ Html.div
   676                                  (chevronRight
   677                                      { enabled = False
   678                                      , hovered = False
   679                                      }
   680                                  )
   681                                  []
   682                              ]
   683  
   684                      Just page ->
   685                          let
   686                              jobRoute =
   687                                  Routes.Job { id = model.jobIdentifier, page = Just page }
   688                          in
   689                          Html.div
   690                              ([ onMouseEnter <| Hover <| Just NextPageButton
   691                               , onMouseLeave <| Hover Nothing
   692                               ]
   693                                  ++ chevronContainer
   694                              )
   695                              [ Html.a
   696                                  ([ StrictEvents.onLeftClick <| GoToRoute jobRoute
   697                                   , href <| Routes.toString jobRoute
   698                                   , attribute "aria-label" "Next Page"
   699                                   ]
   700                                      ++ chevronRight
   701                                          { enabled = True
   702                                          , hovered =
   703                                              HoverState.isHovered
   704                                                  NextPageButton
   705                                                  session.hovered
   706                                          }
   707                                  )
   708                                  []
   709                              ]
   710                  ]
   711  
   712              _ ->
   713                  [ Html.div
   714                      chevronContainer
   715                      [ Html.div
   716                          (chevronLeft
   717                              { enabled = False
   718                              , hovered = False
   719                              }
   720                          )
   721                          []
   722                      ]
   723                  , Html.div
   724                      chevronContainer
   725                      [ Html.div
   726                          (chevronRight
   727                              { enabled = False
   728                              , hovered = False
   729                              }
   730                          )
   731                          []
   732                      ]
   733                  ]
   734          )
   735  
   736  
   737  viewBuildWithResources :
   738      Session
   739      -> Model
   740      -> BuildWithResources
   741      -> Html Message
   742  viewBuildWithResources session model bwr =
   743      Html.li [ class "js-build" ] <|
   744          let
   745              buildResourcesView =
   746                  viewBuildResources bwr
   747          in
   748          [ viewBuildHeader bwr.build
   749          , Html.div [ class "pam clearfix" ] <|
   750              BuildDuration.view session.timeZone bwr.build.duration model.now
   751                  :: buildResourcesView
   752          ]
   753  
   754  
   755  viewBuildHeader : Concourse.Build -> Html Message
   756  viewBuildHeader b =
   757      Html.a
   758          [ class <| Concourse.BuildStatus.show b.status
   759          , StrictEvents.onLeftClick <|
   760              GoToRoute <|
   761                  Routes.buildRoute b.id b.name b.job
   762          , href <|
   763              Routes.toString <|
   764                  Routes.buildRoute b.id b.name b.job
   765          ]
   766          [ Html.text ("#" ++ b.name)
   767          ]
   768  
   769  
   770  viewBuildResources : BuildWithResources -> List (Html Message)
   771  viewBuildResources buildWithResources =
   772      let
   773          inputsTable =
   774              case buildWithResources.resources of
   775                  Nothing ->
   776                      LoadingIndicator.view
   777  
   778                  Just resources ->
   779                      Html.table [ class "build-resources" ] <|
   780                          List.map viewBuildInputs resources.inputs
   781  
   782          outputsTable =
   783              case buildWithResources.resources of
   784                  Nothing ->
   785                      LoadingIndicator.view
   786  
   787                  Just resources ->
   788                      Html.table [ class "build-resources" ] <|
   789                          List.map viewBuildOutputs resources.outputs
   790      in
   791      [ Html.div [ class "inputs mrl" ]
   792          [ Html.div
   793              Styles.buildResourceHeader
   794              [ Icon.icon
   795                  { sizePx = 12
   796                  , image = Assets.DownArrow
   797                  }
   798                  Styles.buildResourceIcon
   799              , Html.text "inputs"
   800              ]
   801          , inputsTable
   802          ]
   803      , Html.div [ class "outputs mrl" ]
   804          [ Html.div
   805              Styles.buildResourceHeader
   806              [ Icon.icon
   807                  { sizePx = 12
   808                  , image = Assets.UpArrow
   809                  }
   810                  Styles.buildResourceIcon
   811              , Html.text "outputs"
   812              ]
   813          , outputsTable
   814          ]
   815      ]
   816  
   817  
   818  viewBuildInputs : Concourse.BuildResourcesInput -> Html Message
   819  viewBuildInputs bi =
   820      Html.tr [ class "mbs pas resource fl clearfix" ]
   821          [ Html.td [ class "resource-name mrm" ]
   822              [ Html.text bi.name
   823              ]
   824          , Html.td [ class "resource-version" ]
   825              [ viewVersion bi.version
   826              ]
   827          ]
   828  
   829  
   830  viewBuildOutputs : Concourse.BuildResourcesOutput -> Html Message
   831  viewBuildOutputs bo =
   832      Html.tr [ class "mbs pas resource fl clearfix" ]
   833          [ Html.td [ class "resource-name mrm" ]
   834              [ Html.text bo.name
   835              ]
   836          , Html.td [ class "resource-version" ]
   837              [ viewVersion bo.version
   838              ]
   839          ]
   840  
   841  
   842  viewVersion : Concourse.Version -> Html Message
   843  viewVersion version =
   844      version
   845          |> Dict.map (always Html.text)
   846          |> DictView.view []