Skip to content

Commit 9c4364e

Browse files
committed
client/web: copy existing UI to basic react components
This copies the existing go template frontend into very crude react components that will be driven by a simple JSON api for fetching and updating data. For now, this returns a static set of test data. This just implements the simple existing UI, so I've put these all in a "legacy" component, with the expectation that we will rebuild this with more properly defined components, some pulled from corp. Updates tailscale/corp#13775 Signed-off-by: Will Norris <will@tailscale.com>
1 parent ddba482 commit 9c4364e

File tree

6 files changed

+488
-2
lines changed

6 files changed

+488
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ cmd/tailscaled/tailscaled
3838
# Ignore web client node modules
3939
.vite/
4040
client/web/node_modules
41+
client/web/build
4142

4243
/gocross
4344
/dist

client/web/index.html

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
11
<!doctype html>
2+
<html class="bg-gray-50">
3+
<head>
4+
<title>Tailscale</title>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
28
<link rel="stylesheet" type="text/css" href="/src/index.css" />
9+
</head>
10+
<body>
11+
<div class="min-h-screen py-10 flex justify-center items-center" style="display: none">
12+
<div class="max-w-md">
13+
<h3 class="font-semibold text-lg mb-4">Your web browser is unsupported.</h3>
14+
<p class="mb-2">
15+
Update to a modern browser to access the Tailscale web client. You can use
16+
<a class="link" href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>,
17+
<a class="link" href="https://www.microsoft.com/en-us/edge" target="_blank">Edge</a>,
18+
<a class="link" href="https://www.apple.com/safari/" target="_blank">Safari</a>,
19+
or <a class="link" href="https://www.google.com/chrome/" target="_blank">Chrome</a>.</p>
20+
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a></p>
21+
</div>
22+
</div>
23+
<noscript>
24+
<p class="mb-2">You need to enable Javascript to access the Tailscale web client.</p>
25+
<p>If you need any help, feel free to <a href="mailto:support+webclient@tailscale.com" class="link">contact us</a>.</p>
26+
</noscript>
327
<script type="module" src="/src/index.tsx"></script>
4-
</html>
28+
</body>
29+
</html>

client/web/src/components/app.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import React from "react"
2+
import { Footer, Header, IP, State } from "src/components/legacy"
3+
import useNodeData from "src/hooks/node-data"
24

