mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-08 15:17:19 +02:00
This uses the new react-based web client for all builds, not just with the --dev flag. If the web client assets have not been built, the client will serve a message that Tailscale was built without the web client, and link to build instructions. Because we will include the web client in all of our builds, this should only be seen by developers or users building from source. (And eventually this will be replaced by attempting to download needed assets as runtime.) We do now checkin the build/index.html file, which serves the error message when assets are unavailable. This will also eventually be used to trigger in CI when new assets should be built and uploaded to a well-known location. Updates tailscale/corp#13775 Signed-off-by: Will Norris <will@tailscale.com>
146 lines
3.9 KiB
TypeScript
146 lines
3.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react"
|
|
import { apiFetch } from "src/api"
|
|
|
|
export type NodeData = {
|
|
Profile: UserProfile
|
|
Status: string
|
|
DeviceName: string
|
|
IP: string
|
|
AdvertiseExitNode: boolean
|
|
AdvertiseRoutes: string
|
|
LicensesURL: string
|
|
TUNMode: boolean
|
|
IsSynology: boolean
|
|
DSMVersion: number
|
|
IsUnraid: boolean
|
|
UnraidToken: string
|
|
IPNVersion: string
|
|
}
|
|
|
|
export type UserProfile = {
|
|
LoginName: string
|
|
DisplayName: string
|
|
ProfilePicURL: string
|
|
}
|
|
|
|
export type NodeUpdate = {
|
|
AdvertiseRoutes?: string
|
|
AdvertiseExitNode?: boolean
|
|
Reauthenticate?: boolean
|
|
ForceLogout?: boolean
|
|
}
|
|
|
|
// useNodeData returns basic data about the current node.
|
|
export default function useNodeData() {
|
|
const [data, setData] = useState<NodeData>()
|
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
|
|
|
const fetchNodeData = useCallback(() => {
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const nextParams = new URLSearchParams()
|
|
const token = urlParams.get("SynoToken")
|
|
if (token) {
|
|
nextParams.set("SynoToken", token)
|
|
}
|
|
const search = nextParams.toString()
|
|
const url = `api/data${search ? `?${search}` : ""}`
|
|
|
|
apiFetch(url)
|
|
.then((r) => r.json())
|
|
.then((d) => setData(d))
|
|
.catch((error) => console.error(error))
|
|
}, [setData])
|
|
|
|
const updateNode = useCallback(
|
|
(update: NodeUpdate) => {
|
|
// The contents of this function are mostly copied over
|
|
// from the legacy client's web.html file.
|
|
// It makes all data updates through one API endpoint.
|
|
// As we build out the web client in React,
|
|
// this endpoint will eventually be deprecated.
|
|
|
|
if (isPosting || !data) {
|
|
return
|
|
}
|
|
setIsPosting(true)
|
|
|
|
update = {
|
|
...update,
|
|
// Default to current data value for any unset fields.
|
|
AdvertiseRoutes:
|
|
update.AdvertiseRoutes !== undefined
|
|
? update.AdvertiseRoutes
|
|
: data.AdvertiseRoutes,
|
|
AdvertiseExitNode:
|
|
update.AdvertiseExitNode !== undefined
|
|
? update.AdvertiseExitNode
|
|
: data.AdvertiseExitNode,
|
|
}
|
|
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
const nextParams = new URLSearchParams({ up: "true" })
|
|
const token = urlParams.get("SynoToken")
|
|
if (token) {
|
|
nextParams.set("SynoToken", token)
|
|
}
|
|
const search = nextParams.toString()
|
|
const url = `api/data${search ? `?${search}` : ""}`
|
|
|
|
var body, contentType: string
|
|
|
|
if (data.IsUnraid) {
|
|
const params = new URLSearchParams()
|
|
params.append("csrf_token", data.UnraidToken)
|
|
params.append("ts_data", JSON.stringify(update))
|
|
body = params.toString()
|
|
contentType = "application/x-www-form-urlencoded;charset=UTF-8"
|
|
} else {
|
|
body = JSON.stringify(update)
|
|
contentType = "application/json"
|
|
}
|
|
|
|
apiFetch(url, {
|
|
method: "POST",
|
|
headers: { Accept: "application/json", "Content-Type": contentType },
|
|
body: body,
|
|
})
|
|
.then((r) => r.json())
|
|
.then((r) => {
|
|
setIsPosting(false)
|
|
const err = r["error"]
|
|
if (err) {
|
|
throw new Error(err)
|
|
}
|
|
const url = r["url"]
|
|
if (url) {
|
|
window.open(url, "_blank")
|
|
}
|
|
fetchNodeData()
|
|
})
|
|
.catch((err) => alert("Failed operation: " + err.message))
|
|
},
|
|
[data]
|
|
)
|
|
|
|
useEffect(
|
|
() => {
|
|
// Initial data load.
|
|
fetchNodeData()
|
|
|
|
// Refresh on browser tab focus.
|
|
const onVisibilityChange = () => {
|
|
document.visibilityState === "visible" && fetchNodeData()
|
|
}
|
|
window.addEventListener("visibilitychange", onVisibilityChange)
|
|
return () => {
|
|
// Cleanup browser tab listener.
|
|
window.removeEventListener("visibilitychange", onVisibilityChange)
|
|
}
|
|
},
|
|
// Run once.
|
|
[]
|
|
)
|
|
|
|
return { data, updateNode, isPosting }
|
|
}
|