client/web: add device details view

Initial addition of device details view on the frontend. A little
more backend piping work to come to fill all of the detail fields,
for now using placeholders.

Updates tailscale/corp#14335

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
Sonia Appasamy
2023-11-06 19:01:16 -05:00
committed by Sonia Appasamy
parent 3e9026efda
commit d73e923b73
5 changed files with 242 additions and 52 deletions

View File

@ -1,31 +1,57 @@
import cx from "classnames"
import React from "react"
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, SessionsCallbacks } from "src/hooks/auth"
import useNodeData from "src/hooks/node-data"
import { Route, Switch } from "wouter"
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, sessions } = useAuth()
const { data, refreshData, updateNode } = useNodeData()
useEffect(() => {
refreshData()
}, [auth, refreshData])
return (
<main className="flex flex-col items-center min-w-sm max-w-lg mx-auto py-14">
{loadingAuth ? (
<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
) : (
<Switch>
<Route path="/">
<HomeView auth={auth} sessions={sessions} />
</Route>
<Route path="/details">{/* TODO */}Device details</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>Page not found</Route>
</Switch>
<>
{/* 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}
sessions={sessions}
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>
)
@ -33,18 +59,20 @@ export default function App() {
function HomeView({
auth,
data,
sessions,
refreshData,
updateNode,
}: {
auth?: AuthResponse
data: NodeData
sessions: SessionsCallbacks
refreshData: () => Promise<void>
updateNode: (update: NodeUpdate) => void
}) {
const { data, refreshData, updateNode } = useNodeData()
return (
<>
{!data ? (
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
) : data?.Status === "NeedsLogin" || data?.Status === "NoState" ? (
{data?.Status === "NeedsLogin" || data?.Status === "NoState" ? (
// Client not on a tailnet, render login.
<LoginClientView
data={data}
@ -64,19 +92,47 @@ function HomeView({
updateNode={updateNode}
/>
)}
{data && <Footer licensesURL={data.LicensesURL} />}
{<Footer licensesURL={data.LicensesURL} />}
</>
)
}
export function Footer(props: { licensesURL: string; className?: string }) {
function Header({ node }: { node: NodeData }) {
const [loc] = useLocation()
return (
<footer
className={cx("container max-w-lg mx-auto text-center", props.className)}
>
<>
<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={props.licensesURL}
href={licensesURL}
>
Open Source Licenses
</a>