github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/BuildButton.tsx (about)

     1  import React, { useCallback } from "react"
     2  import styled from "styled-components"
     3  import { Tags } from "./analytics"
     4  import { ApiButton } from "./ApiButton"
     5  import { ReactComponent as StartBuildButtonManualSvg } from "./assets/svg/start-build-button-manual.svg"
     6  import { ReactComponent as StartBuildButtonSvg } from "./assets/svg/start-build-button.svg"
     7  import { ReactComponent as StopBuildButtonSvg } from "./assets/svg/stop-build-button.svg"
     8  import { InstrumentedButton } from "./instrumentedComponents"
     9  import TiltTooltip from "./Tooltip"
    10  import { BuildButtonTooltip, buildButtonTooltip } from "./trigger"
    11  import { TriggerMode, UIButton } from "./types"
    12  
    13  export type StartBuildButtonProps = {
    14    isBuilding: boolean
    15    hasBuilt: boolean
    16    triggerMode: TriggerMode
    17    isSelected?: boolean
    18    hasPendingChanges: boolean
    19    isQueued: boolean
    20    onStartBuild: () => void
    21    analyticsTags: Tags
    22    className?: string
    23  }
    24  
    25  type StopBuildButtonProps = {
    26    stopBuildButton?: UIButton
    27  }
    28  
    29  export type BuildButtonProps = StartBuildButtonProps & StopBuildButtonProps
    30  
    31  function BuildButton(props: BuildButtonProps) {
    32    const { stopBuildButton, ...startBuildButtonProps } = props
    33    if (props.isBuilding) {
    34      if (!stopBuildButton) {
    35        return null
    36      }
    37      let classes = [props.className, "stop-button", "is-clickable"]
    38      if (props.isSelected) {
    39        classes.push("is-selected")
    40      }
    41  
    42      return (
    43        <TiltTooltip title={BuildButtonTooltip.Stop}>
    44          <BuildButtonCursorWrapper>
    45            <ApiButton uiButton={stopBuildButton} className={classes.join(" ")}>
    46              <StopBuildButtonSvg className="icon" />
    47            </ApiButton>
    48          </BuildButtonCursorWrapper>
    49        </TiltTooltip>
    50      )
    51    } else {
    52      const classes = [props.className, "start-button"]
    53      return (
    54        <StartBuildButton
    55          {...startBuildButtonProps}
    56          className={classes.join(" ")}
    57        />
    58      )
    59    }
    60  }
    61  
    62  // A wrapper to receive pointer events so that we get cursor and tooltip when disabled
    63  // https://mui.com/components/tooltips/#disabled-elements
    64  const BuildButtonCursorWrapper = styled.div`
    65    display: inline-block;
    66    cursor: not-allowed;
    67    // keep the button in front of the "in-progress" barber pole animation
    68    z-index: 1;
    69    .is-clickable {
    70      cursor: pointer;
    71    }
    72  `
    73  
    74  function StartBuildButton(props: StartBuildButtonProps) {
    75    let isManual =
    76      props.triggerMode === TriggerMode.TriggerModeManual ||
    77      props.triggerMode === TriggerMode.TriggerModeManualWithAutoInit
    78    let isAutoInit =
    79      props.triggerMode === TriggerMode.TriggerModeAuto ||
    80      props.triggerMode === TriggerMode.TriggerModeManualWithAutoInit
    81  
    82    // clickable (i.e. start build button will appear) if it doesn't already have some kind of pending / active build
    83    let clickable =
    84      !props.isQueued && // already queued for manual run
    85      !props.isBuilding && // currently building
    86      !(isAutoInit && !props.hasBuilt) // waiting to perform its initial build
    87  
    88    let isEmphasized = false
    89    if (clickable) {
    90      if (props.hasPendingChanges && isManual) {
    91        isEmphasized = true
    92      } else if (!props.hasBuilt && !isAutoInit) {
    93        isEmphasized = true
    94      }
    95    }
    96  
    97    let onClick = useCallback(
    98      (e: React.MouseEvent<HTMLButtonElement>) => {
    99        // In the sidebar, StartBuildButton is nested in a link,
   100        // and preventDefault is the standard way to cancel the navigation.
   101        e.preventDefault()
   102  
   103        // stopPropagation prevents the overview card from opening.
   104        e.stopPropagation()
   105  
   106        props.onStartBuild()
   107      },
   108      [props.onStartBuild]
   109    )
   110  
   111    let classes = [props.className]
   112    if (props.isSelected) {
   113      classes.push("is-selected")
   114    }
   115    if (clickable) {
   116      classes.push("is-clickable")
   117    }
   118    if (props.isQueued) {
   119      classes.push("is-queued")
   120    }
   121    if (isManual) {
   122      classes.push("is-manual")
   123    }
   124    if (isEmphasized) {
   125      classes.push("is-emphasized")
   126    }
   127    if (props.isBuilding) {
   128      classes.push("is-building")
   129    }
   130    const tooltip = buildButtonTooltip(clickable, isEmphasized, props.isQueued)
   131    // Set the tooltip key to the tooltip message so that each message is a different "component" and enterNextDelay
   132    // applies when the message changes.
   133    // Otherwise, we often display a flicker of "resource is already queued!" after clicking "start build" before
   134    // the "stop build" button appears.
   135    return (
   136      <TiltTooltip title={tooltip} key={tooltip}>
   137        <BuildButtonCursorWrapper
   138          className={clickable ? "is-clickable" : undefined}
   139        >
   140          <InstrumentedButton
   141            onClick={onClick}
   142            className={classes.join(" ")}
   143            disabled={!clickable}
   144            aria-label={tooltip}
   145            analyticsName={"ui.web.triggerResource"}
   146            analyticsTags={props.analyticsTags}
   147          >
   148            {isEmphasized ? (
   149              <StartBuildButtonManualSvg
   150                role="presentation"
   151                className="icon"
   152                data-testid="build-manual-icon"
   153              />
   154            ) : (
   155              <StartBuildButtonSvg
   156                role="presentation"
   157                className="icon"
   158                data-testid="build-auto-icon"
   159              />
   160            )}
   161          </InstrumentedButton>
   162        </BuildButtonCursorWrapper>
   163      </TiltTooltip>
   164    )
   165  }
   166  
   167  export default React.memo(BuildButton)