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