mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-23 09:21:41 +00:00
client/web: add visual indication for exit node pending approval (#10532)
Add visual indication when running as an exit node prior to receiving admin approval. Updates https://github.com/tailscale/tailscale/issues/10261 Signed-off-by: Mario Minardi <mario@tailscale.com> Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
e9f203d747
commit
763b9daa84
@ -92,7 +92,8 @@ export function useAPI() {
|
|||||||
<MutateDataType, FetchDataType = any>(
|
<MutateDataType, FetchDataType = any>(
|
||||||
key: string,
|
key: string,
|
||||||
fetch: Promise<FetchDataType>,
|
fetch: Promise<FetchDataType>,
|
||||||
optimisticData: (current: MutateDataType) => MutateDataType
|
optimisticData: (current: MutateDataType) => MutateDataType,
|
||||||
|
revalidate?: boolean // optionally specify whether to run final revalidation (step 3)
|
||||||
): Promise<FetchDataType | undefined> => {
|
): Promise<FetchDataType | undefined> => {
|
||||||
const options: MutatorOptions = {
|
const options: MutatorOptions = {
|
||||||
/**
|
/**
|
||||||
@ -105,6 +106,7 @@ export function useAPI() {
|
|||||||
*/
|
*/
|
||||||
populateCache: false,
|
populateCache: false,
|
||||||
optimisticData,
|
optimisticData,
|
||||||
|
revalidate: revalidate,
|
||||||
}
|
}
|
||||||
return mutate(key, fetch, options)
|
return mutate(key, fetch, options)
|
||||||
},
|
},
|
||||||
@ -226,8 +228,12 @@ export function useAPI() {
|
|||||||
...old,
|
...old,
|
||||||
UsingExitNode: Boolean(body.UseExitNode) ? t.data : undefined,
|
UsingExitNode: Boolean(body.UseExitNode) ? t.data : undefined,
|
||||||
AdvertisingExitNode: Boolean(body.AdvertiseExitNode),
|
AdvertisingExitNode: Boolean(body.AdvertiseExitNode),
|
||||||
|
AdvertisingExitNodeApproved: Boolean(body.AdvertiseExitNode)
|
||||||
|
? true // gets updated in revalidation
|
||||||
|
: old.AdvertisingExitNodeApproved,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
false // skip final revalidation
|
||||||
)
|
)
|
||||||
.then(() => metrics.forEach((m) => incrementMetric(m)))
|
.then(() => metrics.forEach((m) => incrementMetric(m)))
|
||||||
.catch(handlePostError("Failed to update exit node"))
|
.catch(handlePostError("Failed to update exit node"))
|
||||||
|
@ -14,6 +14,7 @@ import useExitNodes, {
|
|||||||
import { ExitNode, NodeData } from "src/types"
|
import { ExitNode, NodeData } from "src/types"
|
||||||
import Popover from "src/ui/popover"
|
import Popover from "src/ui/popover"
|
||||||
import SearchInput from "src/ui/search-input"
|
import SearchInput from "src/ui/search-input"
|
||||||
|
import { useSWRConfig } from "swr"
|
||||||
|
|
||||||
export default function ExitNodeSelector({
|
export default function ExitNodeSelector({
|
||||||
className,
|
className,
|
||||||
@ -27,7 +28,14 @@ export default function ExitNodeSelector({
|
|||||||
const api = useAPI()
|
const api = useAPI()
|
||||||
const [open, setOpen] = useState<boolean>(false)
|
const [open, setOpen] = useState<boolean>(false)
|
||||||
const [selected, setSelected] = useState<ExitNode>(toSelectedExitNode(node))
|
const [selected, setSelected] = useState<ExitNode>(toSelectedExitNode(node))
|
||||||
|
const [pending, setPending] = useState<boolean>(false)
|
||||||
|
const { mutate } = useSWRConfig() // allows for global mutation
|
||||||
useEffect(() => setSelected(toSelectedExitNode(node)), [node])
|
useEffect(() => setSelected(toSelectedExitNode(node)), [node])
|
||||||
|
useEffect(() => {
|
||||||
|
setPending(
|
||||||
|
node.AdvertisingExitNode && node.AdvertisingExitNodeApproved === false
|
||||||
|
)
|
||||||
|
}, [node])
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(n: ExitNode) => {
|
(n: ExitNode) => {
|
||||||
@ -35,9 +43,18 @@ export default function ExitNodeSelector({
|
|||||||
if (n.ID === selected.ID) {
|
if (n.ID === selected.ID) {
|
||||||
return // no update
|
return // no update
|
||||||
}
|
}
|
||||||
|
// Eager clear of pending state to avoid UI oddities
|
||||||
|
if (n.ID !== runAsExitNode.ID) {
|
||||||
|
setPending(false)
|
||||||
|
}
|
||||||
api({ action: "update-exit-node", data: n })
|
api({ action: "update-exit-node", data: n })
|
||||||
|
|
||||||
|
// refresh data after short timeout to pick up any pending approval updates
|
||||||
|
setTimeout(() => {
|
||||||
|
mutate("/data")
|
||||||
|
}, 1000)
|
||||||
},
|
},
|
||||||
[api, selected]
|
[api, mutate, selected.ID]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@ -52,7 +69,7 @@ export default function ExitNodeSelector({
|
|||||||
selected.ID !== noExitNode.ID && selected.ID !== runAsExitNode.ID,
|
selected.ID !== noExitNode.ID && selected.ID !== runAsExitNode.ID,
|
||||||
!selected.Online,
|
!selected.Online,
|
||||||
],
|
],
|
||||||
[selected]
|
[selected.ID, selected.Online]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,6 +78,7 @@ export default function ExitNodeSelector({
|
|||||||
"rounded-md",
|
"rounded-md",
|
||||||
{
|
{
|
||||||
"bg-red-600": offline,
|
"bg-red-600": offline,
|
||||||
|
"bg-yellow-400": pending,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@ -160,6 +178,12 @@ export default function ExitNodeSelector({
|
|||||||
blocked until you disable the exit node or select a different one.
|
blocked until you disable the exit node or select a different one.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{pending && (
|
||||||
|
<p className="text-white p-3">
|
||||||
|
Pending approval to run as exit node. This device won't be usable as
|
||||||
|
an exit node until then.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export type NodeData = {
|
|||||||
KeyExpired: boolean
|
KeyExpired: boolean
|
||||||
UsingExitNode?: ExitNode
|
UsingExitNode?: ExitNode
|
||||||
AdvertisingExitNode: boolean
|
AdvertisingExitNode: boolean
|
||||||
|
AdvertisingExitNodeApproved: boolean
|
||||||
AdvertisedRoutes?: SubnetRoute[]
|
AdvertisedRoutes?: SubnetRoute[]
|
||||||
TUNMode: boolean
|
TUNMode: boolean
|
||||||
IsSynology: boolean
|
IsSynology: boolean
|
||||||
|
@ -589,10 +589,11 @@ type nodeData struct {
|
|||||||
UnraidToken string
|
UnraidToken string
|
||||||
URLPrefix string // if set, the URL prefix the client is served behind
|
URLPrefix string // if set, the URL prefix the client is served behind
|
||||||
|
|
||||||
UsingExitNode *exitNode
|
UsingExitNode *exitNode
|
||||||
AdvertisingExitNode bool
|
AdvertisingExitNode bool
|
||||||
AdvertisedRoutes []subnetRoute // excludes exit node routes
|
AdvertisingExitNodeApproved bool // whether running this node as an exit node has been approved by an admin
|
||||||
RunningSSHServer bool
|
AdvertisedRoutes []subnetRoute // excludes exit node routes
|
||||||
|
RunningSSHServer bool
|
||||||
|
|
||||||
ClientVersion *tailcfg.ClientVersion
|
ClientVersion *tailcfg.ClientVersion
|
||||||
|
|
||||||
@ -693,6 +694,8 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
|||||||
return p == route
|
return p == route
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
data.AdvertisingExitNodeApproved = routeApproved(exitNodeRouteV4) || routeApproved(exitNodeRouteV6)
|
||||||
|
|
||||||
for _, r := range prefs.AdvertiseRoutes {
|
for _, r := range prefs.AdvertiseRoutes {
|
||||||
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
||||||
data.AdvertisingExitNode = true
|
data.AdvertisingExitNode = true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user