client/web: handle offline exit nodes

If the currently selected exit node is offline, render the exit node
selector in red with an error message. Update exit nodes in the dropdown
to indicate if they are offline, and don't allow them to be selected.

This also updates some older color values to use the new colors.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
This commit is contained in:
Will Norris 2023-12-04 11:58:14 -08:00 committed by Will Norris
parent b144391c06
commit f5989f317f
3 changed files with 90 additions and 61 deletions

View File

@ -46,11 +46,13 @@ export default function ExitNodeSelector({
none, // not using exit nodes
advertising, // advertising as exit node
using, // using another exit node
offline, // selected exit node node is offline
] = useMemo(
() => [
selected.ID === noExitNode.ID,
selected.ID === runAsExitNode.ID,
selected.ID !== noExitNode.ID && selected.ID !== runAsExitNode.ID,
!selected.Online,
],
[selected]
)
@ -74,74 +76,91 @@ export default function ExitNodeSelector({
>
<div
className={cx(
"p-1.5 rounded-md border flex items-stretch gap-1.5",
"rounded-md",
{
"border-gray-200": none,
"bg-amber-600 border-amber-600": advertising,
"bg-blue-500 border-blue-500": using,
"bg-red-600": offline,
},
className
)}
>
<button
className={cx("flex-1 px-2 py-1.5 rounded-[1px]", {
"bg-white": none,
"hover:bg-gray-100": none && !disabled,
"bg-orange-600": advertising,
"hover:bg-orange-400": advertising && !disabled,
"bg-blue-500": using,
"hover:bg-blue-400": using && !disabled,
<div
className={cx("p-1.5 rounded-md border flex items-stretch gap-1.5", {
"border-gray-200": none,
"bg-yellow-300 border-yellow-300": advertising && !offline,
"bg-blue-500 border-blue-500": using && !offline,
"bg-red-500 border-red-500": offline,
})}
onClick={() => setOpen(!open)}
disabled={disabled}
>
<p
className={cx(
"text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1",
{ "bg-opacity-70 text-white": advertising || using }
)}
>
Exit node
</p>
<div className="flex items-center">
<p
className={cx("text-gray-800", {
"text-white": advertising || using,
})}
>
{selected.Location && (
<>
<CountryFlag code={selected.Location.CountryCode} />{" "}
</>
)}
{selected === runAsExitNode
? "Running as exit node"
: selected.Name}
</p>
{!disabled && (
<ChevronDown
className={cx("ml-1", {
"stroke-gray-800": none,
"stroke-white": advertising || using,
})}
/>
)}
</div>
</button>
{!disabled && (advertising || using) && (
<button
className={cx("px-3 py-2 rounded-sm text-white", {
"bg-orange-400": advertising,
"bg-blue-400": using,
className={cx("flex-1 px-2 py-1.5 rounded-[1px]", {
"bg-white": none,
"hover:bg-gray-100": none && !disabled,
"bg-yellow-300": advertising && !offline,
"hover:bg-yellow-200": advertising && !offline && !disabled,
"bg-blue-500": using && !offline,
"hover:bg-blue-400": using && !offline && !disabled,
"bg-red-500": offline,
"hover:bg-red-400": offline && !disabled,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
onClick={() => setOpen(!open)}
disabled={disabled}
>
Disable
<p
className={cx(
"text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1",
{ "bg-opacity-70 text-white": advertising || using }
)}
>
Exit node{offline && " offline"}
</p>
<div className="flex items-center">
<p
className={cx("text-gray-800", {
"text-white": advertising || using,
})}
>
{selected.Location && (
<>
<CountryFlag code={selected.Location.CountryCode} />{" "}
</>
)}
{selected === runAsExitNode
? "Running as exit node"
: selected.Name}
</p>
{!disabled && (
<ChevronDown
className={cx("ml-1", {
"stroke-gray-800": none,
"stroke-white": advertising || using,
})}
/>
)}
</div>
</button>
{!disabled && (advertising || using) && (
<button
className={cx("px-3 py-2 rounded-sm text-white", {
"bg-yellow-200": advertising && !offline,
"bg-blue-400": using && !offline,
"bg-red-400": offline,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
>
Disable
</button>
)}
</div>
{offline && (
<p className="text-white p-3">
The selected exit node is currently offline. Your internet traffic
is blocked until you disable the exit node or select a different
one.
</p>
)}
</div>
</Popover>
@ -254,10 +273,16 @@ function ExitNodeSelectorItem({
return (
<button
key={node.ID}
className="w-full px-4 py-2 flex justify-between items-center cursor-pointer hover:bg-gray-100"
className={cx(
"w-full px-4 py-2 flex justify-between items-center cursor-pointer hover:bg-gray-100",
{
"text-gray-400 cursor-not-allowed": !node.Online,
}
)}
onClick={onSelect}
disabled={!node.Online}
>
<div>
<div className="w-full">
{node.Location && (
<>
<CountryFlag code={node.Location.CountryCode} />{" "}
@ -265,7 +290,8 @@ function ExitNodeSelectorItem({
)}
<span className="leading-snug">{node.Name}</span>
</div>
{isSelected && <Check />}
{node.Online || <span className="leading-snug">Offline</span>}
{isSelected && <Check className="ml-1" />}
</button>
)
}

View File

@ -222,8 +222,10 @@ export function trimDNSSuffix(s: string, tailnetDNSName: string): string {
return s
}
export const noExitNode: ExitNode = { ID: "NONE", Name: "None" }
// Neither of these are really "online", but setting this makes them selectable.
export const noExitNode: ExitNode = { ID: "NONE", Name: "None", Online: true }
export const runAsExitNode: ExitNode = {
ID: "RUNNING",
Name: "Run as exit node…",
Online: true,
}

View File

@ -738,6 +738,7 @@ func (s *Server) serveGetExitNodes(w http.ResponseWriter, r *http.Request) {
ID: ps.ID,
Name: ps.DNSName,
Location: ps.Location,
Online: ps.Online,
})
}
writeJSON(w, exitNodes)