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

     1  module Dashboard.Dashboard exposing
     2      ( documentTitle
     3      , handleCallback
     4      , handleDelivery
     5      , init
     6      , subscriptions
     7      , tooltip
     8      , update
     9      , view
    10      )
    11  
    12  import Application.Models exposing (Session)
    13  import Colors
    14  import Concourse
    15  import Concourse.BuildStatus
    16  import Concourse.Cli as Cli
    17  import Dashboard.DashboardPreview as DashboardPreview
    18  import Dashboard.Drag as Drag
    19  import Dashboard.Filter as Filter
    20  import Dashboard.Footer as Footer
    21  import Dashboard.Group as Group
    22  import Dashboard.Group.Models exposing (Pipeline)
    23  import Dashboard.Models as Models
    24      exposing
    25          ( DragState(..)
    26          , DropState(..)
    27          , Dropdown(..)
    28          , FetchError(..)
    29          , Model
    30          )
    31  import Dashboard.PipelineGrid as PipelineGrid
    32  import Dashboard.PipelineGrid.Constants as PipelineGridConstants
    33  import Dashboard.RequestBuffer as RequestBuffer exposing (Buffer(..))
    34  import Dashboard.SearchBar as SearchBar
    35  import Dashboard.Styles as Styles
    36  import Dashboard.Text as Text
    37  import Dict exposing (Dict)
    38  import EffectTransformer exposing (ET)
    39  import FetchResult exposing (FetchResult(..), changedFrom)
    40  import HoverState
    41  import Html exposing (Html)
    42  import Html.Attributes
    43      exposing
    44          ( attribute
    45          , class
    46          , download
    47          , href
    48          , id
    49          , src
    50          , style
    51          )
    52  import Html.Events
    53      exposing
    54          ( onMouseEnter
    55          , onMouseLeave
    56          )
    57  import Http
    58  import List.Extra
    59  import Login.Login as Login
    60  import Message.Callback exposing (Callback(..))
    61  import Message.Effects exposing (Effect(..), toHtmlID)
    62  import Message.Message as Message
    63      exposing
    64          ( DomID(..)
    65          , Message(..)
    66          , VisibilityAction(..)
    67          )
    68  import Message.Subscription
    69      exposing
    70          ( Delivery(..)
    71          , Interval(..)
    72          , Subscription(..)
    73          )
    74  import Routes
    75  import ScreenSize exposing (ScreenSize(..))
    76  import Set exposing (Set)
    77  import SideBar.SideBar as SideBar
    78  import StrictEvents exposing (onScroll)
    79  import Time
    80  import Tooltip
    81  import UserState
    82  import Views.Spinner as Spinner
    83  import Views.Styles
    84  import Views.Toggle as Toggle
    85  
    86  
    87  type alias Flags =
    88      { searchType : Routes.SearchType
    89      , dashboardView : Routes.DashboardView
    90      }
    91  
    92  
    93  init : Flags -> ( Model, List Effect )
    94  init f =
    95      ( { now = Nothing
    96        , hideFooter = False
    97        , hideFooterCounter = 0
    98        , showHelp = False
    99        , highDensity = f.searchType == Routes.HighDensity
   100        , query = Routes.extractQuery f.searchType
   101        , dashboardView = f.dashboardView
   102        , pipelinesWithResourceErrors = Set.empty
   103        , jobs = None
   104        , pipelines = Nothing
   105        , pipelineLayers = Dict.empty
   106        , teams = None
   107        , isUserMenuExpanded = False
   108        , dropdown = Hidden
   109        , dragState = Models.NotDragging
   110        , dropState = Models.NotDropping
   111        , isJobsRequestFinished = False
   112        , isTeamsRequestFinished = False
   113        , isResourcesRequestFinished = False
   114        , isPipelinesRequestFinished = False
   115        , jobsError = Nothing
   116        , teamsError = Nothing
   117        , resourcesError = Nothing
   118        , pipelinesError = Nothing
   119        , viewportWidth = 0
   120        , viewportHeight = 0
   121        , scrollTop = 0
   122        , pipelineJobs = Dict.empty
   123        , effectsToRetry = []
   124        }
   125      , [ FetchAllTeams
   126        , PinTeamNames Message.Effects.stickyHeaderConfig
   127        , GetScreenSize
   128        , FetchAllResources
   129        , FetchAllJobs
   130        , FetchAllPipelines
   131        , LoadCachedJobs
   132        , LoadCachedPipelines
   133        , LoadCachedTeams
   134        , GetViewportOf Dashboard
   135        ]
   136      )
   137  
   138  
   139  buffers : List (Buffer Model)
   140  buffers =
   141      [ Buffer FetchAllTeams
   142          (\c ->
   143              case c of
   144                  AllTeamsFetched _ ->
   145                      True
   146  
   147                  _ ->
   148                      False
   149          )
   150          (.dragState >> (/=) NotDragging)
   151          { get = \m -> m.isTeamsRequestFinished
   152          , set = \f m -> { m | isTeamsRequestFinished = f }
   153          }
   154      , Buffer FetchAllResources
   155          (\c ->
   156              case c of
   157                  AllResourcesFetched _ ->
   158                      True
   159  
   160                  _ ->
   161                      False
   162          )
   163          (.dragState >> (/=) NotDragging)
   164          { get = \m -> m.isResourcesRequestFinished
   165          , set = \f m -> { m | isResourcesRequestFinished = f }
   166          }
   167      , Buffer FetchAllJobs
   168          (\c ->
   169              case c of
   170                  AllJobsFetched _ ->
   171                      True
   172  
   173                  _ ->
   174                      False
   175          )
   176          (\model -> model.dragState /= NotDragging || model.jobsError == Just Disabled)
   177          { get = \m -> m.isJobsRequestFinished
   178          , set = \f m -> { m | isJobsRequestFinished = f }
   179          }
   180      , Buffer FetchAllPipelines
   181          (\c ->
   182              case c of
   183                  AllPipelinesFetched _ ->
   184                      True
   185  
   186                  _ ->
   187                      False
   188          )
   189          (.dragState >> (/=) NotDragging)
   190          { get = \m -> m.isPipelinesRequestFinished
   191          , set = \f m -> { m | isPipelinesRequestFinished = f }
   192          }
   193      ]
   194  
   195  
   196  handleCallback : Callback -> ET Model
   197  handleCallback callback ( model, effects ) =
   198      (case callback of
   199          AllTeamsFetched (Err _) ->
   200              ( { model | teamsError = Just Failed }
   201              , effects
   202              )
   203  
   204          AllTeamsFetched (Ok teams) ->
   205              let
   206                  newTeams =
   207                      Fetched teams
   208              in
   209              ( { model
   210                  | teams = newTeams
   211                  , teamsError = Nothing
   212                }
   213              , effects
   214                  ++ (if newTeams |> changedFrom model.teams then
   215                          [ SaveCachedTeams teams ]
   216  
   217                      else
   218                          []
   219                     )
   220              )
   221  
   222          AllJobsFetched (Ok allJobsInEntireCluster) ->
   223              let
   224                  removeBuild job =
   225                      { job
   226                          | finishedBuild = Nothing
   227                          , transitionBuild = Nothing
   228                          , nextBuild = Nothing
   229                      }
   230  
   231                  newJobs =
   232                      allJobsInEntireCluster
   233                          |> List.map
   234                              (\job ->
   235                                  ( ( job.teamName
   236                                    , job.pipelineName
   237                                    , job.name
   238                                    )
   239                                  , job
   240                                  )
   241                              )
   242                          |> Dict.fromList
   243                          |> Fetched
   244  
   245                  maxJobsInCache =
   246                      1000
   247  
   248                  mapToJobIds jobsResult =
   249                      jobsResult
   250                          |> FetchResult.map (Dict.toList >> List.map Tuple.first)
   251  
   252                  newModel =
   253                      { model
   254                          | jobs = newJobs
   255                          , jobsError = Nothing
   256                      }
   257              in
   258              if mapToJobIds newJobs |> changedFrom (mapToJobIds model.jobs) then
   259                  ( newModel |> precomputeJobMetadata
   260                  , effects
   261                      ++ [ allJobsInEntireCluster
   262                              |> List.take maxJobsInCache
   263                              |> List.map removeBuild
   264                              |> SaveCachedJobs
   265                         ]
   266                  )
   267  
   268              else
   269                  ( newModel, effects )
   270  
   271          AllJobsFetched (Err err) ->
   272              case err of
   273                  Http.BadStatus { status } ->
   274                      case status.code of
   275                          501 ->
   276                              ( { model
   277                                  | jobsError = Just Disabled
   278                                  , jobs = Fetched Dict.empty
   279                                  , pipelines =
   280                                      model.pipelines
   281                                          |> Maybe.map
   282                                              (Dict.map
   283                                                  (\_ l ->
   284                                                      List.map
   285                                                          (\p ->
   286                                                              { p | jobsDisabled = True }
   287                                                          )
   288                                                          l
   289                                                  )
   290                                              )
   291                                }
   292                              , effects ++ [ DeleteCachedJobs ]
   293                              )
   294  
   295                          503 ->
   296                              ( { model
   297                                  | effectsToRetry =
   298                                      model.effectsToRetry
   299                                          ++ (if List.member FetchAllJobs model.effectsToRetry then
   300                                                  []
   301  
   302                                              else
   303                                                  [ FetchAllJobs ]
   304                                             )
   305                                }
   306                              , effects
   307                              )
   308  
   309                          _ ->
   310                              ( { model | jobsError = Just Failed }, effects )
   311  
   312                  _ ->
   313                      ( { model | jobsError = Just Failed }, effects )
   314  
   315          AllResourcesFetched (Ok resources) ->
   316              let
   317                  failingToCheck { build } =
   318                      case build of
   319                          Nothing ->
   320                              False
   321  
   322                          Just { status } ->
   323                              Concourse.BuildStatus.isBad status
   324              in
   325              ( { model
   326                  | pipelinesWithResourceErrors =
   327                      resources
   328                          |> List.filter failingToCheck
   329                          |> List.map (\r -> ( r.teamName, r.pipelineName ))
   330                          |> Set.fromList
   331                  , resourcesError = Nothing
   332                }
   333              , effects
   334              )
   335  
   336          AllResourcesFetched (Err _) ->
   337              ( { model | resourcesError = Just Failed }, effects )
   338  
   339          AllPipelinesFetched (Ok allPipelinesInEntireCluster) ->
   340              let
   341                  newPipelines =
   342                      allPipelinesInEntireCluster
   343                          |> List.map (toDashboardPipeline False (model.jobsError == Just Disabled))
   344                          |> groupBy .teamName
   345                          |> Just
   346              in
   347              ( { model
   348                  | pipelines = newPipelines
   349                  , pipelinesError = Nothing
   350                }
   351              , effects
   352                  ++ GetViewportOf Dashboard
   353                  :: (if List.isEmpty allPipelinesInEntireCluster then
   354                          [ ModifyUrl "/" ]
   355  
   356                      else
   357                          []
   358                     )
   359                  ++ (if newPipelines |> pipelinesChangedFrom model.pipelines then
   360                          [ SaveCachedPipelines allPipelinesInEntireCluster ]
   361  
   362                      else
   363                          []
   364                     )
   365              )
   366  
   367          AllPipelinesFetched (Err _) ->
   368              ( { model | pipelinesError = Just Failed }, effects )
   369  
   370          PipelinesOrdered teamName _ ->
   371              ( model, effects ++ [ FetchPipelines teamName ] )
   372  
   373          PipelinesFetched _ ->
   374              ( { model | dropState = NotDropping }
   375              , effects
   376              )
   377  
   378          LoggedOut (Ok ()) ->
   379              ( model
   380              , effects
   381                  ++ [ NavigateTo <|
   382                          Routes.toString <|
   383                              Routes.Dashboard
   384                                  { searchType =
   385                                      if model.highDensity then
   386                                          Routes.HighDensity
   387  
   388                                      else
   389                                          Routes.Normal model.query
   390                                  , dashboardView = model.dashboardView
   391                                  }
   392                     , FetchAllTeams
   393                     , FetchAllResources
   394                     , FetchAllJobs
   395                     , FetchAllPipelines
   396                     , DeleteCachedPipelines
   397                     , DeleteCachedJobs
   398                     , DeleteCachedTeams
   399                     ]
   400              )
   401  
   402          PipelineToggled _ (Ok ()) ->
   403              ( model, effects ++ [ FetchAllPipelines ] )
   404  
   405          VisibilityChanged Hide pipelineId (Ok ()) ->
   406              ( updatePipeline
   407                  (\p -> { p | public = False, isVisibilityLoading = False })
   408                  pipelineId
   409                  model
   410              , effects
   411              )
   412  
   413          VisibilityChanged Hide pipelineId (Err _) ->
   414              ( updatePipeline
   415                  (\p -> { p | public = True, isVisibilityLoading = False })
   416                  pipelineId
   417                  model
   418              , effects
   419              )
   420  
   421          VisibilityChanged Expose pipelineId (Ok ()) ->
   422              ( updatePipeline
   423                  (\p -> { p | public = True, isVisibilityLoading = False })
   424                  pipelineId
   425                  model
   426              , effects
   427              )
   428  
   429          VisibilityChanged Expose pipelineId (Err _) ->
   430              ( updatePipeline
   431                  (\p -> { p | public = False, isVisibilityLoading = False })
   432                  pipelineId
   433                  model
   434              , effects
   435              )
   436  
   437          GotViewport Dashboard (Ok viewport) ->
   438              ( { model
   439                  | viewportWidth = viewport.viewport.width
   440                  , viewportHeight = viewport.viewport.height
   441                  , scrollTop = viewport.viewport.y
   442                }
   443              , effects
   444              )
   445  
   446          _ ->
   447              ( model, effects )
   448      )
   449          |> RequestBuffer.handleCallback callback buffers
   450  
   451  
   452  updatePipeline :
   453      (Pipeline -> Pipeline)
   454      -> Concourse.PipelineIdentifier
   455      -> Model
   456      -> Model
   457  updatePipeline updater pipelineId model =
   458      { model
   459          | pipelines =
   460              model.pipelines
   461                  |> Maybe.map
   462                      (Dict.update pipelineId.teamName
   463                          (Maybe.map
   464                              (List.Extra.updateIf
   465                                  (\p -> p.name == pipelineId.pipelineName)
   466                                  updater
   467                              )
   468                          )
   469                      )
   470      }
   471  
   472  
   473  findPipeline : Concourse.PipelineIdentifier -> Maybe (Dict String (List Pipeline)) -> Maybe Pipeline
   474  findPipeline pipelineId pipelines =
   475      pipelines
   476          |> Maybe.andThen (Dict.get pipelineId.teamName)
   477          |> Maybe.andThen (List.Extra.find (.name >> (==) pipelineId.pipelineName))
   478  
   479  
   480  handleDelivery : Delivery -> ET Model
   481  handleDelivery delivery =
   482      SearchBar.handleDelivery delivery
   483          >> Footer.handleDelivery delivery
   484          >> RequestBuffer.handleDelivery delivery buffers
   485          >> handleDeliveryBody delivery
   486  
   487  
   488  handleDeliveryBody : Delivery -> ET Model
   489  handleDeliveryBody delivery ( model, effects ) =
   490      case delivery of
   491          ClockTicked OneSecond time ->
   492              ( { model | now = Just time, effectsToRetry = [] }, model.effectsToRetry )
   493  
   494          WindowResized _ _ ->
   495              ( model, effects ++ [ GetViewportOf Dashboard ] )
   496  
   497          SideBarStateReceived _ ->
   498              ( model, effects ++ [ GetViewportOf Dashboard ] )
   499  
   500          CachedPipelinesReceived (Ok pipelines) ->
   501              if model.pipelines == Nothing then
   502                  ( { model
   503                      | pipelines =
   504                          pipelines
   505                              |> List.map
   506                                  (toDashboardPipeline
   507                                      True
   508                                      (model.jobsError == Just Disabled)
   509                                  )
   510                              |> groupBy .teamName
   511                              |> Just
   512                    }
   513                  , effects
   514                  )
   515  
   516              else
   517                  ( model, effects )
   518  
   519          CachedJobsReceived (Ok jobs) ->
   520              let
   521                  newJobs =
   522                      jobs
   523                          |> List.map
   524                              (\job ->
   525                                  ( ( job.teamName
   526                                    , job.pipelineName
   527                                    , job.name
   528                                    )
   529                                  , job
   530                                  )
   531                              )
   532                          |> Dict.fromList
   533                          |> Cached
   534  
   535                  mapToJobIds jobsResult =
   536                      jobsResult
   537                          |> FetchResult.map (Dict.toList >> List.map Tuple.first)
   538              in
   539              if mapToJobIds newJobs |> changedFrom (mapToJobIds model.jobs) then
   540                  ( { model | jobs = newJobs } |> precomputeJobMetadata
   541                  , effects
   542                  )
   543  
   544              else
   545                  ( model, effects )
   546  
   547          CachedTeamsReceived (Ok teams) ->
   548              let
   549                  newTeams =
   550                      Cached teams
   551              in
   552              if newTeams |> changedFrom model.teams then
   553                  ( { model | teams = newTeams }, effects )
   554  
   555              else
   556                  ( model, effects )
   557  
   558          _ ->
   559              ( model, effects )
   560  
   561  
   562  toDashboardPipeline : Bool -> Bool -> Concourse.Pipeline -> Pipeline
   563  toDashboardPipeline isStale jobsDisabled p =
   564      { id = p.id
   565      , name = p.name
   566      , teamName = p.teamName
   567      , public = p.public
   568      , isToggleLoading = False
   569      , isVisibilityLoading = False
   570      , paused = p.paused
   571      , archived = p.archived
   572      , stale = isStale
   573      , jobsDisabled = jobsDisabled
   574      }
   575  
   576  
   577  toConcoursePipeline : Pipeline -> Concourse.Pipeline
   578  toConcoursePipeline p =
   579      { id = p.id
   580      , name = p.name
   581      , teamName = p.teamName
   582      , public = p.public
   583      , paused = p.paused
   584      , archived = p.archived
   585      , groups = []
   586      , backgroundImage = Maybe.Nothing
   587      }
   588  
   589  
   590  pipelinesChangedFrom :
   591      Maybe (Dict String (List Pipeline))
   592      -> Maybe (Dict String (List Pipeline))
   593      -> Bool
   594  pipelinesChangedFrom ps qs =
   595      let
   596          project =
   597              Maybe.map <|
   598                  Dict.values
   599                      >> List.concat
   600                      >> List.map (\x -> { x | stale = True })
   601      in
   602      project ps /= project qs
   603  
   604  
   605  groupBy : (a -> comparable) -> List a -> Dict comparable (List a)
   606  groupBy keyfn list =
   607      -- From https://github.com/elm-community/dict-extra/blob/2.3.0/src/Dict/Extra.elm
   608      List.foldr
   609          (\x acc ->
   610              Dict.update (keyfn x) (Maybe.map ((::) x) >> Maybe.withDefault [ x ] >> Just) acc
   611          )
   612          Dict.empty
   613          list
   614  
   615  
   616  precomputeJobMetadata : Model -> Model
   617  precomputeJobMetadata model =
   618      let
   619          allJobs =
   620              model.jobs
   621                  |> FetchResult.withDefault Dict.empty
   622                  |> Dict.values
   623  
   624          pipelineJobs =
   625              allJobs |> groupBy (\j -> ( j.teamName, j.pipelineName ))
   626  
   627          jobToId job =
   628              { teamName = job.teamName
   629              , pipelineName = job.pipelineName
   630              , jobName = job.name
   631              }
   632      in
   633      { model
   634          | pipelineLayers =
   635              pipelineJobs
   636                  |> Dict.map
   637                      (\_ jobs ->
   638                          jobs
   639                              |> DashboardPreview.groupByRank
   640                              |> List.map (List.map jobToId)
   641                      )
   642          , pipelineJobs =
   643              pipelineJobs
   644                  |> Dict.map (\_ jobs -> jobs |> List.map jobToId)
   645      }
   646  
   647  
   648  update : Session -> Message -> ET Model
   649  update session msg =
   650      SearchBar.update session msg >> updateBody msg
   651  
   652  
   653  updateBody : Message -> ET Model
   654  updateBody msg ( model, effects ) =
   655      case msg of
   656          DragStart teamName pipelineName ->
   657              ( { model | dragState = Models.Dragging teamName pipelineName }, effects )
   658  
   659          DragOver target ->
   660              ( { model | dropState = Models.Dropping target }, effects )
   661  
   662          TooltipHd pipelineName teamName ->
   663              ( model, effects ++ [ ShowTooltipHd ( pipelineName, teamName ) ] )
   664  
   665          Tooltip pipelineName teamName ->
   666              ( model, effects ++ [ ShowTooltip ( pipelineName, teamName ) ] )
   667  
   668          DragEnd ->
   669              case ( model.dragState, model.dropState ) of
   670                  ( Dragging teamName pipelineName, Dropping target ) ->
   671                      let
   672                          teamPipelines =
   673                              model.pipelines
   674                                  |> Maybe.andThen (Dict.get teamName)
   675                                  |> Maybe.withDefault []
   676                                  |> Drag.dragPipeline pipelineName target
   677  
   678                          pipelines =
   679                              model.pipelines
   680                                  |> Maybe.withDefault Dict.empty
   681                                  |> Dict.update teamName (always <| Just teamPipelines)
   682                      in
   683                      ( { model
   684                          | pipelines = Just pipelines
   685                          , dragState = NotDragging
   686                          , dropState = DroppingWhileApiRequestInFlight teamName
   687                        }
   688                      , effects
   689                          ++ [ teamPipelines
   690                                  |> List.map .name
   691                                  |> SendOrderPipelinesRequest teamName
   692                             , pipelines
   693                                  |> Dict.values
   694                                  |> List.concat
   695                                  |> List.map toConcoursePipeline
   696                                  |> SaveCachedPipelines
   697                             ]
   698                      )
   699  
   700                  _ ->
   701                      ( { model
   702                          | dragState = NotDragging
   703                          , dropState = NotDropping
   704                        }
   705                      , effects
   706                      )
   707  
   708          Hover (Just domID) ->
   709              ( model, effects ++ [ GetViewportOf domID ] )
   710  
   711          Click LogoutButton ->
   712              ( { model
   713                  | teams = None
   714                  , pipelines = Nothing
   715                  , jobs = None
   716                }
   717              , effects
   718              )
   719  
   720          Click (PipelineCardPauseToggle _ pipelineId) ->
   721              let
   722                  isPaused =
   723                      model.pipelines
   724                          |> findPipeline pipelineId
   725                          |> Maybe.map .paused
   726              in
   727              case isPaused of
   728                  Just ip ->
   729                      ( updatePipeline
   730                          (\p -> { p | isToggleLoading = True })
   731                          pipelineId
   732                          model
   733                      , effects
   734                          ++ [ SendTogglePipelineRequest pipelineId ip ]
   735                      )
   736  
   737                  Nothing ->
   738                      ( model, effects )
   739  
   740          Click (VisibilityButton _ pipelineId) ->
   741              let
   742                  isPublic =
   743                      model.pipelines
   744                          |> findPipeline pipelineId
   745                          |> Maybe.map .public
   746              in
   747              case isPublic of
   748                  Just public ->
   749                      ( updatePipeline
   750                          (\p -> { p | isVisibilityLoading = True })
   751                          pipelineId
   752                          model
   753                      , effects
   754                          ++ [ if public then
   755                                  ChangeVisibility Hide pipelineId
   756  
   757                               else
   758                                  ChangeVisibility Expose pipelineId
   759                             ]
   760                      )
   761  
   762                  Nothing ->
   763                      ( model, effects )
   764  
   765          Click HamburgerMenu ->
   766              ( model, effects ++ [ GetViewportOf Dashboard ] )
   767  
   768          Scrolled scrollState ->
   769              ( { model | scrollTop = scrollState.scrollTop }, effects )
   770  
   771          _ ->
   772              ( model, effects )
   773  
   774  
   775  subscriptions : List Subscription
   776  subscriptions =
   777      [ OnClockTick OneSecond
   778      , OnClockTick FiveSeconds
   779      , OnMouse
   780      , OnKeyDown
   781      , OnKeyUp
   782      , OnWindowResize
   783      , OnCachedJobsReceived
   784      , OnCachedPipelinesReceived
   785      , OnCachedTeamsReceived
   786      ]
   787  
   788  
   789  documentTitle : String
   790  documentTitle =
   791      "Dashboard"
   792  
   793  
   794  view : Session -> Model -> Html Message
   795  view session model =
   796      Html.div
   797          (id "page-including-top-bar" :: Views.Styles.pageIncludingTopBar)
   798          [ topBar session model
   799          , Html.div
   800              [ id "page-below-top-bar"
   801              , style "padding-top" "54px"
   802              , style "box-sizing" "border-box"
   803              , style "display" "flex"
   804              , style "height" "100%"
   805              , style "padding-bottom" <|
   806                  if model.showHelp || model.hideFooter then
   807                      "0"
   808  
   809                  else
   810                      "50px"
   811              ]
   812            <|
   813              [ SideBar.view session Nothing
   814              , dashboardView session model
   815              ]
   816          , Footer.view session model
   817          ]
   818  
   819  
   820  tooltip : { a | pipelines : Maybe (Dict String (List Pipeline)) } -> { b | hovered : HoverState.HoverState } -> Maybe Tooltip.Tooltip
   821  tooltip model { hovered } =
   822      case hovered of
   823          HoverState.Tooltip (Message.PipelineStatusIcon _ _) _ ->
   824              Just
   825                  { body =
   826                      Html.div
   827                          Styles.jobsDisabledTooltip
   828                          [ Html.text "automatic job monitoring disabled" ]
   829                  , attachPosition = { direction = Tooltip.Top, alignment = Tooltip.Start }
   830                  , arrow = Nothing
   831                  }
   832  
   833          HoverState.Tooltip (Message.VisibilityButton _ pipelineId) _ ->
   834              model.pipelines
   835                  |> findPipeline pipelineId
   836                  |> Maybe.map
   837                      (\p ->
   838                          { body =
   839                              Html.div
   840                                  Styles.visibilityTooltip
   841                                  [ Html.text <|
   842                                      if p.public then
   843                                          "hide pipeline"
   844  
   845                                      else
   846                                          "expose pipeline"
   847                                  ]
   848                          , attachPosition =
   849                              { direction = Tooltip.Top
   850                              , alignment = Tooltip.End
   851                              }
   852                          , arrow = Nothing
   853                          }
   854                      )
   855  
   856          _ ->
   857              Nothing
   858  
   859  
   860  topBar : Session -> Model -> Html Message
   861  topBar session model =
   862      Html.div
   863          (id "top-bar-app" :: Views.Styles.topBar False)
   864      <|
   865          [ Html.div [ style "display" "flex", style "align-items" "center" ]
   866              [ SideBar.hamburgerMenu session
   867              , Html.a (href "/" :: Views.Styles.concourseLogo) []
   868              , clusterNameView session
   869              ]
   870          ]
   871              ++ (let
   872                      isDropDownHidden =
   873                          model.dropdown == Hidden
   874  
   875                      isMobile =
   876                          session.screenSize == ScreenSize.Mobile
   877                  in
   878                  if
   879                      not model.highDensity
   880                          && isMobile
   881                          && (not isDropDownHidden || model.query /= "")
   882                  then
   883                      [ SearchBar.view session model ]
   884  
   885                  else if not model.highDensity then
   886                      [ topBarContent [ SearchBar.view session model ]
   887                      , showArchivedToggleView model
   888                      , Login.view session.userState model
   889                      ]
   890  
   891                  else
   892                      [ topBarContent []
   893                      , showArchivedToggleView model
   894                      , Login.view session.userState model
   895                      ]
   896                 )
   897  
   898  
   899  topBarContent : List (Html Message) -> Html Message
   900  topBarContent content =
   901      Html.div
   902          (id "top-bar-content" :: Styles.topBarContent)
   903          content
   904  
   905  
   906  clusterNameView : Session -> Html Message
   907  clusterNameView session =
   908      Html.div
   909          Styles.clusterName
   910          [ Html.text session.clusterName ]
   911  
   912  
   913  showArchivedToggleView :
   914      { a
   915          | pipelines : Maybe (Dict String (List Pipeline))
   916          , query : String
   917          , highDensity : Bool
   918          , dashboardView : Routes.DashboardView
   919      }
   920      -> Html Message
   921  showArchivedToggleView model =
   922      let
   923          noPipelines =
   924              model.pipelines
   925                  |> Maybe.withDefault Dict.empty
   926                  |> Dict.values
   927                  |> List.all List.isEmpty
   928  
   929          on =
   930              model.dashboardView == Routes.ViewAllPipelines
   931      in
   932      if noPipelines then
   933          Html.text ""
   934  
   935      else
   936          Toggle.toggleSwitch
   937              { ariaLabel = "Toggle whether archived pipelines are displayed"
   938              , hrefRoute =
   939                  Routes.Dashboard
   940                      { searchType =
   941                          if model.highDensity then
   942                              Routes.HighDensity
   943  
   944                          else
   945                              Routes.Normal model.query
   946                      , dashboardView =
   947                          if on then
   948                              Routes.ViewNonArchivedPipelines
   949  
   950                          else
   951                              Routes.ViewAllPipelines
   952                      }
   953              , text = "show archived"
   954              , textDirection = Toggle.Left
   955              , on = on
   956              , styles = Styles.showArchivedToggle
   957              }
   958  
   959  
   960  showTurbulence :
   961      { a
   962          | jobsError : Maybe FetchError
   963          , teamsError : Maybe FetchError
   964          , resourcesError : Maybe FetchError
   965          , pipelinesError : Maybe FetchError
   966      }
   967      -> Bool
   968  showTurbulence model =
   969      (model.jobsError == Just Failed)
   970          || (model.teamsError == Just Failed)
   971          || (model.resourcesError == Just Failed)
   972          || (model.pipelinesError == Just Failed)
   973  
   974  
   975  dashboardView :
   976      { a
   977          | hovered : HoverState.HoverState
   978          , screenSize : ScreenSize
   979          , userState : UserState.UserState
   980          , turbulenceImgSrc : String
   981          , pipelineRunningKeyframes : String
   982          , favoritedPipelines : Set Concourse.DatabaseID
   983      }
   984      -> Model
   985      -> Html Message
   986  dashboardView session model =
   987      if showTurbulence model then
   988          turbulenceView session.turbulenceImgSrc
   989  
   990      else
   991          Html.div
   992              (class (.pageBodyClass Message.Effects.stickyHeaderConfig)
   993                  :: id (toHtmlID Dashboard)
   994                  :: onScroll Scrolled
   995                  :: Styles.content model.highDensity
   996              )
   997              (case model.pipelines of
   998                  Nothing ->
   999                      [ loadingView ]
  1000  
  1001                  Just pipelines ->
  1002                      if pipelines |> Dict.values |> List.all List.isEmpty then
  1003                          welcomeCard session :: pipelinesView session model
  1004  
  1005                      else
  1006                          Html.text "" :: pipelinesView session model
  1007              )
  1008  
  1009  
  1010  loadingView : Html Message
  1011  loadingView =
  1012      Html.div
  1013          (class "loading" :: Styles.loadingView)
  1014          [ Spinner.spinner { sizePx = 36, margin = "0" } ]
  1015  
  1016  
  1017  welcomeCard :
  1018      { a | hovered : HoverState.HoverState, userState : UserState.UserState }
  1019      -> Html Message
  1020  welcomeCard session =
  1021      let
  1022          cliIcon : HoverState.HoverState -> Cli.Cli -> Html Message
  1023          cliIcon hoverable cli =
  1024              Html.a
  1025                  ([ href <| Cli.downloadUrl cli
  1026                   , attribute "aria-label" <| Cli.label cli
  1027                   , id <| "top-cli-" ++ Cli.id cli
  1028                   , onMouseEnter <| Hover <| Just <| Message.WelcomeCardCliIcon cli
  1029                   , onMouseLeave <| Hover Nothing
  1030                   , download ""
  1031                   ]
  1032                      ++ Styles.topCliIcon
  1033                          { hovered =
  1034                              HoverState.isHovered
  1035                                  (Message.WelcomeCardCliIcon cli)
  1036                                  hoverable
  1037                          , cli = cli
  1038                          }
  1039                  )
  1040                  []
  1041      in
  1042      Html.div
  1043          (id "welcome-card" :: Styles.welcomeCard)
  1044          [ Html.div
  1045              Styles.welcomeCardTitle
  1046              [ Html.text Text.welcome ]
  1047          , Html.div
  1048              Styles.welcomeCardBody
  1049            <|
  1050              [ Html.div
  1051                  [ style "display" "flex"
  1052                  , style "align-items" "center"
  1053                  ]
  1054                <|
  1055                  [ Html.div
  1056                      [ style "margin-right" "10px" ]
  1057                      [ Html.text Text.cliInstructions ]
  1058                  ]
  1059                      ++ List.map (cliIcon session.hovered) Cli.clis
  1060              , Html.div
  1061                  []
  1062                  [ Html.text Text.setPipelineInstructions ]
  1063              ]
  1064                  ++ loginInstruction session.userState
  1065          , Html.pre
  1066              Styles.asciiArt
  1067              [ Html.text Text.asciiArt ]
  1068          ]
  1069  
  1070  
  1071  loginInstruction : UserState.UserState -> List (Html Message)
  1072  loginInstruction userState =
  1073      case userState of
  1074          UserState.UserStateLoggedIn _ ->
  1075              []
  1076  
  1077          _ ->
  1078              [ Html.div
  1079                  [ id "login-instruction"
  1080                  , style "line-height" "42px"
  1081                  ]
  1082                  [ Html.text "login "
  1083                  , Html.a
  1084                      [ href "/login"
  1085                      , style "text-decoration" "underline"
  1086                      , style "color" Colors.welcomeCardText
  1087                      ]
  1088                      [ Html.text "here" ]
  1089                  ]
  1090              ]
  1091  
  1092  
  1093  noResultsView : String -> Html Message
  1094  noResultsView query =
  1095      let
  1096          boldedQuery =
  1097              Html.span [ class "monospace-bold" ] [ Html.text query ]
  1098      in
  1099      Html.div
  1100          (class "no-results" :: Styles.noResults)
  1101          [ Html.text "No results for "
  1102          , boldedQuery
  1103          , Html.text " matched your search."
  1104          ]
  1105  
  1106  
  1107  turbulenceView : String -> Html Message
  1108  turbulenceView path =
  1109      Html.div
  1110          [ class "error-message" ]
  1111          [ Html.div [ class "message" ]
  1112              [ Html.img [ src path, class "seatbelt" ] []
  1113              , Html.p [] [ Html.text "experiencing turbulence" ]
  1114              , Html.p [ class "explanation" ] []
  1115              ]
  1116          ]
  1117  
  1118  
  1119  pipelinesView :
  1120      { a
  1121          | userState : UserState.UserState
  1122          , hovered : HoverState.HoverState
  1123          , pipelineRunningKeyframes : String
  1124          , favoritedPipelines : Set Concourse.DatabaseID
  1125      }
  1126      ->
  1127          { b
  1128              | teams : FetchResult (List Concourse.Team)
  1129              , query : String
  1130              , highDensity : Bool
  1131              , dashboardView : Routes.DashboardView
  1132              , pipelinesWithResourceErrors : Set ( String, String )
  1133              , pipelineLayers : Dict ( String, String ) (List (List Concourse.JobIdentifier))
  1134              , pipelines : Maybe (Dict String (List Pipeline))
  1135              , jobs : FetchResult (Dict ( String, String, String ) Concourse.Job)
  1136              , dragState : DragState
  1137              , dropState : DropState
  1138              , now : Maybe Time.Posix
  1139              , viewportWidth : Float
  1140              , viewportHeight : Float
  1141              , scrollTop : Float
  1142              , pipelineJobs : Dict ( String, String ) (List Concourse.JobIdentifier)
  1143          }
  1144      -> List (Html Message)
  1145  pipelinesView session params =
  1146      let
  1147          pipelines =
  1148              params.pipelines
  1149                  |> Maybe.withDefault Dict.empty
  1150  
  1151          jobs =
  1152              params.jobs
  1153                  |> FetchResult.withDefault Dict.empty
  1154  
  1155          teams =
  1156              params.teams
  1157                  |> FetchResult.withDefault []
  1158  
  1159          filteredGroups =
  1160              Filter.filterGroups
  1161                  { pipelineJobs = params.pipelineJobs
  1162                  , jobs = jobs
  1163                  , query = params.query
  1164                  , teams = teams
  1165                  , pipelines = pipelines
  1166                  , dashboardView = params.dashboardView
  1167                  , favoritedPipelines = session.favoritedPipelines
  1168                  }
  1169                  |> List.sortWith (Group.ordering session)
  1170  
  1171          ( headerView, offsetHeight ) =
  1172              if params.highDensity then
  1173                  ( [], 0 )
  1174  
  1175              else
  1176                  let
  1177                      favoritedPipelines =
  1178                          filteredGroups
  1179                              |> List.concatMap .pipelines
  1180                              |> List.filter
  1181                                  (\fp ->
  1182                                      Set.member fp.id session.favoritedPipelines
  1183                                  )
  1184  
  1185                      allPipelinesHeader =
  1186                          Html.div Styles.pipelineSectionHeader [ Html.text "all pipelines" ]
  1187                  in
  1188                  if List.isEmpty filteredGroups then
  1189                      ( [], 0 )
  1190  
  1191                  else if List.isEmpty favoritedPipelines then
  1192                      ( [ allPipelinesHeader ], PipelineGridConstants.sectionHeaderHeight )
  1193  
  1194                  else
  1195                      let
  1196                          offset =
  1197                              PipelineGridConstants.sectionHeaderHeight
  1198  
  1199                          layout =
  1200                              PipelineGrid.computeFavoritePipelinesLayout
  1201                                  { pipelineLayers = params.pipelineLayers
  1202                                  , viewportWidth = params.viewportWidth
  1203                                  , viewportHeight = params.viewportHeight
  1204                                  , scrollTop = params.scrollTop - offset
  1205                                  }
  1206                                  favoritedPipelines
  1207                      in
  1208                      [ Html.div Styles.pipelineSectionHeader [ Html.text "favorite pipelines" ]
  1209                      , Group.viewFavoritePipelines
  1210                          session
  1211                          { dragState = NotDragging
  1212                          , dropState = NotDropping
  1213                          , now = params.now
  1214                          , hovered = session.hovered
  1215                          , pipelineRunningKeyframes = session.pipelineRunningKeyframes
  1216                          , pipelinesWithResourceErrors = params.pipelinesWithResourceErrors
  1217                          , pipelineLayers = params.pipelineLayers
  1218                          , pipelineCards = layout.pipelineCards
  1219                          , headers = layout.headers
  1220                          , groupCardsHeight = layout.height
  1221                          , pipelineJobs = params.pipelineJobs
  1222                          , jobs = jobs
  1223                          }
  1224                      , Views.Styles.separator PipelineGridConstants.sectionSpacerHeight
  1225                      , allPipelinesHeader
  1226                      ]
  1227                          |> (\html ->
  1228                                  ( html
  1229                                  , layout.height
  1230                                      + (2 * PipelineGridConstants.sectionHeaderHeight)
  1231                                      + PipelineGridConstants.sectionSpacerHeight
  1232                                  )
  1233                             )
  1234  
  1235          groupViews =
  1236              filteredGroups
  1237                  |> (if params.highDensity then
  1238                          List.concatMap
  1239                              (Group.hdView
  1240                                  { pipelineRunningKeyframes = session.pipelineRunningKeyframes
  1241                                  , pipelinesWithResourceErrors = params.pipelinesWithResourceErrors
  1242                                  , pipelineJobs = params.pipelineJobs
  1243                                  , jobs = jobs
  1244                                  }
  1245                                  session
  1246                              )
  1247  
  1248                      else
  1249                          List.foldl
  1250                              (\g ( htmlList, totalOffset ) ->
  1251                                  let
  1252                                      layout =
  1253                                          PipelineGrid.computeLayout
  1254                                              { dragState = params.dragState
  1255                                              , dropState = params.dropState
  1256                                              , pipelineLayers = params.pipelineLayers
  1257                                              , viewportWidth = params.viewportWidth
  1258                                              , viewportHeight = params.viewportHeight
  1259                                              , scrollTop = params.scrollTop - totalOffset
  1260                                              }
  1261                                              g
  1262                                  in
  1263                                  Group.view
  1264                                      session
  1265                                      { dragState = params.dragState
  1266                                      , dropState = params.dropState
  1267                                      , now = params.now
  1268                                      , hovered = session.hovered
  1269                                      , pipelineRunningKeyframes = session.pipelineRunningKeyframes
  1270                                      , pipelinesWithResourceErrors = params.pipelinesWithResourceErrors
  1271                                      , pipelineLayers = params.pipelineLayers
  1272                                      , pipelineCards = layout.pipelineCards
  1273                                      , dropAreas = layout.dropAreas
  1274                                      , groupCardsHeight = layout.height
  1275                                      , pipelineJobs = params.pipelineJobs
  1276                                      , jobs = jobs
  1277                                      }
  1278                                      g
  1279                                      |> (\html ->
  1280                                              ( html :: htmlList
  1281                                              , totalOffset
  1282                                                  + layout.height
  1283                                                  + PipelineGridConstants.headerHeight
  1284                                                  + PipelineGridConstants.padding
  1285                                              )
  1286                                         )
  1287                              )
  1288                              ( [], offsetHeight )
  1289                              >> Tuple.first
  1290                              >> List.reverse
  1291                     )
  1292      in
  1293      if
  1294          (params.pipelines /= Nothing)
  1295              && List.isEmpty groupViews
  1296              && not (String.isEmpty params.query)
  1297      then
  1298          [ noResultsView params.query ]
  1299  
  1300      else
  1301          headerView ++ groupViews