github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/ResourceNav.tsx (about)

     1  import React, {
     2    useCallback,
     3    useContext,
     4    useEffect,
     5    useMemo,
     6    useState,
     7  } from "react"
     8  import { matchPath, useHistory, useLocation } from "react-router-dom"
     9  import { usePathBuilder } from "./PathBuilder"
    10  import { ResourceName } from "./types"
    11  
    12  // Resource navigation semantics.
    13  // 1. standardizes navigation
    14  // 2. saves components from having to jump through hoops to get history + pathbuilder
    15  export type ResourceNav = {
    16    // The currently selected resource.
    17    selectedResource: string
    18  
    19    // Resource provided from the user URL that didn't exist.
    20    // Different parts of the UI might display this error differently.
    21    invalidResource: string
    22  
    23    // Behavior when you click on a link to a resource.
    24    openResource(name: string): void
    25  }
    26  
    27  const resourceNavContext = React.createContext<ResourceNav>({
    28    selectedResource: "",
    29    invalidResource: "",
    30    openResource: (name: string) => {},
    31  })
    32  
    33  export function useResourceNav(): ResourceNav {
    34    return useContext(resourceNavContext)
    35  }
    36  
    37  export let ResourceNavContextConsumer = resourceNavContext.Consumer
    38  export let ResourceNavContextProvider = resourceNavContext.Provider
    39  
    40  export function ResourceNavProvider(
    41    props: React.PropsWithChildren<{
    42      validateResource: (name: string) => boolean
    43    }>
    44  ) {
    45    let validateResource = useCallback(
    46      (name: string): boolean => {
    47        // The ALL resource should always validate
    48        return props.validateResource(name) || name === ResourceName.all
    49      },
    50      [props.validateResource]
    51    )
    52  
    53    let history = useHistory()
    54    let location = useLocation()
    55    let pb = usePathBuilder()
    56    let selectedResource = ""
    57    let [filterByResource, setFilterByResource] = useState(
    58      {} as { [key: string]: string }
    59    )
    60    let invalidResource = ""
    61  
    62    let matchResource = matchPath(location.pathname, {
    63      path: pb.path("/r/:name"),
    64    })
    65    let candidateResource = decodeURIComponent(
    66      (matchResource?.params as any)?.name || ""
    67    )
    68    if (candidateResource && validateResource(candidateResource)) {
    69      selectedResource = candidateResource
    70    } else {
    71      invalidResource = candidateResource
    72    }
    73  
    74    let search = location.search
    75  
    76    useEffect(() => {
    77      let existing = filterByResource[selectedResource] || ""
    78      if (existing != search) {
    79        let obj = {} as { [key: string]: string }
    80        Object.assign(obj, filterByResource)
    81        obj[selectedResource] = search
    82        setFilterByResource(obj)
    83      }
    84    }, [selectedResource, search])
    85  
    86    let openResource = useCallback(
    87      (name: string) => {
    88        name = name || ResourceName.all
    89        let url = pb.encpath`/r/${name}/overview`
    90  
    91        // We deliberately make search terms stick to a resource.
    92        //
    93        // So if you add a log filter to resource A, navigate to B,
    94        // then come back to A, we preserve the filter on A.
    95        //
    96        // We're not sure if this is the right behavior, and do not
    97        // store it in any sort of persistent store.
    98        let storedFilter = filterByResource[name] || ""
    99        history.push(url + storedFilter)
   100      },
   101      [history, filterByResource]
   102    )
   103  
   104    let resourceNav = useMemo(() => {
   105      return {
   106        invalidResource: invalidResource,
   107        selectedResource: selectedResource,
   108        openResource,
   109      }
   110    }, [invalidResource, selectedResource, openResource])
   111  
   112    return (
   113      <resourceNavContext.Provider value={resourceNav}>
   114        {props.children}
   115      </resourceNavContext.Provider>
   116    )
   117  }