mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-14 10:07:24 +02:00
Add confirmation dialogs for disconnecting and stopping advertisement of a subnet route. Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
171 lines
4.8 KiB
TypeScript
171 lines
4.8 KiB
TypeScript
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
import React from "react"
|
|
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
|
|
import LoginToggle from "src/components/login-toggle"
|
|
import DeviceDetailsView from "src/components/views/device-details-view"
|
|
import HomeView from "src/components/views/home-view"
|
|
import LoginView from "src/components/views/login-view"
|
|
import SSHView from "src/components/views/ssh-view"
|
|
import SubnetRouterView from "src/components/views/subnet-router-view"
|
|
import { UpdatingView } from "src/components/views/updating-view"
|
|
import useAuth, { AuthResponse } from "src/hooks/auth"
|
|
import { Feature, featureDescription, NodeData } from "src/types"
|
|
import Card from "src/ui/card"
|
|
import EmptyState from "src/ui/empty-state"
|
|
import LoadingDots from "src/ui/loading-dots"
|
|
import useSWR from "swr"
|
|
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
|
|
|
export default function App() {
|
|
const { data: auth, loading: loadingAuth, newSession } = useAuth()
|
|
|
|
return (
|
|
<main className="min-w-sm max-w-lg mx-auto py-4 sm:py-14 px-5">
|
|
{loadingAuth || !auth ? (
|
|
<LoadingView />
|
|
) : (
|
|
<WebClient auth={auth} newSession={newSession} />
|
|
)}
|
|
</main>
|
|
)
|
|
}
|
|
|
|
function WebClient({
|
|
auth,
|
|
newSession,
|
|
}: {
|
|
auth: AuthResponse
|
|
newSession: () => Promise<void>
|
|
}) {
|
|
const { data: node } = useSWR<NodeData>("/data")
|
|
|
|
return !node ? (
|
|
<LoadingView />
|
|
) : node.Status === "NeedsLogin" ||
|
|
node.Status === "NoState" ||
|
|
node.Status === "Stopped" ? (
|
|
// Client not on a tailnet, render login.
|
|
<LoginView data={node} />
|
|
) : (
|
|
// Otherwise render the new web client.
|
|
<>
|
|
<Router base={node.URLPrefix}>
|
|
<Header node={node} auth={auth} newSession={newSession} />
|
|
<Switch>
|
|
<Route path="/">
|
|
<HomeView readonly={!auth.canManageNode} node={node} />
|
|
</Route>
|
|
<Route path="/details">
|
|
<DeviceDetailsView readonly={!auth.canManageNode} node={node} />
|
|
</Route>
|
|
<FeatureRoute path="/subnets" feature="advertise-routes" node={node}>
|
|
<SubnetRouterView readonly={!auth.canManageNode} node={node} />
|
|
</FeatureRoute>
|
|
<FeatureRoute path="/ssh" feature="ssh" node={node}>
|
|
<SSHView readonly={!auth.canManageNode} node={node} />
|
|
</FeatureRoute>
|
|
{/* <Route path="/serve">Share local content</Route> */}
|
|
<FeatureRoute path="/update" feature="auto-update" node={node}>
|
|
<UpdatingView
|
|
versionInfo={node.ClientVersion}
|
|
currentVersion={node.IPNVersion}
|
|
/>
|
|
</FeatureRoute>
|
|
<Route path="/disconnected">
|
|
<Card className="mt-8">
|
|
<EmptyState description="You have been disconnected" />
|
|
</Card>
|
|
</Route>
|
|
<Route>
|
|
<Card className="mt-8">
|
|
<EmptyState description="Page not found" />
|
|
</Card>
|
|
</Route>
|
|
</Switch>
|
|
</Router>
|
|
</>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* FeatureRoute renders a Route component,
|
|
* but only displays the child view if the specified feature is
|
|
* available for use on this node's platform. If not available,
|
|
* a not allowed view is rendered instead.
|
|
*/
|
|
function FeatureRoute({
|
|
path,
|
|
node,
|
|
feature,
|
|
children,
|
|
}: {
|
|
path: string
|
|
node: NodeData
|
|
feature: Feature
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<Route path={path}>
|
|
{!node.Features[feature] ? (
|
|
<Card className="mt-8">
|
|
<EmptyState
|
|
description={`${featureDescription(
|
|
feature
|
|
)} not available on this device.`}
|
|
/>
|
|
</Card>
|
|
) : (
|
|
children
|
|
)}
|
|
</Route>
|
|
)
|
|
}
|
|
|
|
function Header({
|
|
node,
|
|
auth,
|
|
newSession,
|
|
}: {
|
|
node: NodeData
|
|
auth: AuthResponse
|
|
newSession: () => Promise<void>
|
|
}) {
|
|
const [loc] = useLocation()
|
|
|
|
if (loc === "/disconnected") {
|
|
// No header on view presented after logout.
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-wrap gap-4 justify-between items-center mb-9 md:mb-12">
|
|
<Link to="/" className="flex gap-3 overflow-hidden">
|
|
<TailscaleIcon />
|
|
<div className="inline text-gray-800 text-lg font-medium leading-snug truncate">
|
|
{node.DomainName}
|
|
</div>
|
|
</Link>
|
|
<LoginToggle node={node} auth={auth} newSession={newSession} />
|
|
</div>
|
|
{loc !== "/" && loc !== "/update" && (
|
|
<Link to="/" className="link font-medium block mb-2">
|
|
← Back to {node.DeviceName}
|
|
</Link>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* LoadingView fills its container with small animated loading dots
|
|
* in the center.
|
|
*/
|
|
export function LoadingView() {
|
|
return (
|
|
<LoadingDots className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
)
|
|
}
|