mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-23 09:21:41 +00:00

This commit makes some restructural changes to how we handle api posting from the web client frontend. Now that we're using SWR, we have less of a need for hooks like useNodeData that return a useSWR response alongside some mutation callbacks. SWR makes it easy to mutate throughout the UI without needing access to the original data state in order to reflect updates. So, we can fetch data without having to tie it to post callbacks that have to be passed around through components. In an effort to consolidate our posting endpoints, and make it easier to add more api handlers cleanly in the future, this change introduces a new `useAPI` hook that returns a single `api` callback that can make any changes from any component in the UI. The hook itself handles using SWR to mutate the relevant data keys, which get globally reflected throughout the UI. As a concurrent cleanup, node types are also moved to their own types.ts file, to consolidate data types across the app. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
import { useCallback, useEffect, useState } from "react"
|
|
import { apiFetch } from "src/api"
|
|
import { VersionInfo } from "src/types"
|
|
|
|
// see ipnstate.UpdateProgress
|
|
export type UpdateProgress = {
|
|
status: "UpdateFinished" | "UpdateInProgress" | "UpdateFailed"
|
|
message: string
|
|
version: string
|
|
}
|
|
|
|
export enum UpdateState {
|
|
UpToDate,
|
|
Available,
|
|
InProgress,
|
|
Complete,
|
|
Failed,
|
|
}
|
|
|
|
// useInstallUpdate initiates and tracks a Tailscale self-update via the LocalAPI,
|
|
// and returns state messages showing the progress of the update.
|
|
export function useInstallUpdate(currentVersion: string, cv?: VersionInfo) {
|
|
const [updateState, setUpdateState] = useState<UpdateState>(
|
|
cv?.RunningLatest ? UpdateState.UpToDate : UpdateState.Available
|
|
)
|
|
|
|
const [updateLog, setUpdateLog] = useState<string>("")
|
|
|
|
const appendUpdateLog = useCallback(
|
|
(msg: string) => {
|
|
setUpdateLog(updateLog + msg + "\n")
|
|
},
|
|
[updateLog, setUpdateLog]
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (updateState !== UpdateState.Available) {
|
|
// useEffect cleanup function
|
|
return () => {}
|
|
}
|
|
|
|
setUpdateState(UpdateState.InProgress)
|
|
|
|
apiFetch("/local/v0/update/install", "POST").catch((err) => {
|
|
console.error(err)
|
|
setUpdateState(UpdateState.Failed)
|
|
})
|
|
|
|
let tsAwayForPolls = 0
|
|
let updateMessagesRead = 0
|
|
|
|
let timer: NodeJS.Timeout | undefined
|
|
|
|
function poll() {
|
|
apiFetch<UpdateProgress[]>("/local/v0/update/progress", "GET")
|
|
.then((res) => {
|
|
// res contains a list of UpdateProgresses that is strictly increasing
|
|
// 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
|
|
// initialized to zero here and we don't just use res.forEach()
|
|
for (; updateMessagesRead < res.length; ++updateMessagesRead) {
|
|
const up = res[updateMessagesRead]
|
|
if (up.status === "UpdateFailed") {
|
|
setUpdateState(UpdateState.Failed)
|
|
if (up.message) appendUpdateLog("ERROR: " + up.message)
|
|
return
|
|
}
|
|
|
|
if (up.status === "UpdateFinished") {
|
|
// if update finished and tailscaled did not go away (ie. did not restart),
|
|
// then the version being the same might not be an error, it might just require
|
|
// the user to restart Tailscale manually (this is required in some cases in the
|
|
// clientupdate package).
|
|
if (up.version === currentVersion && tsAwayForPolls > 0) {
|
|
setUpdateState(UpdateState.Failed)
|
|
appendUpdateLog(
|
|
"ERROR: Update failed, still running Tailscale " + up.version
|
|
)
|
|
if (up.message) appendUpdateLog("ERROR: " + up.message)
|
|
} else {
|
|
setUpdateState(UpdateState.Complete)
|
|
if (up.message) appendUpdateLog("INFO: " + up.message)
|
|
}
|
|
return
|
|
}
|
|
|
|
setUpdateState(UpdateState.InProgress)
|
|
if (up.message) appendUpdateLog("INFO: " + up.message)
|
|
}
|
|
|
|
// If we have gone through the entire loop without returning out of the function,
|
|
// the update is still in progress. So we want to poll again for further status
|
|
// updates.
|
|
timer = setTimeout(poll, 1000)
|
|
})
|
|
.catch((err) => {
|
|
++tsAwayForPolls
|
|
if (tsAwayForPolls >= 5 * 60) {
|
|
setUpdateState(UpdateState.Failed)
|
|
appendUpdateLog(
|
|
"ERROR: tailscaled went away but did not come back!"
|
|
)
|
|
appendUpdateLog("ERROR: last error received:")
|
|
appendUpdateLog(err.toString())
|
|
} else {
|
|
timer = setTimeout(poll, 1000)
|
|
}
|
|
})
|
|
}
|
|
|
|
poll()
|
|
|
|
// useEffect cleanup function
|
|
return () => {
|
|
if (timer) clearTimeout(timer)
|
|
timer = undefined
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
return !cv
|
|
? { updateState: UpdateState.UpToDate, updateLog: "" }
|
|
: { updateState, updateLog }
|
|
}
|