client/web: show features based on platform support

Hiding/disabling UI features when not available on the running
client.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-11-30 13:01:29 -05:00
committed by Sonia Appasamy
parent 7d61b827e8
commit 7a4ba609d9
14 changed files with 220 additions and 68 deletions

View File

@ -3,6 +3,7 @@
import { useEffect, useMemo, useState } from "react"
import { apiFetch } from "src/api"
import { NodeData } from "src/hooks/node-data"
export type ExitNode = {
ID: string
@ -28,7 +29,7 @@ export type ExitNodeGroup = {
nodes: ExitNode[]
}
export default function useExitNodes(tailnetName: string, filter?: string) {
export default function useExitNodes(node: NodeData, filter?: string) {
const [data, setData] = useState<ExitNode[]>([])
useEffect(() => {
@ -47,6 +48,14 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
let tailnetNodes: ExitNode[] = []
const locationNodes = new Map<CountryCode, Map<CityCode, ExitNode[]>>()
if (!node.Features["use-exit-node"]) {
// early-return
return {
tailnetNodesSorted: tailnetNodes,
locationNodesMap: locationNodes,
}
}
data?.forEach((n) => {
const loc = n.Location
if (!loc) {
@ -55,7 +64,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
// Only Mullvad exit nodes have locations filled.
tailnetNodes.push({
...n,
Name: trimDNSSuffix(n.Name, tailnetName),
Name: trimDNSSuffix(n.Name, node.TailnetName),
})
return
}
@ -70,12 +79,15 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
tailnetNodesSorted: tailnetNodes.sort(compareByName),
locationNodesMap: locationNodes,
}
}, [data, tailnetName])
}, [data, node.Features, node.TailnetName])
const hasFilter = Boolean(filter)
const mullvadNodesSorted = useMemo(() => {
const nodes: ExitNode[] = []
if (!node.Features["use-exit-node"]) {
return nodes // early-return
}
// addBestMatchNode adds the node with the "higest priority"
// match from a list of exit node `options` to `nodes`.
@ -123,14 +135,27 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
}
return nodes.sort(compareByName)
}, [hasFilter, locationNodesMap])
}, [hasFilter, locationNodesMap, node.Features])
// Ordered and filtered grouping of exit nodes.
const exitNodeGroups = useMemo(() => {
const filterLower = !filter ? undefined : filter.toLowerCase()
const selfGroup = {
id: "self",
name: undefined,
nodes: filter
? []
: !node.Features["advertise-exit-node"]
? [noExitNode] // don't show "runAsExitNode" option
: [noExitNode, runAsExitNode],
}
if (!node.Features["use-exit-node"]) {
return [selfGroup]
}
return [
{ id: "self", nodes: filter ? [] : [noExitNode, runAsExitNode] },
selfGroup,
{
id: "tailnet",
nodes: filterLower
@ -149,7 +174,7 @@ export default function useExitNodes(tailnetName: string, filter?: string) {
: mullvadNodesSorted,
},
]
}, [tailnetNodesSorted, mullvadNodesSorted, filter])
}, [filter, node.Features, tailnetNodesSorted, mullvadNodesSorted])
return { data: exitNodeGroups }
}

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"
import { apiFetch, setUnraidCsrfToken } from "src/api"
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
import { VersionInfo } from "src/hooks/self-update"
import { assertNever } from "src/util"
export type NodeData = {
Profile: UserProfile
@ -34,6 +35,7 @@ export type NodeData = {
RunningSSHServer: boolean
ControlAdminURL: string
LicensesURL: string
Features: { [key in Feature]: boolean } // value is true if given feature is available on this client
}
type NodeState =
@ -55,6 +57,30 @@ export type SubnetRoute = {
Approved: boolean
}
export type Feature =
| "advertise-exit-node"
| "advertise-routes"
| "use-exit-node"
| "ssh"
| "auto-update"
export const featureDescription = (f: Feature) => {
switch (f) {
case "advertise-exit-node":
return "Advertising as an exit node"
case "advertise-routes":
return "Advertising subnet routes"
case "use-exit-node":
return "Using an exit node"
case "ssh":
return "Running a Tailscale SSH server"
case "auto-update":
return "Auto updating client versions"
default:
assertNever(f)
}
}
/**
* NodeUpdaters provides a set of mutation functions for a node.
*