starling-devex 0.1.2

Starling: a local dev orchestrator with a central daemon, shared named-URL proxy, and a k9s-style TUI (a Rust port of Tilt + portless)
import React from "react"
import { useNavigate } from "react-router-dom"
import styled from "styled-components"
import { ReactComponent as DisabledSvg } from "./assets/svg/not-allowed.svg"
import { ReactComponent as StarSvg } from "./assets/svg/star.svg"
import { InstrumentedButton } from "./instrumentedComponents"
import { useLogStore } from "./LogStore"
import { usePathBuilder } from "./PathBuilder"
import {
  ClassNameFromResourceStatus,
  disabledResourceStyleMixin,
} from "./ResourceStatus"
import { useStarredResources } from "./StarredResourcesContext"
import { buildStatus, combinedStatus, runtimeStatus } from "./status"
import {
  AnimDuration,
  barberpole,
  Color,
  ColorAlpha,
  ColorRGBA,
  Font,
  FontSize,
  Glow,
  mixinResetButtonStyle,
  SizeUnit,
} from "./style-helpers"
import TiltTooltip from "./Tooltip"
import { ResourceName, ResourceStatus } from "./types"
import type { View } from "./webview"

export const StarredResourceLabel = styled.div`
  max-width: ${SizeUnit(4.5)};
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;

  font-size: ${FontSize.small};
  font-family: ${Font.monospace};

  user-select: none;
`
const ResourceButton = styled(InstrumentedButton)`
  ${mixinResetButtonStyle};
  color: inherit;
  display: flex;
`
const StarIcon = styled(StarSvg)`
  height: ${SizeUnit(0.5)};
  width: ${SizeUnit(0.5)};
`

const DisabledIcon = styled(DisabledSvg)`
  height: ${SizeUnit(0.5)};
  margin-right: ${SizeUnit(1 / 8)};
  width: ${SizeUnit(0.5)};
`

export const StarButton = styled(InstrumentedButton)`
  ${mixinResetButtonStyle};
  ${StarIcon} {
    fill: ${Color.gray50};
  }
  &:hover {
    ${StarIcon} {
      fill: ${Color.grayLightest};
    }
  }
`
const StarredResourceRoot = styled.div`
  border-width: 1px;
  border-style: solid;
  border-radius: ${SizeUnit(0.125)};
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  background-color: ${Color.gray30};
  padding-top: ${SizeUnit(0.125)};
  padding-bottom: ${SizeUnit(0.125)};
  position: relative; // Anchor the .isBuilding::after pseudo-element

  &:hover {
    background-color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)};
  }

  &.isWarning {
    color: ${Color.yellow};
    border-color: ${ColorRGBA(Color.yellow, ColorAlpha.translucent)};
  }
  &.isHealthy {
    color: ${Color.green};
    border-color: ${ColorRGBA(Color.green, ColorAlpha.translucent)};
  }
  &.isUnhealthy {
    color: ${Color.red};
    border-color: ${ColorRGBA(Color.red, ColorAlpha.translucent)};
  }
  &.isBuilding {
    color: ${ColorRGBA(Color.white, ColorAlpha.translucent)};
  }
  .isSelected &.isBuilding {
    color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)};
  }
  &.isPending {
    color: ${ColorRGBA(Color.white, ColorAlpha.translucent)};
    animation: ${Glow.white} 2s linear infinite;
  }
  .isSelected &.isPending {
    color: ${ColorRGBA(Color.gray30, ColorAlpha.translucent)};
    animation: ${Glow.dark} 2s linear infinite;
  }
  &.isNone {
    color: ${Color.gray40};
    transition: border-color ${AnimDuration.default} linear;
  }
  &.isSelected {
    background-color: ${Color.white};
    color: ${Color.gray30};
  }

  &.isBuilding::after {
    content: "";
    position: absolute;
    pointer-events: none;
    width: 100%;
    top: 0;
    bottom: 0;
    background: repeating-linear-gradient(
      225deg,
      ${ColorRGBA(Color.gray50, ColorAlpha.translucent)},
      ${ColorRGBA(Color.gray50, ColorAlpha.translucent)} 1px,
      ${ColorRGBA(Color.black, 0)} 1px,
      ${ColorRGBA(Color.black, 0)} 6px
    );
    background-size: 200% 200%;
    animation: ${barberpole} 8s linear infinite;
  }

  &.isDisabled {
    border-color: ${ColorRGBA(Color.gray60, ColorAlpha.translucent)};

    &:not(.isSelected) {
      color: ${Color.gray60};
    }

    ${StarredResourceLabel} {
      ${disabledResourceStyleMixin}
    }
  }

  /* implement margins as padding on child buttons, to ensure the buttons consume the
     whole bounding box */
  ${StarButton} {
    margin-left: ${SizeUnit(0.25)};
    padding-right: ${SizeUnit(0.25)};
  }
  ${ResourceButton} {
    padding-left: ${SizeUnit(0.25)};
  }
  &.isStarredAggregate ${ResourceButton} {
    padding-right: ${SizeUnit(0.25)};
  }
`
const StarredResourceBarRoot = styled.section`
  padding-left: ${SizeUnit(0.5)};
  padding-right: ${SizeUnit(0.5)};
  padding-top: ${SizeUnit(0.25)};
  padding-bottom: ${SizeUnit(0.25)};
  margin-bottom: ${SizeUnit(0.25)};
  background-color: ${Color.grayDarker};
  display: flex;

  ${StarredResourceRoot} {
    margin-right: ${SizeUnit(0.25)};
  }
`

