mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-11 10:31:14 +09:00
templates: consolidate shared page and component helpers
This commit is contained in:
+169
-164
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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...,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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...),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user