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