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  }