github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/StarredResourceBar.tsx (about) 1 import React from "react" 2 import { useHistory } from "react-router" 3 import styled from "styled-components" 4 import { ReactComponent as DisabledSvg } from "./assets/svg/not-allowed.svg" 5 import { ReactComponent as StarSvg } from "./assets/svg/star.svg" 6 import { InstrumentedButton } from "./instrumentedComponents" 7 import { useLogStore } from "./LogStore" 8 import { usePathBuilder } from "./PathBuilder" 9 import { 10 ClassNameFromResourceStatus, 11 disabledResourceStyleMixin, 12 } from "./ResourceStatus" 13 import { useStarredResources } from "./StarredResourcesContext" 14 import { buildStatus, combinedStatus, runtimeStatus } from "./status" 15 import { 16 AnimDuration, 17 barberpole, 18 Color, 19 ColorAlpha, 20 ColorRGBA, 21 Font, 22 FontSize, 23 Glow, 24 mixinResetButtonStyle, 25 SizeUnit, 26 } from "./style-helpers" 27 import TiltTooltip from "./Tooltip" 28 import { ResourceName, ResourceStatus } from "./types" 29 30 export const StarredResourceLabel = styled.div` 31 max-width: ${SizeUnit(4.5)}; 32 overflow: hidden; 33 text-overflow: ellipsis; 34 white-space: nowrap; 35 display: inline-block; 36 37 font-size: ${FontSize.small}; 38 font-family: ${Font.monospace}; 39 40 user-select: none; 41 ` 42 const ResourceButton = styled(InstrumentedButton)` 43 ${mixinResetButtonStyle}; 44 color: inherit; 45 display: flex; 46 ` 47 const StarIcon = styled(StarSvg)` 48 height: ${SizeUnit(0.5)}; 49 width: ${SizeUnit(0.5)}; 50 ` 51 52 const DisabledIcon = styled(DisabledSvg)` 53 height: ${SizeUnit(0.5)}; 54 margin-right: ${SizeUnit(1 / 8)}; 55 width: ${SizeUnit(0.5)}; 56 ` 57 58 export const StarButton = styled(InstrumentedButton)` 59 ${mixinResetButtonStyle}; 60 ${StarIcon} { 61 fill: ${Color.gray50}; 62 } 63 &:hover { 64 ${StarIcon} { 65 fill: ${Color.grayLightest}; 66 } 67 } 68 ` 69 const StarredResourceRoot = styled.div` 70 border-width: 1px; 71 border-style: solid; 72 border-radius: ${SizeUnit(0.125)}; 73 cursor: pointer; 74 display: inline-flex; 75 align-items: center; 76 background-color: ${Color.gray30}; 77 padding-top: ${SizeUnit(0.125)}; 78 padding-bottom: ${SizeUnit(0.125)}; 79 position: relative; // Anchor the .isBuilding::after pseudo-element 80 81 &:hover { 82 background-color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)}; 83 } 84 85 &.isWarning { 86 color: ${Color.yellow}; 87 border-color: ${ColorRGBA(Color.yellow, ColorAlpha.translucent)}; 88 } 89 &.isHealthy { 90 color: ${Color.green}; 91 border-color: ${ColorRGBA(Color.green, ColorAlpha.translucent)}; 92 } 93 &.isUnhealthy { 94 color: ${Color.red}; 95 border-color: ${ColorRGBA(Color.red, ColorAlpha.translucent)}; 96 } 97 &.isBuilding { 98 color: ${ColorRGBA(Color.white, ColorAlpha.translucent)}; 99 } 100 .isSelected &.isBuilding { 101 color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)}; 102 } 103 &.isPending { 104 color: ${ColorRGBA(Color.white, ColorAlpha.translucent)}; 105 animation: ${Glow.white} 2s linear infinite; 106 } 107 .isSelected &.isPending { 108 color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)}; 109 animation: ${Glow.dark} 2s linear infinite; 110 } 111 &.isNone { 112 color: ${Color.gray40}; 113 transition: border-color ${AnimDuration.default} linear; 114 } 115 &.isSelected { 116 background-color: ${Color.white}; 117 color: ${Color.gray30}; 118 } 119 120 &.isBuilding::after { 121 content: ""; 122 position: absolute; 123 pointer-events: none; 124 width: 100%; 125 top: 0; 126 bottom: 0; 127 background: repeating-linear-gradient( 128 225deg, 129 ${ColorRGBA(Color.gray50, ColorAlpha.translucent)}, 130 ${ColorRGBA(Color.gray50, ColorAlpha.translucent)} 1px, 131 ${ColorRGBA(Color.black, 0)} 1px, 132 ${ColorRGBA(Color.black, 0)} 6px 133 ); 134 background-size: 200% 200%; 135 animation: ${barberpole} 8s linear infinite; 136 } 137 138 &.isDisabled { 139 border-color: ${ColorRGBA(Color.gray60, ColorAlpha.translucent)}; 140 141 &:not(.isSelected) { 142 color: ${Color.gray60}; 143 } 144 145 ${StarredResourceLabel} { 146 ${disabledResourceStyleMixin} 147 } 148 } 149 150 /* implement margins as padding on child buttons, to ensure the buttons consume the 151 whole bounding box */ 152 ${StarButton} { 153 margin-left: ${SizeUnit(0.25)}; 154 padding-right: ${SizeUnit(0.25)}; 155 } 156 ${ResourceButton} { 157 padding-left: ${SizeUnit(0.25)}; 158 } 159 &.isStarredAggregate ${ResourceButton} { 160 padding-right: ${SizeUnit(0.25)}; 161 } 162 ` 163 const StarredResourceBarRoot = styled.section` 164 padding-left: ${SizeUnit(0.5)}; 165 padding-right: ${SizeUnit(0.5)}; 166 padding-top: ${SizeUnit(0.25)}; 167 padding-bottom: ${SizeUnit(0.25)}; 168 margin-bottom: ${SizeUnit(0.25)}; 169 background-color: ${Color.grayDarker}; 170 display: flex; 171 172 ${StarredResourceRoot} { 173 margin-right: ${SizeUnit(0.25)}; 174 } 175 ` 176 177 export type ResourceNameAndStatus = { 178 name: string 179 status: ResourceStatus 180 } 181 export type StarredResourceBarProps = { 182 selectedResource?: string 183 resources: ResourceNameAndStatus[] 184 unstar: (name: string) => void 185 } 186 187 export function StarredResource(props: { 188 resource: ResourceNameAndStatus 189 unstar: (name: string) => void 190 isSelected: boolean 191 }) { 192 const pb = usePathBuilder() 193 const href = pb.encpath`/r/${props.resource.name}/overview` 194 const history = useHistory() 195 const onClick = (e: any) => { 196 props.unstar(props.resource.name) 197 e.preventDefault() 198 e.stopPropagation() 199 } 200 201 let classes = [ClassNameFromResourceStatus(props.resource.status)] 202 if (props.isSelected) { 203 classes.push("isSelected") 204 } 205 206 const starredResourceIcon = 207 props.resource.status === ResourceStatus.Disabled ? ( 208 <DisabledIcon role="presentation" /> 209 ) : null 210 211 return ( 212 <TiltTooltip title={props.resource.name}> 213 <StarredResourceRoot className={classes.join(" ")}> 214 <ResourceButton 215 onClick={() => { 216 history.push(href) 217 }} 218 analyticsName="ui.web.starredResourceBarResource" 219 > 220 {starredResourceIcon} 221 <StarredResourceLabel>{props.resource.name}</StarredResourceLabel> 222 </ResourceButton> 223 <StarButton 224 onClick={onClick} 225 analyticsName="ui.web.starredResourceBarUnstar" 226 aria-label={`Unstar ${props.resource.name}`} 227 > 228 <StarIcon /> 229 </StarButton> 230 </StarredResourceRoot> 231 </TiltTooltip> 232 ) 233 } 234 235 function StarredResourceAggregate(props: { isSelected: boolean }) { 236 const pb = usePathBuilder() 237 const href = pb.encpath`/r/${ResourceName.starred}/overview` 238 const history = useHistory() 239 let classes = [ 240 ClassNameFromResourceStatus(ResourceStatus.Healthy), 241 "isStarredAggregate", 242 ] 243 if (props.isSelected) { 244 classes.push("isSelected") 245 } 246 247 return ( 248 <TiltTooltip title={"View starred resource logs"}> 249 <StarredResourceRoot className={classes.join(" ")}> 250 <ResourceButton 251 onClick={() => { 252 history.push(href) 253 }} 254 analyticsName="ui.web.starredResourcesAggregatedLogs" 255 > 256 <StarredResourceLabel>All Starred</StarredResourceLabel> 257 </ResourceButton> 258 </StarredResourceRoot> 259 </TiltTooltip> 260 ) 261 } 262 263 export default function StarredResourceBar(props: StarredResourceBarProps) { 264 return ( 265 <StarredResourceBarRoot aria-label="Starred resources"> 266 {props.resources.length ? ( 267 <StarredResourceAggregate 268 isSelected={ResourceName.starred === props.selectedResource} 269 /> 270 ) : null} 271 {props.resources.map((r) => ( 272 <StarredResource 273 resource={r} 274 key={r.name} 275 unstar={props.unstar} 276 isSelected={r.name === props.selectedResource} 277 /> 278 ))} 279 </StarredResourceBarRoot> 280 ) 281 } 282 283 // translates the view to a pared-down model so that `StarredResourceBar` can have a simple API for testing. 284 export function starredResourcePropsFromView( 285 view: Proto.webviewView, 286 selectedResource: string 287 ): StarredResourceBarProps { 288 const ls = useLogStore() 289 const starContext = useStarredResources() 290 const namesAndStatuses = (view?.uiResources || []).flatMap((r) => { 291 let name = r.metadata?.name 292 if (name && starContext.starredResources.includes(name)) { 293 return [ 294 { 295 name: name, 296 status: combinedStatus(buildStatus(r, ls), runtimeStatus(r, ls)), 297 }, 298 ] 299 } else { 300 return [] 301 } 302 }) 303 return { 304 resources: namesAndStatuses, 305 unstar: starContext.unstarResource, 306 selectedResource: selectedResource, 307 } 308 }