github.com/tilt-dev/tilt@v0.36.0/web/src/StarredResourceBar.tsx (about) 1 import React from "react" 2 import { useNavigate } from "react-router-dom" 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 navigate = useNavigate() 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 navigate(href) 217 }} 218 > 219 {starredResourceIcon} 220 <StarredResourceLabel>{props.resource.name}</StarredResourceLabel> 221 </ResourceButton> 222 <StarButton 223 onClick={onClick} 224 aria-label={`Unstar ${props.resource.name}`} 225 > 226 <StarIcon /> 227 </StarButton> 228 </StarredResourceRoot> 229 </TiltTooltip> 230 ) 231 } 232 233 function StarredResourceAggregate(props: { isSelected: boolean }) { 234 const pb = usePathBuilder() 235 const href = pb.encpath`/r/${ResourceName.starred}/overview` 236 const navigate = useNavigate() 237 let classes = [ 238 ClassNameFromResourceStatus(ResourceStatus.Healthy), 239 "isStarredAggregate", 240 ] 241 if (props.isSelected) { 242 classes.push("isSelected") 243 } 244 245 return ( 246 <TiltTooltip title={"View starred resource logs"}> 247 <StarredResourceRoot className={classes.join(" ")}> 248 <ResourceButton 249 onClick={() => { 250 navigate(href) 251 }} 252 > 253 <StarredResourceLabel>All Starred</StarredResourceLabel> 254 </ResourceButton> 255 </StarredResourceRoot> 256 </TiltTooltip> 257 ) 258 } 259 260 export default function StarredResourceBar(props: StarredResourceBarProps) { 261 return ( 262 <StarredResourceBarRoot aria-label="Starred resources"> 263 {props.resources.length ? ( 264 <StarredResourceAggregate 265 isSelected={ResourceName.starred === props.selectedResource} 266 /> 267 ) : null} 268 {props.resources.map((r) => ( 269 <StarredResource 270 resource={r} 271 key={r.name} 272 unstar={props.unstar} 273 isSelected={r.name === props.selectedResource} 274 /> 275 ))} 276 </StarredResourceBarRoot> 277 ) 278 } 279 280 // translates the view to a pared-down model so that `StarredResourceBar` can have a simple API for testing. 281 export function starredResourcePropsFromView( 282 view: Proto.webviewView, 283 selectedResource: string 284 ): StarredResourceBarProps { 285 const ls = useLogStore() 286 const starContext = useStarredResources() 287 const namesAndStatuses = (view?.uiResources || []).flatMap((r) => { 288 let name = r.metadata?.name 289 if (name && starContext.starredResources.includes(name)) { 290 return [ 291 { 292 name: name, 293 status: combinedStatus(buildStatus(r, ls), runtimeStatus(r, ls)), 294 }, 295 ] 296 } else { 297 return [] 298 } 299 }) 300 return { 301 resources: namesAndStatuses, 302 unstar: starContext.unstarResource, 303 selectedResource: selectedResource, 304 } 305 }