2023-10-25 17:58:53 -04:00
|
|
|
import cx from "classnames"
|
2023-11-06 19:01:16 -05:00
|
|
|
import React, { useEffect } from "react"
|
2023-10-25 17:58:53 -04:00
|
|
|
import LegacyClientView from "src/components/views/legacy-client-view"
|
|
|
|
import LoginClientView from "src/components/views/login-client-view"
|
2023-11-03 20:19:20 -04:00
|
|
|
import ManagementClientView from "src/components/views/management-client-view"
|
2023-10-25 17:58:53 -04:00
|
|
|
import ReadonlyClientView from "src/components/views/readonly-client-view"
|
2023-11-07 10:55:55 -05:00
|
|
|
import useAuth, { AuthResponse } from "src/hooks/auth"
|
2023-11-06 19:01:16 -05:00
|
|
|
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"
|
2023-08-11 10:53:27 -04:00
|
|
|
|
|
|
|
export default function App() {
|
2023-11-07 10:55:55 -05:00
|
|
|
const { data: auth, loading: loadingAuth, newSession } = useAuth()
|
2023-11-06 19:01:16 -05:00
|
|
|
const { data, refreshData, updateNode } = useNodeData()
|
|
|
|
useEffect(() => {
|
|
|
|
refreshData()
|
|
|
|
}, [auth, refreshData])
|
2023-08-15 08:24:32 -07:00
|
|
|
|
2023-10-25 17:58:53 -04:00
|
|
|
return (
|
2023-11-06 19:01:16 -05:00
|
|
|
<main className="min-w-sm max-w-lg mx-auto py-14 px-5">
|
|
|
|
{loadingAuth || !data ? (
|
2023-11-02 11:28:07 -07:00
|
|
|
<div className="text-center py-14">Loading...</div> // TODO(sonia): add a loading view
|
|
|
|
) : (
|
2023-11-06 19:01:16 -05:00
|
|
|
<>
|
|
|
|
{/* 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}
|
2023-11-07 10:55:55 -05:00
|
|
|
newSession={newSession}
|
2023-11-06 19:01:16 -05:00
|
|
|
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>
|
|
|
|
</>
|
2023-11-02 11:28:07 -07:00
|
|
|
)}
|
2023-11-03 20:46:30 -04:00
|
|
|
</main>
|
2023-11-02 11:28:07 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-11-03 20:46:30 -04:00
|
|
|
function HomeView({
|
2023-11-02 11:28:07 -07:00
|
|
|
auth,
|
2023-11-06 19:01:16 -05:00
|
|
|
data,
|
2023-11-07 10:55:55 -05:00
|
|
|
newSession,
|
2023-11-06 19:01:16 -05:00
|
|
|
refreshData,
|
|
|
|
updateNode,
|
2023-11-02 11:28:07 -07:00
|
|
|
}: {
|
|
|
|
auth?: AuthResponse
|
2023-11-06 19:01:16 -05:00
|
|
|
data: NodeData
|
2023-11-07 10:55:55 -05:00
|
|
|
newSession: () => Promise<void>
|
2023-11-06 19:01:16 -05:00
|
|
|
refreshData: () => Promise<void>
|
2023-11-07 11:16:23 -05:00
|
|
|
updateNode: (update: NodeUpdate) => Promise<void> | undefined
|
2023-11-02 11:28:07 -07:00
|
|
|
}) {
|
|
|
|
return (
|
|
|
|
<>
|
2023-11-06 19:01:16 -05:00
|
|
|
{data?.Status === "NeedsLogin" || data?.Status === "NoState" ? (
|
2023-10-25 17:58:53 -04:00
|
|
|
// Client not on a tailnet, render login.
|
|
|
|
<LoginClientView
|
|
|
|
data={data}
|
|
|
|
onLoginClick={() => updateNode({ Reauthenticate: true })}
|
|
|
|
/>
|
2023-11-01 10:35:46 -07:00
|
|
|
) : data.DebugMode === "full" && auth?.ok ? (
|
|
|
|
// Render new client interface in management mode.
|
2023-11-07 11:16:23 -05:00
|
|
|
<ManagementClientView node={data} updateNode={updateNode} />
|
2023-10-25 17:58:53 -04:00
|
|
|
) : data.DebugMode === "login" || data.DebugMode === "full" ? (
|
2023-11-01 10:35:46 -07:00
|
|
|
// Render new client interface in readonly mode.
|
2023-11-07 10:55:55 -05:00
|
|
|
<ReadonlyClientView data={data} auth={auth} newSession={newSession} />
|
2023-10-25 17:58:53 -04:00
|
|
|
) : (
|
|
|
|
// Render legacy client interface.
|
|
|
|
<LegacyClientView
|
|
|
|
data={data}
|
|
|
|
refreshData={refreshData}
|
|
|
|
updateNode={updateNode}
|
|
|
|
/>
|
|
|
|
)}
|
2023-11-06 19:01:16 -05:00
|
|
|
{<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]"
|
|
|
|
>
|
|
|
|
← Back to {node.DeviceName}
|
|
|
|
</Link>
|
|
|
|
)}
|
2023-11-02 11:28:07 -07:00
|
|
|
</>
|
2023-09-26 15:57:40 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-11-06 19:01:16 -05:00
|
|
|
export function Footer({
|
|
|
|
licensesURL,
|
|
|
|
className,
|
|
|
|
}: {
|
|
|
|
licensesURL: string
|
|
|
|
className?: string
|
|
|
|
}) {
|
2023-09-26 15:57:40 -04:00
|
|
|
return (
|
2023-11-06 19:01:16 -05:00
|
|
|
<footer className={cx("container max-w-lg mx-auto text-center", className)}>
|
2023-10-25 17:58:53 -04:00
|
|
|
<a
|
|
|
|
className="text-xs text-gray-500 hover:text-gray-600"
|
2023-11-06 19:01:16 -05:00
|
|
|
href={licensesURL}
|
2023-10-25 17:58:53 -04:00
|
|
|
>
|
|
|
|
Open Source Licenses
|
|
|
|
</a>
|
|
|
|
</footer>
|
2023-08-15 08:24:32 -07:00
|
|
|
)
|
2023-08-11 10:53:27 -04:00
|
|
|
}
|