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, {
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react"
import { matchPath, useNavigate, useLocation } from "react-router-dom"
import { usePathBuilder } from "./PathBuilder"
import { ResourceName } from "./types"

// Resource navigation semantics.
// 1. standardizes navigation
// 2. saves components from having to jump through hoops to get history + pathbuilder
export type ResourceNav = {
  // The currently selected resource.
  selectedResource: string

  // Resource provided from the user URL that didn't exist.
  // Different parts of the UI might display this error differently.
  invalidResource: string

  // Behavior when you click on a link to a resource.
  openResource(name: string): void
}

const resourceNavContext = React.createContext<ResourceNav>({
  selectedResource: "",
  invalidResource: "",
  openResource: (name: string) => {},
})

export function useResourceNav(): ResourceNav {
  return useContext(resourceNavContext)
}

export let ResourceNavContextConsumer = resourceNavContext.Consumer
export let ResourceNavContextProvider = resourceNavContext.Provider

export function ResourceNavProvider(
  props: React.PropsWithChildren<{
    validateResource: (name: string) => boolean
  }>
) {
  let validateResource = useCallback(
    (name: string): boolean => {
      // The ALL resource should always validate
      return props.validateResource(name) || name === ResourceName.all
    },
    [props.validateResource]
  )

  const navigate = useNavigate()
  let location = useLocation()
  let pb = usePathBuilder()
  let selectedResource = ""
  let [filterByResource, setFilterByResource] = useState(
    {} as { [key: string]: string }
  )
  let invalidResource = ""

  let matchResource =
    matchPath({ path: pb.path("/r/:name") }, location.pathname) ||
    matchPath({ path: pb.path("/r/:name/*") }, location.pathname)
  let candidateResource = decodeURIComponent(
    (matchResource?.params as any)?.name || ""
  )
  if (candidateResource && validateResource(candidateResource)) {
    selectedResource = candidateResource
  } else {
    invalidResource = candidateResource
  }

  let search = location.search

  useEffect(() => {
    let existing = filterByResource[selectedResource] || ""
    if (existing != search) {
      let obj = {} as { [key: string]: string }
      Object.assign(obj, filterByResource)
      obj[selectedResource] = search
      setFilterByResource(obj)
    }
  }, [selectedResource, search])

  let openResource = useCallback(
    (name: string) => {
      name = name || ResourceName.all
      let url = pb.encpath`/r/${name}/overview`

      // We deliberately make search terms stick to a resource.
      //
      // So if you add a log filter to resource A, navigate to B,
      // then come back to A, we preserve the filter on A.
      //
      // We're not sure if this is the right behavior, and do not
      // store it in any sort of persistent store.
      let storedFilter = filterByResource[name] || ""
      navigate(url + storedFilter)
    },
    [navigate, filterByResource]
  )

  let resourceNav = useMemo(() => {
    return {
      invalidResource: invalidResource,
      selectedResource: selectedResource,
      openResource,
    }
  }, [invalidResource, selectedResource, openResource])

  return (
    <resourceNavContext.Provider value={resourceNav}>
      {props.children}
    </resourceNavContext.Provider>
  )
}