github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/web/elm/src/Dashboard/Group.elm (about) 1 module Dashboard.Group exposing 2 ( PipelineIndex 3 , hdView 4 , ordering 5 , pipelineNotSetView 6 , view 7 , viewFavoritePipelines 8 ) 9 10 import Concourse 11 import Dashboard.Group.Models exposing (Group, Pipeline) 12 import Dashboard.Group.Tag as Tag 13 import Dashboard.Models exposing (DragState(..), DropState(..)) 14 import Dashboard.Pipeline as Pipeline 15 import Dashboard.PipelineGrid as PipelineGrid 16 import Dashboard.PipelineGrid.Constants as PipelineGridConstants 17 import Dashboard.Styles as Styles 18 import Dict exposing (Dict) 19 import HoverState 20 import Html exposing (Html) 21 import Html.Attributes exposing (attribute, class, classList, draggable, id, style) 22 import Html.Events exposing (on, onMouseOut, onMouseOver, preventDefaultOn, stopPropagationOn) 23 import Html.Keyed 24 import Json.Decode 25 import Maybe.Extra 26 import Message.Effects as Effects 27 import Message.Message exposing (DomID(..), DropTarget(..), Message(..), PipelinesSection(..)) 28 import Ordering exposing (Ordering) 29 import Set exposing (Set) 30 import Time 31 import UserState exposing (UserState(..)) 32 import Views.Spinner as Spinner 33 import Views.Styles 34 35 36 ordering : { a | userState : UserState } -> Ordering Group 37 ordering session = 38 Ordering.byFieldWith Tag.ordering (tag session) 39 |> Ordering.breakTiesWith (Ordering.byField .teamName) 40 41 42 type alias PipelineIndex = 43 Int 44 45 46 view : 47 { a | userState : UserState, favoritedPipelines : Set Concourse.DatabaseID } 48 -> 49 { dragState : DragState 50 , dropState : DropState 51 , now : Maybe Time.Posix 52 , hovered : HoverState.HoverState 53 , pipelineRunningKeyframes : String 54 , pipelinesWithResourceErrors : Set ( String, String ) 55 , pipelineLayers : Dict ( String, String ) (List (List Concourse.JobIdentifier)) 56 , pipelineCards : List PipelineGrid.PipelineCard 57 , dropAreas : List PipelineGrid.DropArea 58 , groupCardsHeight : Float 59 , pipelineJobs : Dict ( String, String ) (List Concourse.JobIdentifier) 60 , jobs : Dict ( String, String, String ) Concourse.Job 61 } 62 -> Group 63 -> Html Message 64 view session params g = 65 let 66 pipelineCardViews = 67 if List.isEmpty params.pipelineCards then 68 [ ( "not-set", Pipeline.pipelineNotSetView ) ] 69 70 else 71 params.pipelineCards 72 |> List.map 73 (\{ bounds, pipeline } -> 74 pipelineCardView session 75 params 76 AllPipelinesSection 77 { bounds = bounds, pipeline = pipeline } 78 g.teamName 79 |> (\html -> ( String.fromInt pipeline.id, html )) 80 ) 81 82 dropAreaViews = 83 params.dropAreas 84 |> List.map 85 (\{ bounds, target } -> 86 pipelineDropAreaView params.dragState g.teamName bounds target 87 ) 88 in 89 Html.div 90 [ id <| Effects.toHtmlID <| DashboardGroup g.teamName 91 , class "dashboard-team-group" 92 , attribute "data-team-name" g.teamName 93 ] 94 [ Html.div 95 [ style "display" "flex" 96 , style "align-items" "center" 97 , style "margin-bottom" (String.fromInt PipelineGridConstants.padding ++ "px") 98 , class <| .sectionHeaderClass Effects.stickyHeaderConfig 99 ] 100 (Html.div 101 [ class "dashboard-team-name" 102 , style "font-weight" Views.Styles.fontWeightBold 103 ] 104 [ Html.text g.teamName ] 105 :: (Maybe.Extra.toList <| 106 Maybe.map (Tag.view False) (tag session g) 107 ) 108 ++ (if params.dropState == DroppingWhileApiRequestInFlight g.teamName then 109 [ Spinner.spinner { sizePx = 20, margin = "0 0 0 10px" } ] 110 111 else 112 [] 113 ) 114 ) 115 , Html.Keyed.node "div" 116 [ class <| .sectionBodyClass Effects.stickyHeaderConfig 117 , style "position" "relative" 118 , style "height" <| String.fromFloat params.groupCardsHeight ++ "px" 119 ] 120 (pipelineCardViews 121 ++ [ ( "drop-areas", Html.div [ style "position" "absolute" ] dropAreaViews ) ] 122 ) 123 ] 124 125 126 viewFavoritePipelines : 127 { a | userState : UserState, favoritedPipelines : Set Concourse.DatabaseID } 128 -> 129 { dragState : DragState 130 , dropState : DropState 131 , now : Maybe Time.Posix 132 , hovered : HoverState.HoverState 133 , pipelineRunningKeyframes : String 134 , pipelinesWithResourceErrors : Set ( String, String ) 135 , pipelineLayers : Dict ( String, String ) (List (List Concourse.JobIdentifier)) 136 , pipelineCards : List PipelineGrid.PipelineCard 137 , headers : List PipelineGrid.Header 138 , groupCardsHeight : Float 139 , pipelineJobs : Dict ( String, String ) (List Concourse.JobIdentifier) 140 , jobs : Dict ( String, String, String ) Concourse.Job 141 } 142 -> Html Message 143 viewFavoritePipelines session params = 144 let 145 pipelineCardViews = 146 params.pipelineCards 147 |> List.map 148 (\{ bounds, pipeline } -> 149 pipelineCardView session 150 params 151 FavoritesSection 152 { bounds = bounds, pipeline = pipeline } 153 pipeline.teamName 154 |> (\html -> ( String.fromInt pipeline.id, html )) 155 ) 156 157 headerViews = 158 params.headers 159 |> List.map 160 (\{ bounds, header } -> 161 headerView bounds header 162 ) 163 in 164 Html.Keyed.node "div" 165 [ id <| "dashboard-favorite-pipelines" 166 , style "position" "relative" 167 , style "height" <| String.fromFloat params.groupCardsHeight ++ "px" 168 ] 169 (pipelineCardViews 170 ++ [ ( "headers" 171 , Html.div 172 [ style "position" "absolute" 173 , class "headers" 174 ] 175 headerViews 176 ) 177 ] 178 ) 179 180 181 tag : { a | userState : UserState } -> Group -> Maybe Tag.Tag 182 tag { userState } g = 183 case userState of 184 UserStateLoggedIn user -> 185 Tag.tag user g.teamName 186 187 _ -> 188 Nothing 189 190 191 hdView : 192 { pipelineRunningKeyframes : String 193 , pipelinesWithResourceErrors : Set ( String, String ) 194 , pipelineJobs : Dict ( String, String ) (List Concourse.JobIdentifier) 195 , jobs : Dict ( String, String, String ) Concourse.Job 196 } 197 -> { a | userState : UserState } 198 -> Group 199 -> List (Html Message) 200 hdView { pipelineRunningKeyframes, pipelinesWithResourceErrors, pipelineJobs, jobs } session g = 201 let 202 orderedPipelines = 203 g.pipelines 204 205 header = 206 Html.div 207 [ class "dashboard-team-name" ] 208 [ Html.text g.teamName ] 209 :: (Maybe.Extra.toList <| Maybe.map (Tag.view True) (tag session g)) 210 211 teamPipelines = 212 if List.isEmpty orderedPipelines then 213 [ pipelineNotSetView ] 214 215 else 216 orderedPipelines 217 |> List.map 218 (\p -> 219 Pipeline.hdPipelineView 220 { pipeline = p 221 , pipelineRunningKeyframes = pipelineRunningKeyframes 222 , resourceError = 223 pipelinesWithResourceErrors 224 |> Set.member ( p.teamName, p.name ) 225 , existingJobs = 226 pipelineJobs 227 |> Dict.get ( p.teamName, p.name ) 228 |> Maybe.withDefault [] 229 |> List.filterMap (lookupJob jobs) 230 } 231 ) 232 in 233 case teamPipelines of 234 [] -> 235 header 236 237 p :: ps -> 238 -- Wrap the team name and the first pipeline together so 239 -- the team name is not the last element in a column 240 Html.div 241 (class "dashboard-team-name-wrapper" :: Styles.teamNameHd) 242 (header ++ [ p ]) 243 :: ps 244 245 246 pipelineNotSetView : Html Message 247 pipelineNotSetView = 248 Html.div 249 [ class "card" ] 250 [ Html.div 251 Styles.noPipelineCardHd 252 [ Html.div 253 Styles.noPipelineCardTextHd 254 [ Html.text "no pipelines set" ] 255 ] 256 ] 257 258 259 lookupJob : Dict ( String, String, String ) Concourse.Job -> Concourse.JobIdentifier -> Maybe Concourse.Job 260 lookupJob jobs jobId = 261 jobs 262 |> Dict.get ( jobId.teamName, jobId.pipelineName, jobId.jobName ) 263 264 265 pipelineCardView : 266 { a | userState : UserState, favoritedPipelines : Set Concourse.DatabaseID } 267 -> 268 { b 269 | dragState : DragState 270 , dropState : DropState 271 , now : Maybe Time.Posix 272 , hovered : HoverState.HoverState 273 , pipelineRunningKeyframes : String 274 , pipelinesWithResourceErrors : Set ( String, String ) 275 , pipelineLayers : Dict ( String, String ) (List (List Concourse.JobIdentifier)) 276 , pipelineJobs : Dict ( String, String ) (List Concourse.JobIdentifier) 277 , jobs : Dict ( String, String, String ) Concourse.Job 278 } 279 -> PipelinesSection 280 -> 281 { bounds : PipelineGrid.Bounds 282 , pipeline : Pipeline 283 } 284 -> String 285 -> Html Message 286 pipelineCardView session params section { bounds, pipeline } teamName = 287 Html.div 288 ([ class "pipeline-wrapper" 289 , style "position" "absolute" 290 , style "transform" 291 ("translate(" 292 ++ String.fromFloat bounds.x 293 ++ "px," 294 ++ String.fromFloat bounds.y 295 ++ "px)" 296 ) 297 , style 298 "width" 299 (String.fromFloat bounds.width 300 ++ "px" 301 ) 302 , style "height" 303 (String.fromFloat bounds.height 304 ++ "px" 305 ) 306 , onMouseOver <| 307 Hover <| 308 Just <| 309 PipelineWrapper 310 { pipelineName = pipeline.name 311 , teamName = pipeline.teamName 312 } 313 , onMouseOut <| Hover Nothing 314 ] 315 ++ (if params.dragState /= NotDragging then 316 [ style "transition" "transform 0.2s ease-in-out" ] 317 318 else 319 [] 320 ) 321 ++ (let 322 hoverStyle id = 323 if 324 (id.pipelineName == pipeline.name) 325 && (id.teamName == pipeline.teamName) 326 then 327 [ style "z-index" "1" ] 328 329 else 330 [] 331 in 332 case HoverState.hoveredElement params.hovered of 333 Just (JobPreview _ jobID) -> 334 hoverStyle jobID 335 336 Just (PipelineWrapper pipelineID) -> 337 hoverStyle pipelineID 338 339 _ -> 340 [] 341 ) 342 ) 343 [ Html.div 344 ([ class "card" 345 , style "width" "100%" 346 , style "height" "100%" 347 , attribute "data-pipeline-name" pipeline.name 348 ] 349 ++ (if section == AllPipelinesSection && not pipeline.stale then 350 [ attribute 351 "ondragstart" 352 "event.dataTransfer.setData('text/plain', '');" 353 , draggable "true" 354 , on "dragstart" 355 (Json.Decode.succeed (DragStart pipeline.teamName pipeline.name)) 356 , on "dragend" (Json.Decode.succeed DragEnd) 357 ] 358 359 else 360 [] 361 ) 362 ++ (if params.dragState == Dragging pipeline.teamName pipeline.name then 363 [ style "width" "0" 364 , style "margin" "0 12.5px" 365 , style "overflow" "hidden" 366 ] 367 368 else 369 [] 370 ) 371 ++ (if params.dropState == DroppingWhileApiRequestInFlight teamName then 372 [ style "opacity" "0.45", style "pointer-events" "none" ] 373 374 else 375 [ style "opacity" "1" ] 376 ) 377 ) 378 [ Pipeline.pipelineView 379 { now = params.now 380 , pipeline = pipeline 381 , resourceError = 382 params.pipelinesWithResourceErrors 383 |> Set.member ( pipeline.teamName, pipeline.name ) 384 , existingJobs = 385 params.pipelineJobs 386 |> Dict.get ( pipeline.teamName, pipeline.name ) 387 |> Maybe.withDefault [] 388 |> List.filterMap (lookupJob params.jobs) 389 , layers = 390 params.pipelineLayers 391 |> Dict.get ( pipeline.teamName, pipeline.name ) 392 |> Maybe.withDefault [] 393 |> List.map (List.filterMap <| lookupJob params.jobs) 394 , hovered = params.hovered 395 , pipelineRunningKeyframes = params.pipelineRunningKeyframes 396 , userState = session.userState 397 , favoritedPipelines = session.favoritedPipelines 398 , section = section 399 } 400 ] 401 ] 402 403 404 pipelineDropAreaView : DragState -> String -> PipelineGrid.Bounds -> DropTarget -> Html Message 405 pipelineDropAreaView dragState name { x, y, width, height } target = 406 let 407 active = 408 case dragState of 409 Dragging team _ -> 410 team == name 411 412 _ -> 413 False 414 in 415 Html.div 416 [ classList 417 [ ( "drop-area", True ) 418 , ( "active", active ) 419 ] 420 , style "position" "absolute" 421 , style "transform" <| 422 "translate(" 423 ++ String.fromFloat x 424 ++ "px," 425 ++ String.fromFloat y 426 ++ "px)" 427 , style "width" <| String.fromFloat width ++ "px" 428 , style "height" <| String.fromFloat height ++ "px" 429 , on "dragenter" (Json.Decode.succeed (DragOver target)) 430 431 -- preventDefault is required so that the card will not appear to 432 -- "float" or "snap" back to its original position when dropped. 433 , preventDefaultOn "dragover" (Json.Decode.succeed ( DragOver target, True )) 434 , stopPropagationOn "drop" (Json.Decode.succeed ( DragEnd, True )) 435 ] 436 [] 437 438 439 headerView : PipelineGrid.Bounds -> String -> Html Message 440 headerView { x, y, width, height } header = 441 Html.div 442 [ class "header" 443 , style "position" "absolute" 444 , style "transform" <| 445 "translate(" 446 ++ String.fromFloat x 447 ++ "px," 448 ++ String.fromFloat y 449 ++ "px)" 450 , style "width" <| String.fromFloat width ++ "px" 451 , style "height" <| String.fromFloat height ++ "px" 452 , style "font-size" "18px" 453 , style "padding-left" "12.5px" 454 , style "padding-top" "17.5px" 455 , style "box-sizing" "border-box" 456 , style "text-overflow" "ellipsis" 457 , style "overflow" "hidden" 458 , style "white-space" "nowrap" 459 , style "font-weight" Views.Styles.fontWeightBold 460 ] 461 [ Html.text header ]