You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/client/web/src/components/app.tsx

142 lines
4.3 KiB
TypeScript

import cx from "classnames"
import React, { useEffect } from "react"
import LegacyClientView from "src/components/views/legacy-client-view"
import LoginClientView from "src/components/views/login-client-view"
import ManagementClientView from "src/components/views/management-client-view"
import ReadonlyClientView from "src/components/views/readonly-client-view"
import useAuth, { AuthResponse } from "src/hooks/auth"
import useNodeData, { NodeData, NodeUpdate } from "src/hooks/node-data"
import { ReactComponent as TailscaleIcon } from "src/icons/tailscale-icon.svg"
import ProfilePic from "src/ui/profile-pic"
import { Link, Route, Switch, useLocation } from "wouter"
import DeviceDetailsView from "./views/device-details-view"
export default function App() {
const { data: auth, loading: loadingAuth, newSession } = useAuth()
const { data, refreshData, updateNode } = useNodeData()
useEffect(() => {
refreshData()
}, [auth, refreshData])
return (
<main className="min-w-sm max-w-lg mx-auto py-14 px-5">
{loadingAuth || !data ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : (
<>
{/* TODO(sonia): get rid of the conditions here once full/readonly
* views live on same components */}
{data.DebugMode === "full" && auth?.ok && <Header node={data} />}
<Switch>
<Route path="/">
<HomeView
auth={auth}
data={data}
newSession={newSession}
refreshData={refreshData}
updateNode={updateNode}
/>
</Route>
{data.DebugMode !== "" && (
<>
<Route path="/details">
<DeviceDetailsView node={data} />
</Route>
<Route path="/subnets">{/* TODO */}Subnet router</Route>
<Route path="/ssh">{/* TODO */}Tailscale SSH server</Route>
<Route path="/serve">{/* TODO */}Share local content</Route>
</>
)}
<Route>
<h2 className="mt-8">Page not found</h2>
</Route>
</Switch>
</>
)}
</main>
)
}
function HomeView({
auth,
data,
newSession,
refreshData,
updateNode,
}: {
auth?: AuthResponse
data: NodeData
newSession: () => Promise<void>
refreshData: () => Promise<void>
updateNode: (update: NodeUpdate) => Promise<void> | undefined
}) {
return (
<>
{data?.Status === "NeedsLogin" || data?.Status === "NoState" ? (
// Client not on a tailnet, render login.
<LoginClientView
data={data}
onLoginClick={() => updateNode({ Reauthenticate: true })}
/>
) : data.DebugMode === "full" && auth?.ok ? (
// Render new client interface in management mode.
<ManagementClientView node={data} updateNode={updateNode} />
) : data.DebugMode === "login" || data.DebugMode === "full" ? (
// Render new client interface in readonly mode.
<ReadonlyClientView data={data} auth={auth} newSession={newSession} />
) : (
// Render legacy client interface.
<LegacyClientView
data={data}
refreshData={refreshData}
updateNode={updateNode}
/>
)}
{<Footer licensesURL={data.LicensesURL} />}
</>
)
}
function Header({ node }: { node: NodeData }) {
const [loc] = useLocation()
return (
<>
<div className="flex justify-between mb-12">
<TailscaleIcon />
<div className="flex">
<p className="mr-2">{node.Profile.LoginName}</p>
<ProfilePic url={node.Profile.ProfilePicURL} />
</div>
</div>
{loc !== "/" && (
<Link
to="/"
className="text-indigo-500 font-medium leading-snug block mb-[10px]"
>
&larr; Back to {node.DeviceName}
</Link>
)}
</>
)
}
export function Footer({
licensesURL,
className,
}: {
licensesURL: string
className?: string
}) {
return (
<footer className={cx("container max-w-lg mx-auto text-center", className)}>
<a
className="text-xs text-gray-500 hover:text-gray-600"
href={licensesURL}
>
Open Source Licenses
</a>
</footer>
)
}