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>
pull/10471/head
Will Norris 12 months ago committed by Will Norris
parent b144391c06
commit f5989f317f

@ -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 }
)}
<button
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={() => setOpen(!open)}
disabled={disabled}
>
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} />{" "}
</>
className={cx(
"text-gray-500 text-xs text-left font-medium uppercase tracking-wide mb-1",
{ "bg-opacity-70 text-white": advertising || using }
)}
{selected === runAsExitNode
? "Running as exit node"
: selected.Name}
>
Exit node{offline && " offline"}
</p>
{!disabled && (
<ChevronDown
className={cx("ml-1", {
"stroke-gray-800": none,
"stroke-white": advertising || using,
<div className="flex items-center">
<p
className={cx("text-gray-800", {
"text-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,
})}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleSelect(noExitNode)
}}
>
Disable
>
{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>
)
}

@ -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,
}

@ -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)

Loading…
Cancel
Save