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)