export type ResourceNameAndStatus = {
  name: string
  status: ResourceStatus
}
export type StarredResourceBarProps = {
  selectedResource?: string
  resources: ResourceNameAndStatus[]
  unstar: (name: string) => void
}

export function StarredResource(props: {
  resource: ResourceNameAndStatus
  unstar: (name: string) => void
  isSelected: boolean
}) {
  const pb = usePathBuilder()
  const href = pb.encpath`/r/${props.resource.name}/overview`
  const navigate = useNavigate()
  const onClick = (e: any) => {
    props.unstar(props.resource.name)
    e.preventDefault()
    e.stopPropagation()
  }

  let classes = [ClassNameFromResourceStatus(props.resource.status)]
  if (props.isSelected) {
    classes.push("isSelected")
  }

  const starredResourceIcon =
    props.resource.status === ResourceStatus.Disabled ? (
      <DisabledIcon role="presentation" />
    ) : null

  return (
    <TiltTooltip title={props.resource.name}>
      <StarredResourceRoot className={classes.join(" ")}>
        <ResourceButton
          onClick={() => {
            navigate(href)
          }}
        >
          {starredResourceIcon}
          <StarredResourceLabel>{props.resource.name}</StarredResourceLabel>
        </ResourceButton>
        <StarButton
          onClick={onClick}
          aria-label={`Unstar ${props.resource.name}`}
        >
          <StarIcon />
        </StarButton>
      </StarredResourceRoot>
    </TiltTooltip>
  )
}

function StarredResourceAggregate(props: { isSelected: boolean }) {
  const pb = usePathBuilder()
  const href = pb.encpath`/r/${ResourceName.starred}/overview`
  const navigate = useNavigate()
  let classes = [
    ClassNameFromResourceStatus(ResourceStatus.Healthy),
    "isStarredAggregate",
  ]
  if (props.isSelected) {
    classes.push("isSelected")
  }

  return (
    <TiltTooltip title={"View starred resource logs"}>
      <StarredResourceRoot className={classes.join(" ")}>
        <ResourceButton
          onClick={() => {
            navigate(href)
          }}
        >
          <StarredResourceLabel>All Starred</StarredResourceLabel>
        </ResourceButton>
      </StarredResourceRoot>
    </TiltTooltip>
  )
}

export default function StarredResourceBar(props: StarredResourceBarProps) {
  return (
    <StarredResourceBarRoot aria-label="Starred resources">
      {props.resources.length ? (
        <StarredResourceAggregate
          isSelected={ResourceName.starred === props.selectedResource}
        />
      ) : null}
      {props.resources.map((r) => (
        <StarredResource
          resource={r}
          key={r.name}
          unstar={props.unstar}
          isSelected={r.name === props.selectedResource}
        />
      ))}
    </StarredResourceBarRoot>
  )
}

// translates the view to a pared-down model so that `StarredResourceBar` can have a simple API for testing.
export function starredResourcePropsFromView(
  view: View,
  selectedResource: string
): StarredResourceBarProps {
  const ls = useLogStore()
  const starContext = useStarredResources()
  const namesAndStatuses = (view?.uiResources || []).flatMap((r) => {
    let name = r.metadata?.name
    if (name && starContext.starredResources.includes(name)) {
      return [
        {
          name: name,
          status: combinedStatus(buildStatus(r, ls), runtimeStatus(r, ls)),
        },
      ]
    } else {
      return []
    }
  })
  return {
    resources: namesAndStatuses,
    unstar: starContext.unstarResource,
    selectedResource: selectedResource,
  }
}