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