35
export default function App() {
4-
return <div className="text-center">Hello world</div>
6+
const data = useNodeData()
7+
8+
return (
9+
<div className="py-14">
10+
<main className="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl">
11+
<Header data={data} />
12+
<IP data={data} />
13+
<State data={data} />
14+
</main>
15+
<Footer data={data} />
16+
</div>
17+
)
518
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import React from "react"
2+
import { NodeData } from "src/hooks/node-data"
3+
4+
// TODO(tailscale/corp#13775): legacy.tsx contains a set of components
5+
// that (crudely) implement the pre-2023 web client. These are implemented
6+
// purely to ease migration to the new React-based web client, and will
7+
// eventually be completely removed.
8+
9+
export function Header(props: { data: NodeData }) {
10+
const { data } = props
11+
12+
return (
13+
<header className="flex justify-between items-center min-width-0 py-2 mb-8">
14+
<svg
15+
width="26"
16+
height="26"
17+
viewBox="0 0 23 23"
18+
fill="none"
19+
xmlns="http://www.w3.org/2000/svg"
20+
className="flex-shrink-0 mr-4"
21+
>
22+
<circle
23+
opacity="0.2"
24+
cx="3.4"
25+
cy="3.25"
26+
r="2.7"
27+
fill="currentColor"
28+
></circle>
29+
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
30+
<circle
31+
opacity="0.2"
32+
cx="3.4"
33+
cy="19.5"
34+
r="2.7"
35+
fill="currentColor"
36+
></circle>
37+
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
38+
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
39+
<circle
40+
opacity="0.2"
41+
cx="11.5"
42+
cy="3.25"
43+
r="2.7"
44+
fill="currentColor"
45+
></circle>
46+
<circle
47+
opacity="0.2"
48+
cx="19.5"
49+
cy="3.25"
50+
r="2.7"
51+
fill="currentColor"
52+
></circle>
53+
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
54+
<circle
55+
opacity="0.2"
56+
cx="19.5"
57+
cy="19.5"
58+
r="2.7"
59+
fill="currentColor"
60+
></circle>
61+
</svg>
62+
<div className="flex items-center justify-end space-x-2 w-2/3">
63+
{data.Profile && (
64+
<>
65+
<div className="text-right w-full leading-4">
66+
<h4 className="truncate leading-normal">
67+
{data.Profile.LoginName}
68+
</h4>
69+
<div className="text-xs text-gray-500 text-right">
70+
<a href="#" className="hover:text-gray-700 js-loginButton">
71+
Switch account
72+
</a>{" "}
73+
|{" "}
74+
<a href="#" className="hover:text-gray-700 js-loginButton">
75+
Reauthenticate
76+
</a>{" "}
77+
|{" "}
78+
<a href="#" className="hover:text-gray-700 js-logoutButton">
79+
Logout
80+
</a>
81+
</div>
82+
</div>
83+
<div className="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
84+
{data.Profile.ProfilePicURL ? (
85+
<div
86+
className="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
87+
style={{
88+
backgroundImage: `url(${data.Profile.ProfilePicURL})`,
89+
backgroundSize: "cover",
90+
}}
91+
/>
92+
) : (
93+
<div className="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed" />
94+
)}
95+
</div>
96+
</>
97+
)}
98+
</div>
99+
</header>
100+
)
101+
}
102+
103+
export function IP(props: { data: NodeData }) {
104+
const { data } = props
105+
106+
if (!data.IP) {
107+
return null
108+
}
109+
110+
return (
111+
<>
112+
<div className="border border-gray-200 bg-gray-50 rounded-md p-2 pl-3 pr-3 width-full flex items-center justify-between">
113+
<div className="flex items-center min-width-0">
114+
<svg
115+
className="flex-shrink-0 text-gray-600 mr-3 ml-1"
116+
xmlns="http://www.w3.org/2000/svg"
117+
width="20"
118+
height="20"
119+
viewBox="0 0 24 24"
120+
fill="none"
121+
stroke="currentColor"
122+
strokeWidth="2"
123+
strokeLinecap="round"
124+
strokeLinejoin="round"
125+
>
126+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
127+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
128+
<line x1="6" y1="6" x2="6.01" y2="6"></line>
129+
<line x1="6" y1="18" x2="6.01" y2="18"></line>
130+
</svg>
131+
<div>
132+
<h4 className="font-semibold truncate mr-2">{data.DeviceName}</h4>
133+
</div>
134+
</div>
135+
<h5>{data.IP}</h5>
136+
</div>
137+
<p className="mt-1 ml-1 mb-6 text-xs text-gray-600">
138+
Debug info: Tailscale {data.IPNVersion}, tun={data.TUNMode.toString()}
139+
{data.IsSynology && (
140+
<>
141+
, DSM{data.DSMVersion}
142+
{data.TUNMode || (
143+
<>
144+
{" "}
145+
(
146+
<a
147+
href="https://tailscale.com/kb/1152/synology-outbound/"
148+
className="link-underline text-gray-600"
149+
target="_blank"
150+
aria-label="Configure outbound synology traffic"
151+
rel="noopener noreferrer"
152+
>
153+
outgoing access not configured
154+
</a>
155+
)
156+
</>
157+
)}
158+
</>
159+
)}
160+
</p>
161+
</>
162+
)
163+
}
164+
165+
export function State(props: { data: NodeData }) {
166+
const { data } = props
167+
168+
switch (data.Status) {
169+
case "NeedsLogin":
170+
case "NoState":
171+
if (data.IP) {
172+
return (
173+
<>
174+
<div className="mb-6">
175+
<p className="text-gray-700">
176+
Your device's key has expired. Reauthenticate this device by
177+
logging in again, or{" "}
178+
<a
179+
href="https://tailscale.com/kb/1028/key-expiry"
180+
className="link"
181+
target="_blank"
182+
>
183+
learn more
184+
</a>
185+
.
186+
</p>
187+
</div>
188+
<a href="#" className="mb-4 js-loginButton" target="_blank">
189+
<button className="button button-blue w-full">
190+
Reauthenticate
191+
</button>
192+
</a>
193+
</>
194+
)
195+
} else {
196+
return (
197+
<>
198+
<div className="mb-6">
199+
<h3 className="text-3xl font-semibold mb-3">Log in</h3>
200+
<p className="text-gray-700">
201+
Get started by logging in to your Tailscale network.
202+
Or,&nbsp;learn&nbsp;more at{" "}
203+
<a
204+
href="https://tailscale.com/"
205+
className="link"
206+
target="_blank"
207+
>
208+
tailscale.com
209+
</a>
210+
.
211+
</p>
212+
</div>
213+
<a href="#" className="mb-4 js-loginButton" target="_blank">
214+
<button className="button button-blue w-full">Log In</button>
215+
</a>
216+
</>
217+
)
218+
}
219+
case "NeedsMachineAuth":
220+
return (
221+
<div className="mb-4">
222+
This device is authorized, but needs approval from a network admin
223+
before it can connect to the network.
224+
</div>
225+
)
226+
default:
227+
return (
228+
<>
229+
<div className="mb-4">
230+
<p>
231+
You are connected! Access this device over Tailscale using the
232+
device name or IP address above.
233+
</p>
234+
</div>
235+
<div className="mb-4">
236+
<a href="#" className="mb-4 js-advertiseExitNode">
237+
{data.AdvertiseExitNode ? (
238+
<button
239+
className="button button-red button-medium"
240+
id="enabled"
241+
>
242+
Stop advertising Exit Node
243+
</button>
244+
) : (
245+
<button
246+
className="button button-blue button-medium"
247+
id="enabled"
248+
>
249+
Advertise as Exit Node
250+
</button>
251+
)}
252+
</a>
253+
</div>
254+
</>
255+
)
256+
}
257+
}
258+
259+
export function Footer(props: { data: NodeData }) {
260+
const { data } = props
261+
262+
return (
263+
<footer className="container max-w-lg mx-auto text-center">
264+
<a
265+
className="text-xs text-gray-500 hover:text-gray-600"
266+
href={data.LicensesURL}
267+
>
268+
Open Source Licenses
269+
</a>
270+
</footer>
271+
)
272+
}

client/web/src/hooks/node-data.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export type UserProfile = {
2+
LoginName: string
3+
DisplayName: string
4+
ProfilePicURL: string
5+
}
6+
7+
export type NodeData = {
8+
Profile: UserProfile
9+
Status: string
10+
DeviceName: string
11+
IP: string
12+
AdvertiseExitNode: boolean
13+
AdvertiseRoutes: string
14+
LicensesURL: string
15+
TUNMode: boolean
16+
IsSynology: boolean
17+
DSMVersion: number
18+
IsUnraid: boolean
19+
UnraidToken: string
20+
IPNVersion: string
21+
}
22+
23+
// testData is static set of nodedata used during development.
24+
// This can be removed once we have a real node data API.
25+
const testData: NodeData = {
26+
Profile: {
27+
LoginName: "amelie",
28+
DisplayName: "Amelie Pangolin",
29+
ProfilePicURL: "https://login.tailscale.com/logo192.png",
30+
},
31+
Status: "Running",
32+
DeviceName: "amelies-laptop",
33+
IP: "100.1.2.3",
34+
AdvertiseExitNode: false,
35+
AdvertiseRoutes: "",
36+
LicensesURL: "https://tailscale.com/licenses/tailscale",
37+
TUNMode: false,
38+
IsSynology: true,
39+
DSMVersion: 7,
40+
IsUnraid: false,
41+
UnraidToken: "",
42+
IPNVersion: "0.1.0",
43+
}
44+
45+
// useNodeData returns basic data about the current node.
46+
export default function useNodeData() {
47+
return testData
48+
}

0 commit comments

Comments
 (0)