templates: consolidate shared page and component helpers

This commit is contained in:
Kristoffer Dalby
2026-06-16 08:21:29 +00:00
parent 8c10a96921
commit 468f70a9e5
9 changed files with 332 additions and 346 deletions
+169 -164
View File
@@ -1,191 +1,196 @@
package templates
import (
"fmt"
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/chasefleming/elem-go/styles"
)
func Apple(url string) *elem.Element {
return HtmlStructure(
elem.Title(nil,
elem.Text("headscale - Apple")),
mdTypesetBody(
headscaleLogo(),
H1(elem.Text("iOS configuration")),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Install the official Tailscale iOS client from the "),
externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"),
),
elem.Li(
nil,
elem.Text("Open the "),
elem.Strong(nil, elem.Text("Tailscale")),
elem.Text(" app"),
),
elem.Li(
nil,
elem.Text("Click the account icon in the top-right corner and select "),
elem.Strong(nil, elem.Text("Log in…")),
),
elem.Li(
nil,
elem.Text("Tap the top-right options menu button and select "),
elem.Strong(nil, elem.Text("Use custom coordination server")),
),
elem.Li(
nil,
elem.Text("Enter your instance URL: "),
Code(elem.Text(url)),
),
elem.Li(
nil,
elem.Text(
"Enter your credentials and log in. Headscale should now be working on your iOS device",
),
),
return page(
"headscale - Apple",
H1(elem.Text("iOS configuration")),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Install the official Tailscale iOS client from the "),
externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"),
),
H1(elem.Text("macOS configuration")),
H2(elem.Text("Command line")),
P(
elem.Text("Use Tailscale's login command to add your profile:"),
elem.Li(
nil,
elem.Text("Open the "),
elem.Strong(nil, elem.Text("Tailscale")),
elem.Text(" app"),
),
Pre(PreCode("tailscale login --login-server "+url)),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Option + Click the "),
elem.Strong(nil, elem.Text("Tailscale")),
elem.Text(" icon in the menu and hover over the "),
elem.Strong(nil, elem.Text("Debug")),
elem.Text(" menu"),
),
elem.Li(nil,
elem.Text("Under "),
elem.Strong(nil, elem.Text("Custom Login Server")),
elem.Text(", select "),
elem.Strong(nil, elem.Text("Add Account...")),
),
elem.Li(
nil,
elem.Text("Enter "),
Code(elem.Text(url)),
elem.Text(" of the headscale instance and press "),
elem.Strong(nil, elem.Text("Add Account")),
),
elem.Li(nil,
elem.Text("Follow the login procedure in the browser"),
),
elem.Li(
nil,
elem.Text("Click the account icon in the top-right corner and select "),
elem.Strong(nil, elem.Text("Log in…")),
),
H2(elem.Text("Profiles")),
P(
elem.Li(
nil,
elem.Text("Tap the top-right options menu button and select "),
elem.Strong(nil, elem.Text("Use custom coordination server")),
),
elem.Li(
nil,
elem.Text("Enter your instance URL: "),
Code(elem.Text(url)),
),
elem.Li(
nil,
elem.Text(
"Headscale can be set to the default server by installing a Headscale configuration profile:",
"Enter your credentials and log in. Headscale should now be working on your iOS device",
),
),
elem.Div(attrs.Props{attrs.Style: styles.Props{styles.MarginTop: spaceL, styles.MarginBottom: spaceL}.ToInline()},
downloadButton("/apple/macos-app-store", "macOS AppStore profile"),
downloadButton("/apple/macos-standalone", "macOS Standalone profile"),
),
H1(elem.Text("macOS configuration")),
H2(elem.Text("Command line")),
P(
elem.Text("Use Tailscale's login command to add your profile:"),
),
codeBlockText("tailscale login --login-server "+url),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Option + Click the "),
elem.Strong(nil, elem.Text("Tailscale")),
elem.Text(" icon in the menu and hover over the "),
elem.Strong(nil, elem.Text("Debug")),
elem.Text(" menu"),
),
Ol(
elem.Li(
nil,
elem.Text(
"Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed",
),
),
elem.Li(nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("System Preferences")),
elem.Text(" and go to "),
elem.Strong(nil, elem.Text("Profiles")),
),
elem.Li(nil,
elem.Text("Find and install the "),
elem.Strong(nil, elem.Text("Headscale")),
elem.Text(" profile"),
),
elem.Li(nil,
elem.Text("Restart "),
elem.Strong(nil, elem.Text("Tailscale.app")),
elem.Text(" and log in"),
),
elem.Li(
nil,
elem.Text("Under "),
elem.Strong(nil, elem.Text("Custom Login Server")),
elem.Text(", select "),
elem.Strong(nil, elem.Text("Add Account...")),
),
orDivider(),
P(
elem.Li(
nil,
elem.Text("Enter "),
Code(elem.Text(url)),
elem.Text(" of the headscale instance and press "),
elem.Strong(nil, elem.Text("Add Account")),
),
elem.Li(
nil,
elem.Text("Follow the login procedure in the browser"),
),
),
H2(elem.Text("Profiles")),
P(
elem.Text(
"Headscale can be set to the default server by installing a Headscale configuration profile:",
),
),
elem.Div(
attrs.Props{attrs.Style: styles.Props{styles.MarginTop: spaceL, styles.MarginBottom: spaceL}.ToInline()},
downloadButton("/apple/macos-app-store", "macOS AppStore profile"),
downloadButton("/apple/macos-standalone", "macOS Standalone profile"),
),
Ol(
elem.Li(
nil,
elem.Text(
"Use your terminal to configure the default setting for Tailscale by issuing one of the following commands:",
"Download the profile, then open it. When it has been opened, there should be a notification that a profile can be installed",
),
),
P(elem.Text("For app store client:")),
Pre(PreCode("defaults write io.tailscale.ipn.macos ControlURL "+url)),
P(elem.Text("For standalone client:")),
Pre(PreCode("defaults write io.tailscale.ipn.macsys ControlURL "+url)),
P(
elem.Li(
nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("System Preferences")),
elem.Text(" and go to "),
elem.Strong(nil, elem.Text("Profiles")),
),
elem.Li(
nil,
elem.Text("Find and install the "),
elem.Strong(nil, elem.Text("Headscale")),
elem.Text(" profile"),
),
elem.Li(
nil,
elem.Text("Restart "),
elem.Strong(nil, elem.Text("Tailscale.app")),
elem.Text(" and log in."),
elem.Text(" and log in"),
),
warningBox("Caution", "You should always download and inspect the profile before installing it."),
P(elem.Text("For app store client:")),
Pre(PreCode(fmt.Sprintf(`curl %s/apple/macos-app-store`, url))),
P(elem.Text("For standalone client:")),
Pre(PreCode(fmt.Sprintf(`curl %s/apple/macos-standalone`, url))),
H1(elem.Text("tvOS configuration")),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Install the official Tailscale tvOS client from the "),
externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"),
),
elem.Li(
nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("Settings")),
elem.Text(" (the Apple tvOS settings) > "),
elem.Strong(nil, elem.Text("Apps")),
elem.Text(" > "),
elem.Strong(nil, elem.Text("Tailscale")),
),
elem.Li(
nil,
elem.Text("Enter "),
Code(elem.Text(url)),
elem.Text(" under "),
elem.Strong(nil, elem.Text("ALTERNATE COORDINATION SERVER URL")),
),
elem.Li(nil,
elem.Text("Return to the tvOS "),
elem.Strong(nil, elem.Text("Home")),
elem.Text(" screen"),
),
elem.Li(nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("Tailscale")),
),
elem.Li(nil,
elem.Text("Select "),
elem.Strong(nil, elem.Text("Install VPN configuration")),
),
elem.Li(nil,
elem.Text("Select "),
elem.Strong(nil, elem.Text("Allow")),
),
elem.Li(nil,
elem.Text("Scan the QR code and follow the login procedure"),
),
elem.Li(nil,
elem.Text("Headscale should now be working on your tvOS device"),
),
),
orDivider(),
P(
elem.Text(
"Use your terminal to configure the default setting for Tailscale by issuing one of the following commands:",
),
),
P(elem.Text("For app store client:")),
codeBlockText("defaults write io.tailscale.ipn.macos ControlURL "+url),
P(elem.Text("For standalone client:")),
codeBlockText("defaults write io.tailscale.ipn.macsys ControlURL "+url),
P(
elem.Text("Restart "),
elem.Strong(nil, elem.Text("Tailscale.app")),
elem.Text(" and log in."),
),
warningBox("Caution", "You should always download and inspect the profile before installing it."),
P(elem.Text("For app store client:")),
codeBlockText("curl "+url+"/apple/macos-app-store"),
P(elem.Text("For standalone client:")),
codeBlockText("curl "+url+"/apple/macos-standalone"),
H1(elem.Text("tvOS configuration")),
H2(elem.Text("GUI")),
Ol(
elem.Li(
nil,
elem.Text("Install the official Tailscale tvOS client from the "),
externalLink("https://apps.apple.com/app/tailscale/id1470499037", "App Store"),
),
elem.Li(
nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("Settings")),
elem.Text(" (the Apple tvOS settings) > "),
elem.Strong(nil, elem.Text("Apps")),
elem.Text(" > "),
elem.Strong(nil, elem.Text("Tailscale")),
),
elem.Li(
nil,
elem.Text("Enter "),
Code(elem.Text(url)),
elem.Text(" under "),
elem.Strong(nil, elem.Text("ALTERNATE COORDINATION SERVER URL")),
),
elem.Li(
nil,
elem.Text("Return to the tvOS "),
elem.Strong(nil, elem.Text("Home")),
elem.Text(" screen"),
),
elem.Li(
nil,
elem.Text("Open "),
elem.Strong(nil, elem.Text("Tailscale")),
),
elem.Li(
nil,
elem.Text("Select "),
elem.Strong(nil, elem.Text("Install VPN configuration")),
),
elem.Li(
nil,
elem.Text("Select "),
elem.Strong(nil, elem.Text("Allow")),
),
elem.Li(
nil,
elem.Text("Scan the QR code and follow the login procedure"),
),
elem.Li(
nil,
elem.Text("Headscale should now be working on your tvOS device"),
),
pageFooter(),
),
)
}
+3 -7
View File
@@ -30,12 +30,8 @@ func AuthError(result AuthErrorResult) *elem.Element {
elem.Text(result.Message),
)
return HtmlStructure(
elem.Title(nil, elem.Text(result.Title)),
mdTypesetBody(
headscaleLogo(),
box,
pageFooter(),
),
return page(
result.Title,
box,
)
}
+13 -15
View File
@@ -41,22 +41,20 @@ func AuthSuccess(result AuthSuccessResult) *elem.Element {
elem.Text(". "+result.Message),
)
return HtmlStructure(
elem.Title(nil, elem.Text(result.Title)),
mdTypesetBody(
headscaleLogo(),
box,
H2(elem.Text("Getting started")),
P(elem.Text("Check out the documentation to learn more about headscale and Tailscale:")),
Ul(
elem.Li(nil,
externalLink("https://headscale.net/stable/", "Headscale documentation"),
),
elem.Li(nil,
externalLink("https://tailscale.com/docs", "Tailscale docs"),
),
return page(
result.Title,
box,
H2(elem.Text("Getting started")),
P(elem.Text("Check out the documentation to learn more about headscale and Tailscale:")),
Ul(
elem.Li(
nil,
externalLink("https://headscale.net/stable/", "Headscale documentation"),
),
elem.Li(
nil,
externalLink("https://tailscale.com/docs", "Tailscale docs"),
),
pageFooter(),
),
)
}
+5 -9
View File
@@ -8,14 +8,10 @@ import (
// to complete an authentication or registration flow.
// It is used by both the registration and auth-approve web handlers.
func AuthWeb(title, description, command string) *elem.Element {
return HtmlStructure(
elem.Title(nil, elem.Text(title+" - Headscale")),
mdTypesetBody(
headscaleLogo(),
H1(elem.Text(title)),
P(elem.Text(description)),
Pre(PreCode(command)),
pageFooter(),
),
return page(
title+" - Headscale",
H1(elem.Text(title)),
P(elem.Text(description)),
codeBlockText(command),
)
}
+32 -40
View File
@@ -78,12 +78,17 @@ func orDivider() *elem.Element {
)
}
// successBox creates a green success feedback box with a checkmark icon.
// The heading is displayed as bold green text, and children are rendered below it.
// Pairs with warningBox for consistent feedback styling.
// feedbackBox creates a coloured feedback box with an icon and a bold heading.
// colorVar provides both the border and heading colour, bgVar the background;
// role and ariaLive set the accessibility attributes. Children render below the
// heading.
//
//nolint:unused // Used in auth_success.go template.
func successBox(heading string, children ...elem.Node) *elem.Element {
//nolint:unused // Wrapped by successBox and errorBox.
func feedbackBox(
icon elem.Node,
colorVar, bgVar, role, ariaLive, heading string,
children ...elem.Node,
) *elem.Element {
return elem.Div(
attrs.Props{
attrs.Style: styles.Props{
@@ -91,22 +96,22 @@ func successBox(heading string, children ...elem.Node) *elem.Element {
styles.AlignItems: cssCenter,
styles.Gap: spaceM,
styles.Padding: spaceL,
styles.BackgroundColor: "var(--hs-success-bg)",
styles.Border: "1px solid var(--hs-success)",
styles.BackgroundColor: bgVar,
styles.Border: "1px solid " + colorVar,
styles.BorderRadius: spaceS,
styles.MarginBottom: spaceXL,
}.ToInline(),
attrs.Role: "status",
"aria-live": "polite",
attrs.Role: role,
"aria-live": ariaLive,
},
checkboxIcon(),
icon,
elem.Div(
nil,
append([]elem.Node{
elem.Strong(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "block",
styles.Color: "var(--hs-success)",
styles.Color: colorVar,
styles.FontSize: fontSizeH3,
styles.FontWeight: "700",
styles.MarginBottom: spaceXS,
@@ -117,6 +122,19 @@ func successBox(heading string, children ...elem.Node) *elem.Element {
)
}
// successBox creates a green success feedback box with a checkmark icon.
// The heading is displayed as bold green text, and children are rendered below it.
// Pairs with warningBox for consistent feedback styling.
//
//nolint:unused // Used in auth_success.go template.
func successBox(heading string, children ...elem.Node) *elem.Element {
return feedbackBox(
checkboxIcon(),
"var(--hs-success)", "var(--hs-success-bg)", "status", "polite",
heading, children...,
)
}
// checkboxIcon returns the success checkbox SVG icon as raw HTML.
func checkboxIcon() elem.Node {
return elem.Raw(`<svg id="checkbox" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 512 512" style="flex-shrink:0">
@@ -130,36 +148,10 @@ func checkboxIcon() elem.Node {
//
//nolint:unused // Used in auth_error.go template.
func errorBox(heading string, children ...elem.Node) *elem.Element {
return elem.Div(
attrs.Props{
attrs.Style: styles.Props{
styles.Display: "flex",
styles.AlignItems: cssCenter,
styles.Gap: spaceM,
styles.Padding: spaceL,
styles.BackgroundColor: "var(--hs-error-bg)",
styles.Border: "1px solid var(--hs-error)",
styles.BorderRadius: spaceS,
styles.MarginBottom: spaceXL,
}.ToInline(),
attrs.Role: "alert",
"aria-live": "assertive",
},
return feedbackBox(
errorIcon(),
elem.Div(
nil,
append([]elem.Node{
elem.Strong(attrs.Props{
attrs.Style: styles.Props{
styles.Display: "block",
styles.Color: "var(--hs-error)",
styles.FontSize: fontSizeH3,
styles.FontWeight: "700",
styles.MarginBottom: spaceXS,
}.ToInline(),
}, elem.Text(heading)),
}, children...)...,
),
"var(--hs-error)", "var(--hs-error-bg)", "alert", "assertive",
heading, children...,
)
}
+49 -32
View File
@@ -11,17 +11,18 @@ import (
// that matches the official Headscale documentation design.
// Uses CSS classes with styles defined in [assets.CSS].
func mdTypesetBody(children ...elem.Node) *elem.Element {
return elem.Body(attrs.Props{
attrs.Style: styles.Props{
styles.MinHeight: "100vh",
styles.Display: "flex",
styles.FlexDirection: "column",
styles.AlignItems: "center",
styles.BackgroundColor: "var(--hs-bg)",
styles.Padding: "3rem 1.5rem",
}.ToInline(),
"translate": "no",
},
return elem.Body(
attrs.Props{
attrs.Style: styles.Props{
styles.MinHeight: "100vh",
styles.Display: "flex",
styles.FlexDirection: "column",
styles.AlignItems: "center",
styles.BackgroundColor: "var(--hs-bg)",
styles.Padding: "3rem 1.5rem",
}.ToInline(),
"translate": "no",
},
elem.Main(attrs.Props{
attrs.Class: "md-typeset",
attrs.Style: styles.Props{
@@ -76,14 +77,10 @@ func Code(children ...elem.Node) *elem.Element {
return elem.Code(nil, children...)
}
// Pre creates a preformatted text block styled by .md-typeset pre
func Pre(children ...elem.Node) *elem.Element {
return elem.Pre(nil, children...)
}
// PreCode creates a code block inside Pre styled by .md-typeset pre > code
func PreCode(code string) *elem.Element {
return elem.Code(nil, elem.Text(code))
// codeBlockText creates a preformatted code block styled by
// .md-typeset pre > code.
func codeBlockText(code string) *elem.Element {
return elem.Pre(nil, elem.Code(nil, elem.Text(code)))
}
// headscaleLogo returns the Headscale SVG logo for consistent branding across all pages.
@@ -95,15 +92,16 @@ func headscaleLogo() elem.Node {
// pageFooter creates a consistent footer for all pages.
func pageFooter() *elem.Element {
return elem.Footer(attrs.Props{
attrs.Style: styles.Props{
styles.MarginTop: space3XL,
styles.TextAlign: "center",
styles.FontSize: fontSizeSmall,
styles.Color: "var(--md-default-fg-color--light)",
styles.LineHeight: lineHeightBase,
}.ToInline(),
},
return elem.Footer(
attrs.Props{
attrs.Style: styles.Props{
styles.MarginTop: space3XL,
styles.TextAlign: "center",
styles.FontSize: fontSizeSmall,
styles.Color: "var(--md-default-fg-color--light)",
styles.LineHeight: lineHeightBase,
}.ToInline(),
},
elem.Text("Powered by "),
elem.A(attrs.Props{
attrs.Href: "https://github.com/juanfont/headscale",
@@ -113,14 +111,31 @@ func pageFooter() *elem.Element {
)
}
// page renders a standard Headscale page: the given title in the document
// head, and a body that begins with the Headscale logo, contains the supplied
// content nodes in order, and ends with the shared footer.
func page(title string, content ...elem.Node) *elem.Element {
body := make([]elem.Node, 0, len(content)+2)
body = append(body, headscaleLogo())
body = append(body, content...)
body = append(body, pageFooter())
return HtmlStructure(
elem.Title(nil, elem.Text(title)),
mdTypesetBody(body...),
)
}
// HtmlStructure creates a complete HTML document structure with proper meta tags
// and semantic HTML5 structure. The head and body elements are passed as parameters
// to allow for customization of each page.
// Styling is provided via a CSS stylesheet (Material for MkDocs design system) with
// minimal inline styles for layout and positioning.
func HtmlStructure(head, body *elem.Element) *elem.Element {
return elem.Html(attrs.Props{attrs.Lang: "en"},
elem.Head(nil,
return elem.Html(
attrs.Props{attrs.Lang: "en"},
elem.Head(
nil,
elem.Meta(attrs.Props{
attrs.Charset: "UTF-8",
}),
@@ -157,8 +172,10 @@ func HtmlStructure(head, body *elem.Element) *elem.Element {
// BlankPage creates a minimal blank HTML page with favicon.
// Used for endpoints that need to return a valid HTML page with no content.
func BlankPage() *elem.Element {
return elem.Html(attrs.Props{attrs.Lang: "en"},
elem.Head(nil,
return elem.Html(
attrs.Props{attrs.Lang: "en"},
elem.Head(
nil,
elem.Meta(attrs.Props{
attrs.Charset: "UTF-8",
}),
+21 -24
View File
@@ -39,7 +39,6 @@ type ConnectedNode struct {
// and a list of connected nodes ([ConnectedNode]) as quick-ping links.
func PingPage(query string, result *PingResult, nodes []ConnectedNode) *elem.Element {
children := []elem.Node{
headscaleLogo(),
H1(elem.Text("Ping Node")),
P(elem.Text("Check if a connected node responds to a PingRequest.")),
pingExplanation(),
@@ -54,16 +53,12 @@ func PingPage(query string, result *PingResult, nodes []ConnectedNode) *elem.Ele
children = append(children, connectedNodeList(nodes))
}
children = append(children, pageFooter())
return HtmlStructure(
elem.Title(nil, elem.Text("Ping Node - Headscale")),
mdTypesetBody(children...),
)
return page("Ping Node - Headscale", children...)
}
func pingExplanation() *elem.Element {
return detailsBox("How does this work?",
return detailsBox(
"How does this work?",
Ol(
elem.Li(nil, elem.Text(
"The server sends a PingRequest to the target node via its MapResponse stream.",
@@ -88,17 +83,18 @@ func pingExplanation() *elem.Element {
}
func pingForm(query string) *elem.Element {
return elem.Form(attrs.Props{
attrs.Method: "POST",
attrs.Action: "/debug/ping",
attrs.Style: styles.Props{
styles.Display: "flex",
styles.Gap: spaceS,
styles.AlignItems: cssCenter,
styles.FlexWrap: "wrap",
styles.MarginTop: spaceM,
}.ToInline(),
},
return elem.Form(
attrs.Props{
attrs.Method: "POST",
attrs.Action: "/debug/ping",
attrs.Style: styles.Props{
styles.Display: "flex",
styles.Gap: spaceS,
styles.AlignItems: cssCenter,
styles.FlexWrap: "wrap",
styles.MarginTop: spaceM,
}.ToInline(),
},
elem.Input(attrs.Props{
attrs.Type: "text",
attrs.Name: "node",
@@ -131,11 +127,12 @@ func connectedNodeList(nodes []ConnectedNode) *elem.Element {
items = append(items, elem.Li(nil, A(href, elem.Text(label))))
}
return elem.Div(attrs.Props{
attrs.Style: styles.Props{
styles.MarginTop: space2XL,
}.ToInline(),
},
return elem.Div(
attrs.Props{
attrs.Style: styles.Props{
styles.MarginTop: space2XL,
}.ToInline(),
},
H2(elem.Text("Connected Nodes")),
Ul(items...),
)
+30 -39
View File
@@ -1,6 +1,8 @@
package templates
import (
"cmp"
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/chasefleming/elem-go/styles"
@@ -51,11 +53,11 @@ type RegisterConfirmInfo struct {
// IdP allows silent SSO.
func RegisterConfirm(info RegisterConfirmInfo) *elem.Element {
deviceList := deviceTable(
[4][2]string{
{"Hostname", info.Hostname},
{"OS", displayOrUnknown(info.OS)},
{"Machine key", info.MachineKey},
{"Registered to", info.User},
[]deviceRow{
{"Hostname", elem.Text(info.Hostname)},
{"OS", elem.Text(cmp.Or(info.OS, "(unknown)"))},
{"Machine key", Code(elem.Text(info.MachineKey))},
{"Registered to", elem.Text(info.User)},
},
)
@@ -75,35 +77,32 @@ func RegisterConfirm(info RegisterConfirmInfo) *elem.Element {
),
)
return HtmlStructure(
elem.Title(nil, elem.Text("Headscale - Confirm node registration")),
mdTypesetBody(
headscaleLogo(),
H2(elem.Text("Confirm node registration")),
P(elem.Text(
"A device is asking to be added to your tailnet. "+
"Please review the details below and confirm that this device is yours.",
)),
deviceList,
form,
P(elem.Text(
"If you do not recognise this device, close this window. "+
"The registration request will expire automatically.",
)),
pageFooter(),
),
return page(
"Headscale - Confirm node registration",
H2(elem.Text("Confirm node registration")),
P(elem.Text(
"A device is asking to be added to your tailnet. "+
"Please review the details below and confirm that this device is yours.",
)),
deviceList,
form,
P(elem.Text(
"If you do not recognise this device, close this window. "+
"The registration request will expire automatically.",
)),
)
}
func deviceTable(rows [4][2]string) *elem.Element {
type deviceRow struct {
label string
value elem.Node
}
func deviceTable(rows []deviceRow) *elem.Element {
tableRows := make([]elem.Node, 0, len(rows))
for _, row := range rows {
val := elem.Node(elem.Text(row[1]))
if row[0] == "Machine key" {
val = Code(elem.Text(row[1]))
}
tableRows = append(tableRows, elem.Tr(nil,
tableRows = append(tableRows, elem.Tr(
nil,
elem.Td(attrs.Props{
attrs.Style: styles.Props{
styles.Padding: "0.5rem 1rem 0.5rem 0",
@@ -112,13 +111,13 @@ func deviceTable(rows [4][2]string) *elem.Element {
styles.Color: "var(--md-default-fg-color--light)",
styles.BorderBottom: cssBorderHS,
}.ToInline(),
}, elem.Text(row[0])),
}, elem.Text(row.label)),
elem.Td(attrs.Props{
attrs.Style: styles.Props{
styles.Padding: "0.5rem 0",
styles.BorderBottom: cssBorderHS,
}.ToInline(),
}, val),
}, row.value),
))
}
@@ -131,11 +130,3 @@ func deviceTable(rows [4][2]string) *elem.Element {
}.ToInline(),
}, tableRows...)
}
func displayOrUnknown(s string) string {
if s == "" {
return "(unknown)"
}
return s
}
+10 -16
View File
@@ -5,23 +5,17 @@ import (
)
func Windows(url string) *elem.Element {
return HtmlStructure(
elem.Title(nil,
elem.Text("headscale - Windows"),
return page(
"headscale - Windows",
H1(elem.Text("Windows configuration")),
P(
elem.Text("Download "),
externalLink("https://tailscale.com/download/windows", "Tailscale for Windows"),
elem.Text(" and install it."),
),
mdTypesetBody(
headscaleLogo(),
H1(elem.Text("Windows configuration")),
P(
elem.Text("Download "),
externalLink("https://tailscale.com/download/windows", "Tailscale for Windows"),
elem.Text(" and install it."),
),
P(
elem.Text("Open a Command Prompt or PowerShell and use Tailscale's login command to connect with headscale:"),
),
Pre(PreCode("tailscale login --login-server "+url)),
pageFooter(),
P(
elem.Text("Open a Command Prompt or PowerShell and use Tailscale's login command to connect with headscale:"),
),
codeBlockText("tailscale login --login-server "+url),
)
}