mirror of https://github.com/tailscale/tailscale/
client/web: add initial framework for exit node selector
Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>pull/10153/head
parent
de2af54ffc
commit
5e095ddc20
@ -0,0 +1,171 @@
|
||||
import cx from "classnames"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { NodeData, NodeUpdate } from "src/hooks/node-data"
|
||||
import { ReactComponent as Check } from "src/icons/check.svg"
|
||||
import { ReactComponent as ChevronDown } from "src/icons/chevron-down.svg"
|
||||
import { ReactComponent as Search } from "src/icons/search.svg"
|
||||
|
||||
const noExitNode = "None"
|
||||
const runAsExitNode = "Run as exit node…"
|
||||
|
||||
export default function ExitNodeSelector({
|
||||
className,
|
||||
node,
|
||||
updateNode,
|
||||
}: {
|
||||
className?: string
|
||||
node: NodeData
|
||||
updateNode: (update: NodeUpdate) => Promise<void> | undefined
|
||||
}) {
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [selected, setSelected] = useState(
|
||||
node.AdvertiseExitNode ? runAsExitNode : noExitNode
|
||||
)
|
||||
useEffect(() => {
|
||||
setSelected(node.AdvertiseExitNode ? runAsExitNode : noExitNode)
|
||||
}, [node])
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(item: string) => {
|
||||
setOpen(false)
|
||||
if (item === selected) {
|
||||
return // no update
|
||||
}
|
||||
const old = selected
|
||||
setSelected(item)
|
||||
var update: NodeUpdate = {}
|
||||
switch (item) {
|
||||
case noExitNode:
|
||||
// turn off exit node
|
||||
update = { AdvertiseExitNode: false }
|
||||
break
|
||||
case runAsExitNode:
|
||||
// turn on exit node
|
||||
update = { AdvertiseExitNode: true }
|
||||
break
|
||||
}
|
||||
updateNode(update)?.catch(() => setSelected(old))
|
||||
},
|
||||
[setOpen, selected, setSelected]
|
||||
)
|
||||
// TODO: close on click outside
|
||||
// TODO(sonia): allow choosing to use another exit node
|
||||
|
||||
const [
|
||||
none, // not using exit nodes
|
||||
advertising, // advertising as exit node
|
||||
using, // using another exit node
|
||||
] = useMemo(
|
||||
() => [
|
||||
selected === noExitNode,
|
||||
selected === runAsExitNode,
|
||||
selected !== noExitNode && selected !== runAsExitNode,
|
||||
],
|
||||
[selected]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
"p-1.5 rounded-md border flex items-stretch gap-1.5",
|
||||
{
|
||||
"border-gray-200": none,
|
||||
"bg-amber-600 border-amber-600": advertising,
|
||||
"bg-indigo-500 border-indigo-500": using,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={cx("flex-1 px-2 py-1.5 rounded-[1px] cursor-pointer", {
|
||||
"bg-white hover:bg-stone-100": none,
|
||||
"bg-amber-600 hover:bg-orange-400": advertising,
|
||||
"bg-indigo-500 hover:bg-indigo-400": using,
|
||||
})}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
"text-neutral-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-neutral-800", {
|
||||
"text-white": advertising || using,
|
||||
})}
|
||||
>
|
||||
{selected === runAsExitNode ? "Running as exit node" : "None"}
|
||||
</p>
|
||||
<ChevronDown
|
||||
className={cx("ml-1", {
|
||||
"stroke-neutral-800": none,
|
||||
"stroke-white": advertising || using,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
{(advertising || using) && (
|
||||
<button
|
||||
className={cx("px-3 py-2 rounded-sm text-white cursor-pointer", {
|
||||
"bg-orange-400": advertising,
|
||||
"bg-indigo-400": using,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleSelect(noExitNode)
|
||||
}}
|
||||
>
|
||||
Disable
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{open && (
|
||||
<div className="absolute ml-1.5 -mt-3 w-full max-w-md py-1 bg-white rounded-lg shadow">
|
||||
<div className="w-full px-4 py-2 flex items-center gap-2.5">
|
||||
<Search />
|
||||
<input
|
||||
className="flex-1 leading-snug"
|
||||
placeholder="Search exit nodes…"
|
||||
/>
|
||||
</div>
|
||||
<DropdownSection
|
||||
items={[noExitNode, runAsExitNode]}
|
||||
selected={selected}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownSection({
|
||||
items,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
items: string[]
|
||||
selected?: string
|
||||
onSelect: (item: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full mt-1 pt-1 border-t border-gray-200">
|
||||
{items.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
className="w-full px-4 py-2 flex justify-between items-center cursor-pointer hover:bg-stone-100"
|
||||
onClick={() => onSelect(v)}
|
||||
>
|
||||
<div className="leading-snug">{v}</div>
|
||||
{selected == v && <Check />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6673 5L7.50065 14.1667L3.33398 10" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 236 B |
@ -1,3 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke="#706E6D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 7.5L10 12.5L15 7.5" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 220 B After Width: | Height: | Size: 203 B |
@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.16667 15.8333C12.8486 15.8333 15.8333 12.8486 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333Z" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.5 17.5L13.875 13.875" stroke="#706E6D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 500 B |
Loading…
Reference in New Issue