mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-22 17:01:43 +00:00
client/web: start using swr for some fetching
Adds swr to the web client, and starts by using it from the useNodeData hook. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
014ae98297
commit
95655405b8
@ -13,6 +13,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"swr": "^2.2.4",
|
||||||
"wouter": "^2.11.0",
|
"wouter": "^2.11.0",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
export const swrConfig: SWRConfiguration = {
|
||||||
|
fetcher: (url: string) => apiFetch(url, "GET"),
|
||||||
|
onError: (err, _) => console.error(err),
|
||||||
|
}
|
||||||
|
|
||||||
let csrfToken: string
|
let csrfToken: string
|
||||||
let synoToken: string | undefined // required for synology API requests
|
let synoToken: string | undefined // required for synology API requests
|
||||||
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
|
let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
|
||||||
@ -11,14 +18,13 @@ let unraidCsrfToken: string | undefined // required for unraid POST requests (#8
|
|||||||
// apiFetch adds the `api` prefix to the request URL,
|
// apiFetch adds the `api` prefix to the request URL,
|
||||||
// so endpoint should be provided without the `api` prefix
|
// so endpoint should be provided without the `api` prefix
|
||||||
// (i.e. provide `/data` rather than `api/data`).
|
// (i.e. provide `/data` rather than `api/data`).
|
||||||
export function apiFetch(
|
export function apiFetch<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: "GET" | "POST" | "PATCH",
|
method: "GET" | "POST" | "PATCH",
|
||||||
body?: any,
|
body?: any
|
||||||
params?: Record<string, string>
|
): Promise<T> {
|
||||||
): Promise<Response> {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const nextParams = new URLSearchParams(params)
|
const nextParams = new URLSearchParams()
|
||||||
if (synoToken) {
|
if (synoToken) {
|
||||||
nextParams.set("SynoToken", synoToken)
|
nextParams.set("SynoToken", synoToken)
|
||||||
} else {
|
} else {
|
||||||
@ -51,16 +57,22 @@ export function apiFetch(
|
|||||||
"Content-Type": contentType,
|
"Content-Type": contentType,
|
||||||
"X-CSRF-Token": csrfToken,
|
"X-CSRF-Token": csrfToken,
|
||||||
},
|
},
|
||||||
body,
|
body: body,
|
||||||
}).then((r) => {
|
|
||||||
updateCsrfToken(r)
|
|
||||||
if (!r.ok) {
|
|
||||||
return r.text().then((err) => {
|
|
||||||
throw new Error(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
})
|
})
|
||||||
|
.then((r) => {
|
||||||
|
updateCsrfToken(r)
|
||||||
|
if (!r.ok) {
|
||||||
|
return r.text().then((err) => {
|
||||||
|
throw new Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
if (r.headers.get("Content-Type") === "application/json") {
|
||||||
|
return r.json()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCsrfToken(r: Response) {
|
function updateCsrfToken(r: Response) {
|
||||||
|
@ -146,8 +146,7 @@ type TailscaleUpOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tailscaleUp(options: TailscaleUpOptions) {
|
function tailscaleUp(options: TailscaleUpOptions) {
|
||||||
return apiFetch("/up", "POST", options)
|
return apiFetch<{ url?: string }>("/up", "POST", options)
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
d.url && window.open(d.url, "_blank")
|
d.url && window.open(d.url, "_blank")
|
||||||
})
|
})
|
||||||
|
@ -28,11 +28,10 @@ export default function useAuth() {
|
|||||||
|
|
||||||
const loadAuth = useCallback(() => {
|
const loadAuth = useCallback(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
return apiFetch("/auth", "GET")
|
return apiFetch<AuthResponse>("/auth", "GET")
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
setData(d)
|
setData(d)
|
||||||
switch ((d as AuthResponse).authNeeded) {
|
switch (d.authNeeded) {
|
||||||
case AuthType.synology:
|
case AuthType.synology:
|
||||||
fetch("/webman/login.cgi")
|
fetch("/webman/login.cgi")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
@ -53,15 +52,16 @@ export default function useAuth() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const newSession = useCallback(() => {
|
const newSession = useCallback(() => {
|
||||||
return apiFetch("/auth/session/new", "GET")
|
return apiFetch<{ authUrl?: string }>("/auth/session/new", "GET")
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
if (d.authUrl) {
|
if (d.authUrl) {
|
||||||
window.open(d.authUrl, "_blank")
|
window.open(d.authUrl, "_blank")
|
||||||
return apiFetch("/auth/session/wait", "GET")
|
return apiFetch("/auth/session/wait", "GET")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => loadAuth())
|
.then(() => {
|
||||||
|
loadAuth()
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
@ -70,7 +70,7 @@ export default function useAuth() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAuth().then((d) => {
|
loadAuth().then((d) => {
|
||||||
if (
|
if (
|
||||||
!d.canManageNode &&
|
!d?.canManageNode &&
|
||||||
new URLSearchParams(window.location.search).get("check") === "now"
|
new URLSearchParams(window.location.search).get("check") === "now"
|
||||||
) {
|
) {
|
||||||
newSession()
|
newSession()
|
||||||
|
@ -33,8 +33,7 @@ export default function useExitNodes(node: NodeData, filter?: string) {
|
|||||||
const [data, setData] = useState<ExitNode[]>([])
|
const [data, setData] = useState<ExitNode[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch("/exit-nodes", "GET")
|
apiFetch<ExitNode[]>("/exit-nodes", "GET")
|
||||||
.then((r) => r.json())
|
|
||||||
.then((r) => setData(r))
|
.then((r) => setData(r))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
alert("Failed operation: " + err.message)
|
alert("Failed operation: " + err.message)
|
||||||
|
@ -6,6 +6,7 @@ import { apiFetch, incrementMetric, setUnraidCsrfToken } from "src/api"
|
|||||||
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
import { ExitNode, noExitNode, runAsExitNode } from "src/hooks/exit-nodes"
|
||||||
import { VersionInfo } from "src/hooks/self-update"
|
import { VersionInfo } from "src/hooks/self-update"
|
||||||
import { assertNever } from "src/utils/util"
|
import { assertNever } from "src/utils/util"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
Profile: UserProfile
|
Profile: UserProfile
|
||||||
@ -120,19 +121,12 @@ type RoutesPOSTData = {
|
|||||||
|
|
||||||
// useNodeData returns basic data about the current node.
|
// useNodeData returns basic data about the current node.
|
||||||
export default function useNodeData() {
|
export default function useNodeData() {
|
||||||
const [data, setData] = useState<NodeData>()
|
const { data, mutate } = useSWR<NodeData>("/data")
|
||||||
const [isPosting, setIsPosting] = useState<boolean>(false)
|
const [isPosting, setIsPosting] = useState<boolean>(false)
|
||||||
|
|
||||||
const refreshData = useCallback(
|
useEffect(
|
||||||
() =>
|
() => setUnraidCsrfToken(data?.IsUnraid ? data.UnraidToken : undefined),
|
||||||
apiFetch("/data", "GET")
|
[data]
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d: NodeData) => {
|
|
||||||
setData(d)
|
|
||||||
setUnraidCsrfToken(d.IsUnraid ? d.UnraidToken : undefined)
|
|
||||||
})
|
|
||||||
.catch((error) => console.error(error)),
|
|
||||||
[setData]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const prefsPATCH = useCallback(
|
const prefsPATCH = useCallback(
|
||||||
@ -147,12 +141,12 @@ export default function useNodeData() {
|
|||||||
// then make the prefs PATCH. If the request fails,
|
// then make the prefs PATCH. If the request fails,
|
||||||
// data will be updated to it's previous value in
|
// data will be updated to it's previous value in
|
||||||
// onComplete below.
|
// onComplete below.
|
||||||
setData(optimisticUpdates)
|
mutate(optimisticUpdates, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
setIsPosting(false)
|
setIsPosting(false)
|
||||||
refreshData() // refresh data after PATCH finishes
|
mutate() // refresh data after PATCH finishes
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiFetch("/local/v0/prefs", "PATCH", d)
|
return apiFetch("/local/v0/prefs", "PATCH", d)
|
||||||
@ -163,7 +157,7 @@ export default function useNodeData() {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setIsPosting, refreshData, setData, data]
|
[data, mutate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const routesPOST = useCallback(
|
const routesPOST = useCallback(
|
||||||
@ -171,7 +165,7 @@ export default function useNodeData() {
|
|||||||
setIsPosting(true)
|
setIsPosting(true)
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
setIsPosting(false)
|
setIsPosting(false)
|
||||||
refreshData() // refresh data after POST finishes
|
mutate() // refresh data after POST finishes
|
||||||
}
|
}
|
||||||
const updateMetrics = () => {
|
const updateMetrics = () => {
|
||||||
// only update metrics if values have changed
|
// only update metrics if values have changed
|
||||||
@ -195,26 +189,7 @@ export default function useNodeData() {
|
|||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[setIsPosting, refreshData, data?.AdvertisingExitNode]
|
[mutate, data?.AdvertisingExitNode]
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
// Initial data load.
|
|
||||||
refreshData()
|
|
||||||
|
|
||||||
// Refresh on browser tab focus.
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
document.visibilityState === "visible" && refreshData()
|
|
||||||
}
|
|
||||||
window.addEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
return () => {
|
|
||||||
// Cleanup browser tab listener.
|
|
||||||
window.removeEventListener("visibilitychange", onVisibilityChange)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Run once.
|
|
||||||
[refreshData]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const nodeUpdaters: NodeUpdaters = useMemo(
|
const nodeUpdaters: NodeUpdaters = useMemo(
|
||||||
@ -245,5 +220,5 @@ export default function useNodeData() {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return { data, refreshData, nodeUpdaters, isPosting }
|
return { data, refreshData: mutate, nodeUpdaters, isPosting }
|
||||||
}
|
}
|
||||||
|
@ -61,9 +61,8 @@ export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
|
|||||||
let timer: NodeJS.Timeout | undefined
|
let timer: NodeJS.Timeout | undefined
|
||||||
|
|
||||||
function poll() {
|
function poll() {
|
||||||
apiFetch("/local/v0/update/progress", "GET")
|
apiFetch<UpdateProgress[]>("/local/v0/update/progress", "GET")
|
||||||
.then((res) => res.json())
|
.then((res) => {
|
||||||
.then((res: UpdateProgress[]) => {
|
|
||||||
// res contains a list of UpdateProgresses that is strictly increasing
|
// res contains a list of UpdateProgresses that is strictly increasing
|
||||||
// in size, so updateMessagesRead keeps track (across calls of poll())
|
// in size, so updateMessagesRead keeps track (across calls of poll())
|
||||||
// of how many of those we have already read. This is why it is not
|
// of how many of those we have already read. This is why it is not
|
||||||
|
@ -10,8 +10,10 @@
|
|||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { swrConfig } from "src/api"
|
||||||
import App from "src/components/app"
|
import App from "src/components/app"
|
||||||
import ToastProvider from "src/ui/toaster"
|
import ToastProvider from "src/ui/toaster"
|
||||||
|
import { SWRConfig } from "swr"
|
||||||
|
|
||||||
declare var window: any
|
declare var window: any
|
||||||
// This is used to determine if the react client is built.
|
// This is used to determine if the react client is built.
|
||||||
@ -26,8 +28,10 @@ const root = createRoot(rootEl)
|
|||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ToastProvider>
|
<SWRConfig value={swrConfig}>
|
||||||
<App />
|
<ToastProvider>
|
||||||
</ToastProvider>
|
<App />
|
||||||
|
</ToastProvider>
|
||||||
|
</SWRConfig>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
@ -2599,6 +2599,11 @@ classnames@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||||
|
|
||||||
|
client-only@^0.0.1:
|
||||||
|
version "0.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||||
|
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
@ -4872,6 +4877,14 @@ svg-parser@^2.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
|
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
|
||||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||||
|
|
||||||
|
swr@^2.2.4:
|
||||||
|
version "2.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07"
|
||||||
|
integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==
|
||||||
|
dependencies:
|
||||||
|
client-only "^0.0.1"
|
||||||
|
use-sync-external-store "^1.2.0"
|
||||||
|
|
||||||
symbol-tree@^3.2.4:
|
symbol-tree@^3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
@ -5159,7 +5172,7 @@ use-sidecar@^1.1.2:
|
|||||||
detect-node-es "^1.1.0"
|
detect-node-es "^1.1.0"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
|
use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user