diff --git a/.gitignore b/.gitignore index 64ccd27fe..72fcb3190 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ cmd/tailscaled/tailscaled # Ignore web client node modules .vite/ client/web/node_modules +client/web/build /gocross /dist diff --git a/client/web/index.html b/client/web/index.html index 854c39a5c..062dfd185 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -1,4 +1,29 @@ + + + Tailscale + + + + + + + - \ No newline at end of file + + diff --git a/client/web/src/components/app.tsx b/client/web/src/components/app.tsx index 709dc4781..b0e0ceed0 100644 --- a/client/web/src/components/app.tsx +++ b/client/web/src/components/app.tsx @@ -1,5 +1,18 @@ import React from "react" +import { Footer, Header, IP, State } from "src/components/legacy" +import useNodeData from "src/hooks/node-data" export default function App() { - return
Hello world
+ const data = useNodeData() + + return ( +
+
+
+ + +
+
+ ) } diff --git a/client/web/src/components/legacy.tsx b/client/web/src/components/legacy.tsx new file mode 100644 index 000000000..01cd95c94 --- /dev/null +++ b/client/web/src/components/legacy.tsx @@ -0,0 +1,272 @@ +import React from "react" +import { NodeData } from "src/hooks/node-data" + +// TODO(tailscale/corp#13775): legacy.tsx contains a set of components +// that (crudely) implement the pre-2023 web client. These are implemented +// purely to ease migration to the new React-based web client, and will +// eventually be completely removed. + +export function Header(props: { data: NodeData }) { + const { data } = props + + return ( +
+ + + + + + + + + + + +
+ {data.Profile && ( + <> +
+

+ {data.Profile.LoginName} +

+
+ + Switch account + {" "} + |{" "} + + Reauthenticate + {" "} + |{" "} + + Logout + +
+
+
+ {data.Profile.ProfilePicURL ? ( +
+ ) : ( +
+ )} +
+ + )} +
+
+ ) +} + +export function IP(props: { data: NodeData }) { + const { data } = props + + if (!data.IP) { + return null + } + + return ( + <> +
+
+ + + + + + +
+

{data.DeviceName}

+
+
+
{data.IP}
+
+

+ Debug info: Tailscale {data.IPNVersion}, tun={data.TUNMode.toString()} + {data.IsSynology && ( + <> + , DSM{data.DSMVersion} + {data.TUNMode || ( + <> + {" "} + ( + + outgoing access not configured + + ) + + )} + + )} +

+ + ) +} + +export function State(props: { data: NodeData }) { + const { data } = props + + switch (data.Status) { + case "NeedsLogin": + case "NoState": + if (data.IP) { + return ( + <> +
+

+ Your device's key has expired. Reauthenticate this device by + logging in again, or{" "} + + learn more + + . +

+
+ + + + + ) + } else { + return ( + <> +
+

Log in

+

+ Get started by logging in to your Tailscale network. + Or, learn more at{" "} + + tailscale.com + + . +

+
+ + + + + ) + } + case "NeedsMachineAuth": + return ( +
+ This device is authorized, but needs approval from a network admin + before it can connect to the network. +
+ ) + default: + return ( + <> +
+

+ You are connected! Access this device over Tailscale using the + device name or IP address above. +

+
+
+ + {data.AdvertiseExitNode ? ( + + ) : ( + + )} + +
+ + ) + } +} + +export function Footer(props: { data: NodeData }) { + const { data } = props + + return ( + + ) +} diff --git a/client/web/src/hooks/node-data.ts b/client/web/src/hooks/node-data.ts new file mode 100644 index 000000000..fd30b1c2e --- /dev/null +++ b/client/web/src/hooks/node-data.ts @@ -0,0 +1,48 @@ +export type UserProfile = { + LoginName: string + DisplayName: string + ProfilePicURL: string +} + +export type NodeData = { + Profile: UserProfile + Status: string + DeviceName: string + IP: string + AdvertiseExitNode: boolean + AdvertiseRoutes: string + LicensesURL: string + TUNMode: boolean + IsSynology: boolean + DSMVersion: number + IsUnraid: boolean + UnraidToken: string + IPNVersion: string +} + +// testData is static set of nodedata used during development. +// This can be removed once we have a real node data API. +const testData: NodeData = { + Profile: { + LoginName: "amelie", + DisplayName: "Amelie Pangolin", + ProfilePicURL: "https://login.tailscale.com/logo192.png", + }, + Status: "Running", + DeviceName: "amelies-laptop", + IP: "100.1.2.3", + AdvertiseExitNode: false, + AdvertiseRoutes: "", + LicensesURL: "https://tailscale.com/licenses/tailscale", + TUNMode: false, + IsSynology: true, + DSMVersion: 7, + IsUnraid: false, + UnraidToken: "", + IPNVersion: "0.1.0", +} + +// useNodeData returns basic data about the current node. +export default function useNodeData() { + return testData +} diff --git a/client/web/src/index.css b/client/web/src/index.css index b5c61c956..cb02a1161 100644 --- a/client/web/src/index.css +++ b/client/web/src/index.css @@ -1,3 +1,130 @@ @tailwind base; @tailwind components; @tailwind utilities; + +/** + * Non-Tailwind styles begin here. + */ + +.bg-gray-0 { + --tw-bg-opacity: 1; + background-color: rgba(250, 249, 248, var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgba(249, 247, 246, var(--tw-bg-opacity)); +} + +html { + letter-spacing: -0.015em; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.link { + --text-opacity: 1; + color: #4b70cc; + color: rgba(75, 112, 204, var(--text-opacity)); +} + +.link:hover, +.link:active { + --text-opacity: 1; + color: #19224a; + color: rgba(25, 34, 74, var(--text-opacity)); +} + +.link-underline { + text-decoration: underline; +} + +.link-underline:hover, +.link-underline:active { + text-decoration: none; +} + +.link-muted { + /* same as text-gray-500 */ + --tw-text-opacity: 1; + color: rgba(112, 110, 109, var(--tw-text-opacity)); +} + +.link-muted:hover, +.link-muted:active { + /* same as text-gray-500 */ + --tw-text-opacity: 1; + color: rgba(68, 67, 66, var(--tw-text-opacity)); +} + +.button { + font-weight: 500; + padding-top: 0.45rem; + padding-bottom: 0.45rem; + padding-left: 1rem; + padding-right: 1rem; + border-radius: 0.375rem; + border-width: 1px; + border-color: transparent; + transition-property: background-color, border-color, color, box-shadow; + transition-duration: 120ms; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + min-width: 80px; +} + +.button:focus { + outline: 0; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5); +} + +.button:disabled { + cursor: not-allowed; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.button-blue { + --bg-opacity: 1; + background-color: #4b70cc; + background-color: rgba(75, 112, 204, var(--bg-opacity)); + --border-opacity: 1; + border-color: #4b70cc; + border-color: rgba(75, 112, 204, var(--border-opacity)); + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); +} + +.button-blue:enabled:hover { + --bg-opacity: 1; + background-color: #3f5db3; + background-color: rgba(63, 93, 179, var(--bg-opacity)); + --border-opacity: 1; + border-color: #3f5db3; + border-color: rgba(63, 93, 179, var(--border-opacity)); +} + +.button-blue:disabled { + --text-opacity: 1; + color: #cedefd; + color: rgba(206, 222, 253, var(--text-opacity)); + --bg-opacity: 1; + background-color: #6c94ec; + background-color: rgba(108, 148, 236, var(--bg-opacity)); + --border-opacity: 1; + border-color: #6c94ec; + border-color: rgba(108, 148, 236, var(--border-opacity)); +} + +.button-red { + background-color: #d04841; + border-color: #d04841; + color: #fff; +} + +.button-red:enabled:hover { + background-color: #b22d30; + border-color: #b22d30; +}