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

     1  module Benchmarks exposing (main)
     2  
     3  import Ansi.Log
     4  import Application.Models exposing (Session)
     5  import Array
     6  import Assets
     7  import Benchmark
     8  import Benchmark.Runner exposing (BenchmarkProgram, program)
     9  import Build.Build as Build
    10  import Build.Header.Models exposing (BuildPageType(..), CurrentOutput(..))
    11  import Build.Models
    12  import Build.Output.Models
    13  import Build.Output.Output
    14  import Build.StepTree.Models as STModels
    15  import Build.Styles
    16  import Concourse
    17  import Concourse.BuildStatus
    18  import Concourse.Pagination exposing (Page)
    19  import Dashboard.DashboardPreview as DP
    20  import DateFormat
    21  import Dict exposing (Dict)
    22  import HoverState
    23  import Html exposing (Html)
    24  import Html.Attributes
    25      exposing
    26          ( attribute
    27          , class
    28          , classList
    29          , href
    30          , id
    31          , style
    32          , tabindex
    33          , title
    34          )
    35  import Html.Events exposing (onBlur, onFocus, onMouseEnter, onMouseLeave)
    36  import Html.Lazy
    37  import Keyboard
    38  import Login.Login as Login
    39  import Maybe.Extra
    40  import Message.Message exposing (DomID(..), Message(..), PipelinesSection(..))
    41  import RemoteData exposing (WebData)
    42  import Routes exposing (Highlight)
    43  import ScreenSize
    44  import Set
    45  import SideBar.SideBar as SideBar
    46  import StrictEvents exposing (onLeftClick, onScroll, onWheel)
    47  import Time
    48  import UserState
    49  import Views.BuildDuration as BuildDuration
    50  import Views.Icon as Icon
    51  import Views.LoadingIndicator as LoadingIndicator
    52  import Views.NotAuthorized as NotAuthorized
    53  import Views.Spinner as Spinner
    54  import Views.Styles
    55  import Views.TopBar as TopBar
    56  
    57  
    58  type alias Model =
    59      Login.Model
    60          { page : BuildPageType
    61          , now : Maybe Time.Posix
    62          , disableManualTrigger : Bool
    63          , history : List Concourse.Build
    64          , nextPage : Maybe Page
    65          , currentBuild : WebData CurrentBuild
    66          , autoScroll : Bool
    67          , previousKeyPress : Maybe Keyboard.KeyEvent
    68          , shiftDown : Bool
    69          , isTriggerBuildKeyDown : Bool
    70          , showHelp : Bool
    71          , highlight : Highlight
    72          , hoveredCounter : Int
    73          , fetchingHistory : Bool
    74          , scrolledToCurrentBuild : Bool
    75          , authorized : Bool
    76          }
    77  
    78  
    79  type alias CurrentBuild =
    80      { build : Concourse.Build
    81      , prep : Maybe Concourse.BuildPrep
    82      , output : CurrentOutput
    83      }
    84  
    85  
    86  main : BenchmarkProgram
    87  main =
    88      program <|
    89          Benchmark.describe "benchmark suite"
    90              [ Benchmark.compare "DashboardPreview.view"
    91                  "current"
    92                  (\_ -> DP.view AllPipelinesSection HoverState.NoHover (DP.groupByRank sampleJobs))
    93                  "old"
    94                  (\_ -> dashboardPreviewView sampleJobs)
    95              , Benchmark.compare "Build.view"
    96                  "current"
    97                  (\_ -> Build.view sampleSession sampleModel)
    98                  "old"
    99                  (\_ -> buildView sampleSession sampleOldModel)
   100              ]
   101  
   102  
   103  bodyId : String
   104  bodyId =
   105      "build-body"
   106  
   107  
   108  historyId : String
   109  historyId =
   110      "builds"
   111  
   112  
   113  buildView : Session -> Model -> Html Message
   114  buildView session model =
   115      let
   116          route =
   117              case model.page of
   118                  OneOffBuildPage buildId ->
   119                      Routes.OneOffBuild
   120                          { id = buildId
   121                          , highlight = model.highlight
   122                          }
   123  
   124                  JobBuildPage buildId ->
   125                      Routes.Build
   126                          { id = buildId
   127                          , highlight = model.highlight
   128                          }
   129      in
   130      Html.div
   131          (id "page-including-top-bar" :: Views.Styles.pageIncludingTopBar)
   132          [ Html.div
   133              (id "top-bar-app" :: Views.Styles.topBar False)
   134              [ SideBar.hamburgerMenu session
   135              , TopBar.concourseLogo
   136              , breadcrumbs model
   137              , Login.view session.userState model
   138              ]
   139          , Html.div
   140              (id "page-below-top-bar" :: Views.Styles.pageBelowTopBar route)
   141              [ SideBar.view session
   142                  (currentJob model
   143                      |> Maybe.map
   144                          (\j ->
   145                              { pipelineName = j.pipelineName
   146                              , teamName = j.teamName
   147                              }
   148                          )
   149                  )
   150              , viewBuildPage session model
   151              ]
   152          ]
   153  
   154  
   155  viewBuildPage : Session -> Model -> Html Message
   156  viewBuildPage session model =
   157      case model.currentBuild |> RemoteData.toMaybe of
   158          Just currentBuild ->
   159              Html.div
   160                  [ class "with-fixed-header"
   161                  , attribute "data-build-name" currentBuild.build.name
   162                  , style "flex-grow" "1"
   163                  , style "display" "flex"
   164                  , style "flex-direction" "column"
   165                  , style "overflow" "hidden"
   166                  ]
   167                  [ viewBuildHeader session model currentBuild.build
   168                  , body
   169                      session
   170                      { currentBuild = currentBuild
   171                      , authorized = model.authorized
   172                      , showHelp = model.showHelp
   173                      }
   174                  ]
   175  
   176          _ ->
   177              LoadingIndicator.view
   178  
   179  
   180  currentJob : Model -> Maybe Concourse.JobIdentifier
   181  currentJob =
   182      .currentBuild
   183          >> RemoteData.toMaybe
   184          >> Maybe.map .build
   185          >> Maybe.andThen .job
   186  
   187  
   188  breadcrumbs : Model -> Html Message
   189  breadcrumbs model =
   190      case ( currentJob model, model.page ) of
   191          ( Just jobId, _ ) ->
   192              TopBar.breadcrumbs <|
   193                  Routes.Job
   194                      { id = jobId
   195                      , page = Nothing
   196                      }
   197  
   198          ( _, JobBuildPage buildId ) ->
   199              TopBar.breadcrumbs <|
   200                  Routes.Build
   201                      { id = buildId
   202                      , highlight = model.highlight
   203                      }
   204  
   205          _ ->
   206              Html.text ""
   207  
   208  
   209  body :
   210      Session
   211      ->
   212          { currentBuild : CurrentBuild
   213          , authorized : Bool
   214          , showHelp : Bool
   215          }
   216      -> Html Message
   217  body session { currentBuild, authorized, showHelp } =
   218      Html.div
   219          ([ class "scrollable-body build-body"
   220           , id bodyId
   221           , tabindex 0
   222           , onScroll Scrolled
   223           ]
   224              ++ Build.Styles.body
   225          )
   226      <|
   227          if authorized then
   228              [ viewBuildPrep currentBuild.prep
   229              , Html.Lazy.lazy2 viewBuildOutput session currentBuild.output
   230              , keyboardHelp showHelp
   231              ]
   232                  ++ tombstone session.timeZone currentBuild
   233  
   234          else
   235              [ NotAuthorized.view ]
   236  
   237  
   238  viewBuildHeader :
   239      Session
   240      -> Model
   241      -> Concourse.Build
   242      -> Html Message
   243  viewBuildHeader session model build =
   244      let
   245          triggerButton =
   246              case currentJob model of
   247                  Just _ ->
   248                      let
   249                          buttonDisabled =
   250                              model.disableManualTrigger
   251  
   252                          buttonHovered =
   253                              HoverState.isHovered
   254                                  TriggerBuildButton
   255                                  session.hovered
   256                      in
   257                      Html.button
   258                          ([ attribute "role" "button"
   259                           , attribute "tabindex" "0"
   260                           , attribute "aria-label" "Trigger Build"
   261                           , attribute "title" "Trigger Build"
   262                           , onLeftClick <| Click TriggerBuildButton
   263                           , onMouseEnter <| Hover <| Just TriggerBuildButton
   264                           , onFocus <| Hover <| Just TriggerBuildButton
   265                           , onMouseLeave <| Hover Nothing
   266                           , onBlur <| Hover Nothing
   267                           ]
   268                              ++ Build.Styles.triggerButton
   269                                  buttonDisabled
   270                                  buttonHovered
   271                                  build.status
   272                          )
   273                      <|
   274                          [ Icon.icon
   275                              { sizePx = 40
   276                              , image = Assets.AddCircleIcon |> Assets.CircleOutlineIcon
   277                              }
   278                              []
   279                          ]
   280                              ++ (if buttonDisabled && buttonHovered then
   281                                      [ Html.div
   282                                          (Build.Styles.buttonTooltip 240)
   283                                          [ Html.text <|
   284                                              "manual triggering disabled "
   285                                                  ++ "in job config"
   286                                          ]
   287                                      ]
   288  
   289                                  else
   290                                      []
   291                                 )
   292  
   293                  Nothing ->
   294                      Html.text ""
   295  
   296          abortHovered =
   297              HoverState.isHovered AbortBuildButton session.hovered
   298  
   299          abortButton =
   300              if Concourse.BuildStatus.isRunning build.status then
   301                  Html.button
   302                      ([ onLeftClick (Click <| AbortBuildButton)
   303                       , attribute "role" "button"
   304                       , attribute "tabindex" "0"
   305                       , attribute "aria-label" "Abort Build"
   306                       , attribute "title" "Abort Build"
   307                       , onMouseEnter <| Hover <| Just AbortBuildButton
   308                       , onFocus <| Hover <| Just AbortBuildButton
   309                       , onMouseLeave <| Hover Nothing
   310                       , onBlur <| Hover Nothing
   311                       ]
   312                          ++ Build.Styles.abortButton abortHovered
   313                      )
   314                      [ Icon.icon
   315                          { sizePx = 40
   316                          , image = Assets.AbortCircleIcon |> Assets.CircleOutlineIcon
   317                          }
   318                          []
   319                      ]
   320  
   321              else
   322                  Html.text ""
   323  
   324          buildTitle =
   325              case build.job of
   326                  Just jobId ->
   327                      let
   328                          jobRoute =
   329                              Routes.Job { id = jobId, page = Nothing }
   330                      in
   331                      Html.a
   332                          [ href <| Routes.toString jobRoute ]
   333                          [ Html.span [ class "build-name" ] [ Html.text jobId.jobName ]
   334                          , Html.text (" #" ++ build.name)
   335                          ]
   336  
   337                  _ ->
   338                      Html.text ("build #" ++ String.fromInt build.id)
   339      in
   340      Html.div [ class "fixed-header" ]
   341          [ Html.div
   342              ([ id "build-header"
   343               , class "build-header"
   344               ]
   345                  ++ Build.Styles.header build.status
   346              )
   347              [ Html.div []
   348                  [ Html.h1 [] [ buildTitle ]
   349                  , case model.now of
   350                      Just n ->
   351                          BuildDuration.view session.timeZone build.duration n
   352  
   353                      Nothing ->
   354                          Html.text ""
   355                  ]
   356              , Html.div
   357                  [ style "display" "flex" ]
   358                  [ abortButton, triggerButton ]
   359              ]
   360          , Html.div
   361              [ onWheel ScrollBuilds ]
   362              [ lazyViewHistory build model.history ]
   363          ]
   364  
   365  
   366  tombstone : Time.Zone -> CurrentBuild -> List (Html Message)
   367  tombstone timeZone currentBuild =
   368      let
   369          build =
   370              currentBuild.build
   371  
   372          maybeBirthDate =
   373              Maybe.Extra.or build.duration.startedAt build.duration.finishedAt
   374      in
   375      case ( maybeBirthDate, build.reapTime ) of
   376          ( Just birthDate, Just reapTime ) ->
   377              [ Html.div
   378                  [ class "tombstone" ]
   379                  [ Html.div [ class "heading" ] [ Html.text "RIP" ]
   380                  , Html.div
   381                      [ class "job-name" ]
   382                      [ Html.text <|
   383                          Maybe.withDefault
   384                              "one-off build"
   385                          <|
   386                              Maybe.map .jobName build.job
   387                      ]
   388                  , Html.div
   389                      [ class "build-name" ]
   390                      [ Html.text <|
   391                          "build #"
   392                              ++ (case build.job of
   393                                      Nothing ->
   394                                          String.fromInt build.id
   395  
   396                                      Just _ ->
   397                                          build.name
   398                                 )
   399                      ]
   400                  , Html.div
   401                      [ class "date" ]
   402                      [ Html.text <|
   403                          mmDDYY timeZone birthDate
   404                              ++ "-"
   405                              ++ mmDDYY timeZone reapTime
   406                      ]
   407                  , Html.div
   408                      [ class "epitaph" ]
   409                      [ Html.text <|
   410                          case build.status of
   411                              Concourse.BuildStatus.BuildStatusSucceeded ->
   412                                  "It passed, and now it has passed on."
   413  
   414                              Concourse.BuildStatus.BuildStatusFailed ->
   415                                  "It failed, and now has been forgotten."
   416  
   417                              Concourse.BuildStatus.BuildStatusErrored ->
   418                                  "It errored, but has found forgiveness."
   419  
   420                              Concourse.BuildStatus.BuildStatusAborted ->
   421                                  "It was never given a chance."
   422  
   423                              _ ->
   424                                  "I'm not dead yet."
   425                      ]
   426                  ]
   427              , Html.div
   428                  [ class "explanation" ]
   429                  [ Html.text "This log has been "
   430                  , Html.a
   431                      [ Html.Attributes.href "https://concourse-ci.org/jobs.html#job-build-log-retention" ]
   432                      [ Html.text "reaped." ]
   433                  ]
   434              ]
   435  
   436          _ ->
   437              []
   438  
   439  
   440  keyboardHelp : Bool -> Html Message
   441  keyboardHelp showHelp =
   442      Html.div
   443          [ classList
   444              [ ( "keyboard-help", True )
   445              , ( "hidden", not showHelp )
   446              ]
   447          ]
   448          [ Html.div
   449              [ class "help-title" ]
   450              [ Html.text "keyboard shortcuts" ]
   451          , Html.div
   452              [ class "help-line" ]
   453              [ Html.div
   454                  [ class "keys" ]
   455                  [ Html.span [ class "key" ] [ Html.text "h" ]
   456                  , Html.span [ class "key" ] [ Html.text "l" ]
   457                  ]
   458              , Html.text "previous/next build"
   459              ]
   460          , Html.div
   461              [ class "help-line" ]
   462              [ Html.div
   463                  [ class "keys" ]
   464                  [ Html.span [ class "key" ] [ Html.text "j" ]
   465                  , Html.span [ class "key" ] [ Html.text "k" ]
   466                  ]
   467              , Html.text "scroll down/up"
   468              ]
   469          , Html.div
   470              [ class "help-line" ]
   471              [ Html.div
   472                  [ class "keys" ]
   473                  [ Html.span [ class "key" ] [ Html.text "T" ] ]
   474              , Html.text "trigger a new build"
   475              ]
   476          , Html.div
   477              [ class "help-line" ]
   478              [ Html.div
   479                  [ class "keys" ]
   480                  [ Html.span [ class "key" ] [ Html.text "A" ] ]
   481              , Html.text "abort build"
   482              ]
   483          , Html.div
   484              [ class "help-line" ]
   485              [ Html.div
   486                  [ class "keys" ]
   487                  [ Html.span [ class "key" ] [ Html.text "gg" ] ]
   488              , Html.text "scroll to the top"
   489              ]
   490          , Html.div
   491              [ class "help-line" ]
   492              [ Html.div
   493                  [ class "keys" ]
   494                  [ Html.span [ class "key" ] [ Html.text "G" ] ]
   495              , Html.text "scroll to the bottom"
   496              ]
   497          , Html.div
   498              [ class "help-line" ]
   499              [ Html.div
   500                  [ class "keys" ]
   501                  [ Html.span [ class "key" ] [ Html.text "?" ] ]
   502              , Html.text "hide/show help"
   503              ]
   504          ]
   505  
   506  
   507  viewBuildOutput : Session -> CurrentOutput -> Html Message
   508  viewBuildOutput session output =
   509      case output of
   510          Output o ->
   511              Build.Output.Output.view
   512                  { timeZone = session.timeZone, hovered = session.hovered }
   513                  o
   514  
   515          Cancelled ->
   516              Html.div
   517                  Build.Styles.errorLog
   518                  [ Html.text "build cancelled" ]
   519  
   520          Empty ->
   521              Html.div [] []
   522  
   523  
   524  viewBuildPrep : Maybe Concourse.BuildPrep -> Html Message
   525  viewBuildPrep buildPrep =
   526      case buildPrep of
   527          Just prep ->
   528              Html.div [ class "build-step" ]
   529                  [ Html.div
   530                      [ class "header"
   531                      , style "display" "flex"
   532                      , style "align-items" "center"
   533                      ]
   534                      [ Icon.icon
   535                          { sizePx = 15, image = Assets.CogsIcon }
   536                          [ style "margin" "6.5px"
   537                          , style "margin-right" "0.5px"
   538                          , style "background-size" "contain"
   539                          ]
   540                      , Html.h3 [] [ Html.text "preparing build" ]
   541                      ]
   542                  , Html.div []
   543                      [ Html.ul [ class "prep-status-list" ]
   544                          ([ viewBuildPrepLi "checking pipeline is not paused" prep.pausedPipeline Dict.empty
   545                           , viewBuildPrepLi "checking job is not paused" prep.pausedJob Dict.empty
   546                           ]
   547                              ++ viewBuildPrepInputs prep.inputs
   548                              ++ [ viewBuildPrepLi "waiting for a suitable set of input versions" prep.inputsSatisfied prep.missingInputReasons
   549                                 , viewBuildPrepLi "checking max-in-flight is not reached" prep.maxRunningBuilds Dict.empty
   550                                 ]
   551                          )
   552                      ]
   553                  ]
   554  
   555          Nothing ->
   556              Html.div [] []
   557  
   558  
   559  lazyViewHistory : Concourse.Build -> List Concourse.Build -> Html Message
   560  lazyViewHistory currentBuild builds =
   561      Html.Lazy.lazy2 viewHistory currentBuild builds
   562  
   563  
   564  viewHistory : Concourse.Build -> List Concourse.Build -> Html Message
   565  viewHistory currentBuild builds =
   566      Html.ul [ id historyId ]
   567          (List.map (viewHistoryItem currentBuild) builds)
   568  
   569  
   570  viewHistoryItem : Concourse.Build -> Concourse.Build -> Html Message
   571  viewHistoryItem currentBuild build =
   572      Html.li
   573          ([ classList [ ( "current", build.id == currentBuild.id ) ]
   574           , id <| String.fromInt build.id
   575           ]
   576              ++ Build.Styles.historyItem
   577                  currentBuild.status
   578                  (build.id == currentBuild.id)
   579                  build.status
   580          )
   581          [ Html.a
   582              [ onLeftClick <| Click <| BuildTab build.id build.name
   583              , href <| Routes.toString <| Routes.buildRoute build.id build.name build.job
   584              ]
   585              [ Html.text build.name ]
   586          ]
   587  
   588  
   589  mmDDYY : Time.Zone -> Time.Posix -> String
   590  mmDDYY =
   591      DateFormat.format
   592          [ DateFormat.monthFixed
   593          , DateFormat.text "/"
   594          , DateFormat.dayOfMonthFixed
   595          , DateFormat.text "/"
   596          , DateFormat.yearNumberLastTwo
   597          ]
   598  
   599  
   600  viewBuildPrepLi :
   601      String
   602      -> Concourse.BuildPrepStatus
   603      -> Dict String String
   604      -> Html Message
   605  viewBuildPrepLi text status details =
   606      Html.li
   607          [ classList
   608              [ ( "prep-status", True )
   609              , ( "inactive", status == Concourse.BuildPrepStatusUnknown )
   610              ]
   611          ]
   612          [ Html.div
   613              [ style "align-items" "center"
   614              , style "display" "flex"
   615              ]
   616              [ viewBuildPrepStatus status
   617              , Html.span []
   618                  [ Html.text text ]
   619              ]
   620          , viewBuildPrepDetails details
   621          ]
   622  
   623  
   624  viewBuildPrepInputs : Dict String Concourse.BuildPrepStatus -> List (Html Message)
   625  viewBuildPrepInputs inputs =
   626      List.map viewBuildPrepInput (Dict.toList inputs)
   627  
   628  
   629  viewBuildPrepInput : ( String, Concourse.BuildPrepStatus ) -> Html Message
   630  viewBuildPrepInput ( name, status ) =
   631      viewBuildPrepLi ("discovering any new versions of " ++ name) status Dict.empty
   632  
   633  
   634  viewBuildPrepDetails : Dict String String -> Html Message
   635  viewBuildPrepDetails details =
   636      Html.ul [ class "details" ]
   637          (List.map viewDetailItem (Dict.toList details))
   638  
   639  
   640  viewBuildPrepStatus : Concourse.BuildPrepStatus -> Html Message
   641  viewBuildPrepStatus status =
   642      case status of
   643          Concourse.BuildPrepStatusUnknown ->
   644              Html.div
   645                  [ title "thinking..." ]
   646                  [ Spinner.spinner
   647                      { sizePx = 12
   648                      , margin = "0 5px 0 0"
   649                      }
   650                  ]
   651  
   652          Concourse.BuildPrepStatusBlocking ->
   653              Html.div
   654                  [ title "blocking" ]
   655                  [ Spinner.spinner
   656                      { sizePx = 12
   657                      , margin = "0 5px 0 0"
   658                      }
   659                  ]
   660  
   661          Concourse.BuildPrepStatusNotBlocking ->
   662              Icon.icon
   663                  { sizePx = 12
   664                  , image = Assets.NotBlockingCheckIcon
   665                  }
   666                  [ style "margin-right" "5px"
   667                  , style "background-size" "contain"
   668                  , title "not blocking"
   669                  ]
   670  
   671  
   672  viewDetailItem : ( String, String ) -> Html Message
   673  viewDetailItem ( name, status ) =
   674      Html.li []
   675          [ Html.text (name ++ " - " ++ status) ]
   676  
   677  
   678  sampleSession : Session
   679  sampleSession =
   680      { authToken = ""
   681      , clusterName = ""
   682      , csrfToken = ""
   683      , expandedTeamsInAllPipelines = Set.empty
   684      , collapsedTeamsInFavorites = Set.empty
   685      , favoritedPipelines = Set.empty
   686      , hovered = HoverState.NoHover
   687      , sideBarState =
   688          { isOpen = False
   689          , width = 275
   690          }
   691      , draggingSideBar = False
   692      , notFoundImgSrc = ""
   693      , pipelineRunningKeyframes = ""
   694      , pipelines = RemoteData.NotAsked
   695      , screenSize = ScreenSize.Desktop
   696      , timeZone = Time.utc
   697      , turbulenceImgSrc = ""
   698      , userState = UserState.UserStateLoggedOut
   699      , version = ""
   700      }
   701  
   702  
   703  sampleOldModel : Model
   704  sampleOldModel =
   705      { page = OneOffBuildPage 0
   706      , now = Nothing
   707      , disableManualTrigger = False
   708      , history = []
   709      , nextPage = Nothing
   710      , currentBuild =
   711          RemoteData.Success
   712              { build =
   713                  { id = 0
   714                  , name = "0"
   715                  , job = Nothing
   716                  , status = Concourse.BuildStatus.BuildStatusStarted
   717                  , duration =
   718                      { startedAt = Nothing
   719                      , finishedAt = Nothing
   720                      }
   721                  , reapTime = Nothing
   722                  }
   723              , prep = Nothing
   724              , output =
   725                  Output
   726                      { steps = stepsModel
   727                      , state = Build.Output.Models.StepsLiveUpdating
   728                      , eventSourceOpened = True
   729                      , eventStreamUrlPath = Nothing
   730                      , highlight = Routes.HighlightNothing
   731                      }
   732              }
   733      , autoScroll = True
   734      , previousKeyPress = Nothing
   735      , shiftDown = False
   736      , isTriggerBuildKeyDown = False
   737      , showHelp = False
   738      , highlight = Routes.HighlightNothing
   739      , hoveredCounter = 0
   740      , fetchingHistory = False
   741      , scrolledToCurrentBuild = True
   742      , authorized = True
   743      , isUserMenuExpanded = False
   744      }
   745  
   746  
   747  sampleModel : Build.Models.Model
   748  sampleModel =
   749      { page = OneOffBuildPage 0
   750      , id = 0
   751      , name = "0"
   752      , now = Nothing
   753      , job = Nothing
   754      , disableManualTrigger = False
   755      , history = []
   756      , nextPage = Nothing
   757      , prep = Nothing
   758      , duration = { startedAt = Nothing, finishedAt = Nothing }
   759      , status = Concourse.BuildStatus.BuildStatusStarted
   760      , output =
   761          Output
   762              { steps = stepsModel
   763              , state = Build.Output.Models.StepsLiveUpdating
   764              , eventSourceOpened = True
   765              , eventStreamUrlPath = Nothing
   766              , highlight = Routes.HighlightNothing
   767              }
   768      , autoScroll = True
   769      , isScrollToIdInProgress = False
   770      , previousKeyPress = Nothing
   771      , isTriggerBuildKeyDown = False
   772      , showHelp = False
   773      , highlight = Routes.HighlightNothing
   774      , authorized = True
   775      , fetchingHistory = False
   776      , scrolledToCurrentBuild = False
   777      , shiftDown = False
   778      , isUserMenuExpanded = False
   779      , hasLoadedYet = True
   780      , notFound = False
   781      , reapTime = Nothing
   782      }
   783  
   784  
   785  ansiLogStyle : Ansi.Log.Style
   786  ansiLogStyle =
   787      { foreground = Nothing
   788      , background = Nothing
   789      , bold = False
   790      , faint = False
   791      , italic = False
   792      , underline = False
   793      , blink = False
   794      , inverted = False
   795      , fraktur = False
   796      , framed = False
   797      }
   798  
   799  
   800  position : Ansi.Log.CursorPosition
   801  position =
   802      { row = 0
   803      , column = 0
   804      }
   805  
   806  
   807  log : Ansi.Log.Model
   808  log =
   809      { lineDiscipline = Ansi.Log.Cooked
   810      , lines = Array.empty
   811      , position = position
   812      , savedPosition = Nothing
   813      , style = ansiLogStyle
   814      , remainder = ""
   815      }
   816  
   817  
   818  tree : STModels.StepTree
   819  tree =
   820      STModels.Task "stepid"
   821  
   822  
   823  steps : Dict Routes.StepID STModels.Step
   824  steps =
   825      Dict.singleton "stepid"
   826          { id = "stepid"
   827          , name = "task_step"
   828          , state = STModels.StepStateRunning
   829          , log = log
   830          , error = Nothing
   831          , expanded = True
   832          , version = Nothing
   833          , metadata = []
   834          , changed = False
   835          , timestamps = Dict.empty
   836          , initialize = Nothing
   837          , start = Nothing
   838          , finish = Nothing
   839          , tabFocus = STModels.Auto
   840          , expandedHeaders = Dict.empty
   841          , initializationExpanded = False
   842          , imageCheck = Nothing
   843          , imageGet = Nothing
   844          }
   845  
   846  
   847  stepsModel : Maybe STModels.StepTreeModel
   848  stepsModel =
   849      Just
   850          { tree = tree
   851          , steps = steps
   852          , highlight = Routes.HighlightNothing
   853          , resources = { inputs = [], outputs = [] }
   854          }
   855  
   856  
   857  sampleJob : String -> List String -> Concourse.Job
   858  sampleJob name passed =
   859      { name = name
   860      , pipelineName = "pipeline"
   861      , teamName = "team"
   862      , nextBuild = Nothing
   863      , finishedBuild = Nothing
   864      , transitionBuild = Nothing
   865      , paused = False
   866      , disableManualTrigger = False
   867      , inputs =
   868          [ { name = "input"
   869            , resource = "resource"
   870            , passed = passed
   871            , trigger = True
   872            }
   873          ]
   874      , outputs = []
   875      , groups = []
   876      }
   877  
   878  
   879  sampleJobs : List Concourse.Job
   880  sampleJobs =
   881      [ sampleJob "job1" []
   882      , sampleJob "job2a" [ "job1" ]
   883      , sampleJob "job2b" [ "job1" ]
   884      , sampleJob "job3" [ "job2a" ]
   885      , sampleJob "job4" [ "job3" ]
   886      ]
   887  
   888  
   889  dashboardPreviewView : List Concourse.Job -> Html msg
   890  dashboardPreviewView jobs =
   891      let
   892          groups =
   893              jobGroups jobs
   894  
   895          width =
   896              Dict.size groups
   897  
   898          height =
   899              Maybe.withDefault 0 <| List.maximum (List.map List.length (Dict.values groups))
   900      in
   901      Html.div
   902          [ classList
   903              [ ( "pipeline-grid", True )
   904              , ( "pipeline-grid-wide", width > 12 )
   905              , ( "pipeline-grid-tall", height > 12 )
   906              , ( "pipeline-grid-super-wide", width > 24 )
   907              , ( "pipeline-grid-super-tall", height > 24 )
   908              ]
   909          ]
   910      <|
   911          List.map
   912              (\js ->
   913                  List.map viewJob js
   914                      |> Html.div [ class "parallel-grid" ]
   915              )
   916              (Dict.values groups)
   917  
   918  
   919  viewJob : Concourse.Job -> Html msg
   920  viewJob job =
   921      let
   922          jobStatus =
   923              case job.finishedBuild of
   924                  Just fb ->
   925                      Concourse.BuildStatus.show fb.status
   926  
   927                  Nothing ->
   928                      "no-builds"
   929  
   930          isJobRunning =
   931              job.nextBuild /= Nothing
   932  
   933          latestBuild =
   934              if job.nextBuild == Nothing then
   935                  job.finishedBuild
   936  
   937              else
   938                  job.nextBuild
   939      in
   940      Html.div
   941          [ classList
   942              [ ( "node " ++ jobStatus, True )
   943              , ( "running", isJobRunning )
   944              , ( "paused", job.paused )
   945              ]
   946          , attribute "data-tooltip" job.name
   947          ]
   948      <|
   949          case latestBuild of
   950              Nothing ->
   951                  [ Html.a [ href <| Routes.toString <| Routes.jobRoute job ] [ Html.text "" ] ]
   952  
   953              Just build ->
   954                  [ Html.a [ href <| Routes.toString <| Routes.buildRoute build.id build.name build.job ] [ Html.text "" ] ]
   955  
   956  
   957  jobGroups : List Concourse.Job -> Dict Int (List Concourse.Job)
   958  jobGroups jobs =
   959      let
   960          jobLookup =
   961              jobByName <| List.foldl (\job byName -> Dict.insert job.name job byName) Dict.empty jobs
   962      in
   963      Dict.foldl
   964          (\jobName depth byDepth ->
   965              Dict.update depth
   966                  (\jobsA ->
   967                      Just (jobLookup jobName :: Maybe.withDefault [] jobsA)
   968                  )
   969                  byDepth
   970          )
   971          Dict.empty
   972          (jobDepths jobs Dict.empty)
   973  
   974  
   975  jobByName : Dict String Concourse.Job -> String -> Concourse.Job
   976  jobByName jobs job =
   977      case Dict.get job jobs of
   978          Just a ->
   979              a
   980  
   981          Nothing ->
   982              { name = ""
   983              , pipelineName = ""
   984              , teamName = ""
   985              , nextBuild = Nothing
   986              , finishedBuild = Nothing
   987              , transitionBuild = Nothing
   988              , paused = False
   989              , disableManualTrigger = False
   990              , inputs = []
   991              , outputs = []
   992              , groups = []
   993              }
   994  
   995  
   996  jobDepths : List Concourse.Job -> Dict String Int -> Dict String Int
   997  jobDepths jobs dict =
   998      case jobs of
   999          [] ->
  1000              dict
  1001  
  1002          job :: otherJobs ->
  1003              let
  1004                  passedJobs =
  1005                      List.concatMap .passed job.inputs
  1006              in
  1007              case List.length passedJobs of
  1008                  0 ->
  1009                      jobDepths otherJobs <| Dict.insert job.name 0 dict
  1010  
  1011                  _ ->
  1012                      let
  1013                          passedJobDepths =
  1014                              List.map (\passedJob -> Dict.get passedJob dict) passedJobs
  1015                      in
  1016                      if List.member Nothing passedJobDepths then
  1017                          jobDepths (List.append otherJobs [ job ]) dict
  1018  
  1019                      else
  1020                          let
  1021                              depths =
  1022                                  List.map (\depth -> Maybe.withDefault 0 depth) passedJobDepths
  1023  
  1024                              maxPassedJobDepth =
  1025                                  Maybe.withDefault 0 <| List.maximum depths
  1026                          in
  1027                          jobDepths otherJobs <| Dict.insert job.name (maxPassedJobDepth + 1) dict