client/{tailscale,web}: add initial webUI frontend for self-updates (#10191)
Updates #10187. Signed-off-by: Naman Sood <mail@nsood.in>
This commit is contained in:
@ -6,6 +6,7 @@ import HomeView from "src/components/views/home-view"
|
||||
import LegacyClientView from "src/components/views/legacy-client-view"
|
||||
import LoginClientView from "src/components/views/login-client-view"
|
||||
import SSHView from "src/components/views/ssh-view"
|
||||
import { UpdatingView } from "src/components/views/updating-view"
|
||||
import useAuth, { AuthResponse } from "src/hooks/auth"
|
||||
import useNodeData, { NodeData } from "src/hooks/node-data"
|
||||
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
|
||||
@ -81,6 +82,12 @@ function WebClient({
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/serve">{/* TODO */}Share local content</Route>
|
||||
<Route path="/update">
|
||||
<UpdatingView
|
||||
versionInfo={data.ClientVersion}
|
||||
currentVersion={data.IPNVersion}
|
||||
/>
|
||||
</Route>
|
||||
<Route>
|
||||
<h2 className="mt-8">Page not found</h2>
|
||||
</Route>
|
||||
@ -112,7 +119,7 @@ function Header({
|
||||
</div>
|
||||
<LoginToggle node={node} auth={auth} newSession={newSession} />
|
||||
</div>
|
||||
{loc !== "/" && (
|
||||
{loc !== "/" && loc !== "/update" && (
|
||||
<Link
|
||||
to="/"
|
||||
className="text-indigo-500 font-medium leading-snug block mb-[10px]"
|
||||
|
59
client/web/src/components/update-available.tsx
Normal file
59
client/web/src/components/update-available.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React from "react"
|
||||
import { VersionInfo } from "src/hooks/self-update"
|
||||
import { Link } from "wouter"
|
||||
|
||||
export function UpdateAvailableNotification({
|
||||
details,
|
||||
}: {
|
||||
details: VersionInfo
|
||||
}) {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2 className="mb-2">
|
||||
Update available{" "}
|
||||
{details.LatestVersion && `(v${details.LatestVersion})`}
|
||||
</h2>
|
||||
<p className="text-sm mb-1 mt-1">
|
||||
{details.LatestVersion
|
||||
? `Version ${details.LatestVersion}`
|
||||
: "A new update"}{" "}
|
||||
is now available. <ChangelogText version={details.LatestVersion} />
|
||||
</p>
|
||||
<Link
|
||||
className="button button-blue mt-3 text-sm inline-block"
|
||||
to="/update"
|
||||
>
|
||||
Update now
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// isStableTrack takes a Tailscale version string
|
||||
// of form X.Y.Z (or vX.Y.Z) and returns whether
|
||||
// it is a stable release (even value of Y)
|
||||
// or unstable (odd value of Y).
|
||||
// eg. isStableTrack("1.48.0") === true
|
||||
// eg. isStableTrack("1.49.112") === false
|
||||
function isStableTrack(ver: string): boolean {
|
||||
const middle = ver.split(".")[1]
|
||||
if (middle && Number(middle) % 2 === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function ChangelogText({ version }: { version?: string }) {
|
||||
if (!version || !isStableTrack(version)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
Check out the{" "}
|
||||
<a href="https://tailscale.com/changelog/" className="link">
|
||||
release notes
|
||||
</a>{" "}
|
||||
to find out what's new!
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import cx from "classnames"
|
||||
import React from "react"
|
||||
import { apiFetch } from "src/api"
|
||||
import { UpdateAvailableNotification } from "src/components/update-available"
|
||||
import { NodeData } from "src/hooks/node-data"
|
||||
import { useLocation } from "wouter"
|
||||
import ACLTag from "../acl-tag"
|
||||
@ -45,6 +46,11 @@ export default function DeviceDetailsView({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{node.ClientVersion &&
|
||||
!node.ClientVersion.RunningLatest &&
|
||||
!readonly && (
|
||||
<UpdateAvailableNotification details={node.ClientVersion} />
|
||||
)}
|
||||
<div className="card">
|
||||
<h2 className="mb-2">General</h2>
|
||||
<table>
|
||||
|
90
client/web/src/components/views/updating-view.tsx
Normal file
90
client/web/src/components/views/updating-view.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from "react"
|
||||
import { ChangelogText } from "src/components/update-available"
|
||||
import {
|
||||
UpdateState,
|
||||
useInstallUpdate,
|
||||
VersionInfo,
|
||||
} from "src/hooks/self-update"
|
||||
import { ReactComponent as CheckCircleIcon } from "src/icons/check-circle.svg"
|
||||
import { ReactComponent as XCircleIcon } from "src/icons/x-circle.svg"
|
||||
import Spinner from "src/ui/spinner"
|
||||
import { Link } from "wouter"
|
||||
|
||||
/**
|
||||
* UpdatingView is rendered when the user initiates a Tailscale update, and
|
||||
* the update is in-progress, failed, or completed.
|
||||
*/
|
||||
export function UpdatingView({
|
||||
versionInfo,
|
||||
currentVersion,
|
||||
}: {
|
||||
versionInfo?: VersionInfo
|
||||
currentVersion: string
|
||||
}) {
|
||||
const { updateState, updateLog } = useInstallUpdate(
|
||||
currentVersion,
|
||||
versionInfo
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 flex flex-col justify-center items-center text-center mt-56">
|
||||
{updateState === UpdateState.InProgress ? (
|
||||
<>
|
||||
<Spinner size="sm" className="text-gray-400" />
|
||||
<h1 className="text-2xl m-3">Update in progress</h1>
|
||||
<p className="text-gray-400">
|
||||
The update shouldn't take more than a couple of minutes. Once it's
|
||||
completed, you will be asked to log in again.
|
||||
</p>
|
||||
</>
|
||||
) : updateState === UpdateState.Complete ? (
|
||||
<>
|
||||
<CheckCircleIcon />
|
||||
<h1 className="text-2xl m-3">Update complete!</h1>
|
||||
<p className="text-gray-400">
|
||||
You updated Tailscale
|
||||
{versionInfo && versionInfo.LatestVersion
|
||||
? ` to ${versionInfo.LatestVersion}`
|
||||
: null}
|
||||
. <ChangelogText version={versionInfo?.LatestVersion} />
|
||||
</p>
|
||||
<Link className="button button-blue text-sm m-3" to="/">
|
||||
Log in to access
|
||||
</Link>
|
||||
</>
|
||||
) : updateState === UpdateState.UpToDate ? (
|
||||
<>
|
||||
<CheckCircleIcon />
|
||||
<h1 className="text-2xl m-3">Up to date!</h1>
|
||||
<p className="text-gray-400">
|
||||
You are already running Tailscale {currentVersion}, which is the
|
||||
newest version available.
|
||||
</p>
|
||||
<Link className="button button-blue text-sm m-3" to="/">
|
||||
Return
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
/* TODO(naman,sonia): Figure out the body copy and design for this view. */
|
||||
<>
|
||||
<XCircleIcon />
|
||||
<h1 className="text-2xl m-3">Update failed</h1>
|
||||
<p className="text-gray-400">
|
||||
Update
|
||||
{versionInfo && versionInfo.LatestVersion
|
||||
? ` to ${versionInfo.LatestVersion}`
|
||||
: null}{" "}
|
||||
failed.
|
||||
</p>
|
||||
<Link className="button button-blue text-sm m-3" to="/">
|
||||
Return
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<pre className="h-64 overflow-scroll m-3">
|
||||
<code>{updateLog}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user