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  }