Documentation
¶
Overview ¶
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
templ: version: v0.3.1020
Package web serves the engine's dashboard.
SEAM 3 of 4, and the one that is easiest to get wrong. The other three seams are Go interfaces, which compose naturally. A rendered HTML page does not: you cannot wrap a template in middleware. If the layout hard-codes its own sidebar and header, an application built on top of the engine has exactly two options — fork the templates, or rewrite the dashboard — and forking breaks the rule that the engine must stay runnable on its own.
The fix is to make the chrome data rather than markup. Layout renders slots; a SlotProvider fills them. The engine ships a provider with single-owner defaults. A wrapping application supplies its own, adding an organisation switcher, a balance indicator, or extra navigation, without editing a single file in this package.
templ: version: v0.3.1020
templ: version: v0.3.1020
Index ¶
- Constants
- func Account(d AccountData) templ.Component
- func Activity(d ActivityData) templ.Component
- func AddNode(d AddNodeData) templ.Component
- func AddNodeNodes(d AddNodeData) templ.Component
- func AppDeploymentsBody(d AppDetailData) templ.Component
- func AppDetail(d AppDetailData) templ.Component
- func AppList(apps []app.App) templ.Component
- func AppLogLines(d LogsData) templ.Component
- func AppLogs(d LogsData) templ.Component
- func BranchOptions(d BranchData) templ.Component
- func Canvas(d CanvasData) templ.Component
- func CheckMail() templ.Component
- func Cluster(d ClusterData) templ.Component
- func CustomDomainList(d NetworkingData) templ.Component
- func DeployLogPanel(d DeployLogsData) templ.Component
- func DirectoryOptions(d DirectoryData) templ.Component
- func Events(d EventsData) templ.Component
- func FormatCPU(millis int64) string
- func FormatMemory(mib int64) string
- func Layout(s Slots, page templ.Component) templ.Component
- func NewApp(d NewAppData) templ.Component
- func NodeDetail(d NodeDetailData) templ.Component
- func NodeDetailBody(d NodeDetailData) templ.Component
- func Overview(d OverviewData) templ.Component
- func PlatformDNS(d PlatformDNSData) templ.Component
- func ProjectList(d ProjectListData) templ.Component
- func RegistryPage(d RegistryData) templ.Component
- func Settings(d SettingsData) templ.Component
- func SignIn(d SignInData) templ.Component
- func TeamPage(d TeamPageData) templ.Component
- func TeamSwitcher(teams []TeamChoice) templ.Component
- func TemplateList(d TemplateListData) templ.Component
- func UserMenu(owner identity.Owner, canSignOut bool) templ.Component
- func Volumes(d VolumesData) templ.Component
- type AccountData
- type Accounts
- type ActivityData
- type AddNodeData
- type AppDetailData
- type Apps
- type BranchData
- type CanvasData
- type CanvasEdge
- type CanvasNode
- type ClusterData
- type Crumb
- type DNSRecord
- type DefaultSlots
- type DeployLogsData
- type DirectoryData
- type EventsData
- type ExtraRoutes
- type Flash
- type FlashKind
- type Hooks
- type Joiner
- type Logger
- type LogsData
- type NavGroup
- type NavItem
- type Nets
- type NetworkingData
- type NewAppData
- type NewAppForm
- type NodeDetailData
- type Options
- type OverviewData
- type Page
- type PlatformDNSData
- type ProjectListData
- type Registries
- type RegistryData
- type ResourceSlider
- type Server
- func (s *Server) FlashErr(w http.ResponseWriter, r *http.Request, text string)
- func (s *Server) FlashOK(w http.ResponseWriter, r *http.Request, text string)
- func (s *Server) FlashWarn(w http.ResponseWriter, r *http.Request, text string)
- func (s *Server) Handler() http.Handler
- func (s *Server) Render(w http.ResponseWriter, r *http.Request, page templ.Component)
- func (s *Server) RenderStatus(w http.ResponseWriter, r *http.Request, status int, page templ.Component)
- type SettingsData
- type SignInData
- type SignInMethod
- type SlotProvider
- type SlotProviderFunc
- type Slots
- type Stacks
- type Step
- type StepState
- type Surfaces
- type TeamChoice
- type TeamPageData
- type TemplateListData
- type VolumesData
Constants ¶
const DefaultBrandName = "Yacht"
DefaultBrandName is this engine's own name.
Compared against rather than only assigned: the wordmark is this one word drawn, so it is shown when the name is still this one and replaced by plain text the moment a wrapping application sets its own.
const FlashCookie = "yacht_flash"
FlashCookie is where a pending message waits. Not HttpOnly-exempt and not readable by script for any good reason, so it is HttpOnly like the session.
const PageSize = 50
PageSize is how many rows a list shows at once.
Chosen to fill a screen and stop, rather than to be generous. The failure this exists to prevent is a page that renders every row a cluster can produce: the response gets large, the browser lays out thousands of nodes, and the one row somebody wanted is no easier to find than it was.
const SessionCookie = "yacht_session"
SessionCookie is the name of the cookie carrying a session token.
It matches account.DefaultCookieName because the session provider resolves requests by reading a cookie of that name: a mismatch would write a cookie nothing reads.
Variables ¶
This section is empty.
Functions ¶
func Account ¶
func Account(d AccountData) templ.Component
Account is the page a person manages themselves from.
Separate from /settings, which is the install's facts — the owner, the cluster, the mail transport, the version — and is about the machine rather than about anybody. One prefix meaning two scopes is how a reader stops trusting either.
func Activity ¶
func Activity(d ActivityData) templ.Component
Activity is the cross-app deployment feed: what changed recently, anywhere. The per-app history answers "what happened to this"; this answers "what happened", which is the question you have when something broke and you do not yet know where.
func AddNode ¶
func AddNode(d AddNodeData) templ.Component
AddNode hands over the command that joins a machine to this cluster.
The command carries the join token in the clear, because that is what the person running it needs. Everything else on the page — including the stored settings summary — is built from cluster.Settings, which does not carry it, so the command is the only place it can appear.
func AddNodeNodes ¶
func AddNodeNodes(d AddNodeData) templ.Component
AddNodeNodes is the confirmation half, polled while somebody runs the command on the machine.
func AppDeploymentsBody ¶
func AppDeploymentsBody(d AppDetailData) templ.Component
func AppDetail ¶
func AppDetail(d AppDetailData) templ.Component
AppDetail is the Railway-shaped view: a rail of every app on the left so you can move between them without going back to a list, and a tabbed pane on the right for the one you are looking at.
func BranchOptions ¶
func BranchOptions(d BranchData) templ.Component
BranchOptions is the list the picker swaps in.
Buttons rather than a datalist: a datalist cannot be styled, behaves differently in every browser, and gives no way to say why it is empty — which is the case that matters most here, because a private repository and a typo look identical from outside.
func Canvas ¶
func Canvas(d CanvasData) templ.Component
Canvas draws a project's apps and what connects them.
Server-rendered: the layout is computed in Go and the edges are an SVG path list. There is no client-side graph library because there is no Node in this project, and a canvas that needs a build step to render is one a self-hoster cannot patch. Dragging is progressive — every card is a link first, and the script only adds the ability to move it.
func CheckMail ¶
CheckMail takes no data on purpose.
It carries neither the address nor anything derived from it, so the response to a registered address and to an unregistered one is the same bytes. A page that said "we sent a link to alice@example.com" would only need to say it once, for an address that exists, to become the disclosure this whole flow avoids.
func Cluster ¶
func Cluster(d ClusterData) templ.Component
func CustomDomainList ¶
func CustomDomainList(d NetworkingData) templ.Component
CustomDomainList is the polled half: every claim and how far it has got.
It renders the element carrying its own polling trigger, and the fragment endpoint returns this same component with hx-swap="outerHTML". That is what makes the poll self-stopping: once every domain has settled the replacement markup carries no trigger, and htmx has nothing left to fire. Swapping only the inside would leave the trigger on a wrapper it never replaces, asking forever.
Polled separately from the tab so a request every three seconds does not re-render the add-domain form under somebody typing a hostname into it.
func DeployLogPanel ¶
func DeployLogPanel(d DeployLogsData) templ.Component
DeployLogPanel is one deployment's logs, shown in a sheet over the list.
func DirectoryOptions ¶
func DirectoryOptions(d DirectoryData) templ.Component
DirectoryOptions is the listing the picker swaps in.
Each entry is the full path rather than the bare name, because the field holds a path and clicking something that put "cmd" in it while standing in "server" would be a control that lies about what it does.
func Events ¶
func Events(d EventsData) templ.Component
Events is where Kubernetes explains itself. "Pending" is not actionable; "Insufficient cpu" or "ErrImagePull" is.
func FormatCPU ¶
FormatCPU turns millicores back into what the cluster is given.
Whole cores where it divides cleanly, because "2" is what somebody meant and "2000m" is the same thing said in a way that invites a second look.
func FormatMemory ¶
FormatMemory turns mebibytes back into what the cluster is given.
func Layout ¶
Layout renders the chrome around a page.
Note what this component does NOT do: it never reads an owner, a session, or anything about who is asking. Everything variable arrives as Slots. That is what lets a wrapping application change the entire look of the shell without touching this file.
func NewApp ¶
func NewApp(d NewAppData) templ.Component
func NodeDetail ¶
func NodeDetail(d NodeDetailData) templ.Component
NodeDetail is one machine, what is on it, and how to take it out of service.
func NodeDetailBody ¶
func NodeDetailBody(d NodeDetailData) templ.Component
NodeDetailBody is the polled half: how far the drain has got, and what is still running.
func Overview ¶
func Overview(d OverviewData) templ.Component
func PlatformDNS ¶
func PlatformDNS(d PlatformDNSData) templ.Component
PlatformDNS is the install-wide DNS configuration, and every domain that depends on it.
func ProjectList ¶
func ProjectList(d ProjectListData) templ.Component
ProjectList is every canvas a team has.
Shaped like the cluster page, because it answers the same kind of question: how much is here, and what are the pieces. The counts come first so the size of the install is readable without counting rows.
func RegistryPage ¶
func RegistryPage(d RegistryData) templ.Component
RegistryPage is where images this install builds are pushed and pulled from.
func Settings ¶
func Settings(d SettingsData) templ.Component
func SignIn ¶
func SignIn(d SignInData) templ.Component
SignIn offers both ways in, to everyone, always.
Both forms are rendered unconditionally and with no knowledge of any address. Anything that decided what to show after learning whether an address has a password would be an enumeration oracle, and a worse one than "does this address exist" because it sorts real accounts by how they are configured. It does not matter whether such a probe is a round trip, a fetch or a status code: the only safe design is one where the server is never asked.
func TeamPage ¶
func TeamPage(d TeamPageData) templ.Component
TeamPage lists who is in the team and who has been invited.
Note what is not here: an invitation token, or anything it could be derived from. account.Invitation has no such field, so a template cannot print one even by accident — an invitation link on this page would let any reader join as the invitee.
func TeamSwitcher ¶
func TeamSwitcher(teams []TeamChoice) templ.Component
TeamSwitcher moves the session between the teams a person belongs to.
Every entry is a submit button in one POST form. A link would be a GET that changes what the whole dashboard is scoped by, and prefetchers and crawlers follow links — being moved into another team by a page you merely hovered over is a confusing way to end up acting somewhere you did not choose.
The list is only the chrome's copy of what the session already proved. Membership is checked again on the way in, so an entry forged into this form switches into nothing.
details/summary rather than a scripted menu: the dashboard's one piece of state that must work is the control that says which team you are in, and it should not need JavaScript to open.
The bordered box is not decoration. Sitting flush in a column of navigation links, this read as one more link — and the one control on the page that changes what every query is scoped by should not look like a way to go to a page. The border is what says it opens something.
Shares a name with the user menu so the browser closes one when the other opens. Two panels hanging over the sidebar at once is the kind of thing a scripted menu needs a listener for; native exclusive details does it for free.
func TemplateList ¶
func TemplateList(d TemplateListData) templ.Component
TemplateList offers the stacks that can be deployed in one action.
func UserMenu ¶
UserMenu is the sidebar's floor: who the session belongs to, and the actions that belong to the session rather than to any page.
A details/summary for the same reason the switcher is one — the control that signs you out is the last thing that should need JavaScript to open — and it opens upward because it sits on the floor of the panel.
canSignOut is false on a token-authenticated install. There is no session to end there and /sign-out is not routed at all, so the buttons would 404; drawing nothing is better than drawing a control that cannot work. It is the same question the switcher answers by having no teams to offer.
func Volumes ¶
func Volumes(d VolumesData) templ.Component
Types ¶
type AccountData ¶
type AccountData struct {
Email string
Name string
// Team and Role are the ones the session is currently acting as, so that
// somebody who belongs to several knows which of them this page was reached
// through before they sign out of everything.
Team string
Role string
// Since is when this session began, which is the only thing the sessions
// section has to say about it that a person would recognise.
Since string
// HasPassword decides whether the password section offers to set a first one
// or to replace one. It is the viewer's own fact about their own account,
// read from their own session — there is no route here that answers it about
// anybody else.
HasPassword bool
// RecentAuth reports that this session proved who holds it recently enough
// to change how the account is signed in to.
//
// It is what makes a forgotten password recoverable without a second kind of
// mailed token: a session started from a link in the person's own mail
// carries exactly the authority a reset link would have carried, and it
// carries it for ten minutes rather than for the life of the cookie.
RecentAuth bool
// MinLength mirrors the service's own floor onto the input, so the browser
// says it before a round trip does. The rule is still enforced there.
MinLength int
Error string
Notice string
}
AccountData is what a person can see and change about themselves, as opposed to about the install or about their team.
Everything on it is read from the viewer's own resolved session. There is no field here that could be filled in about anybody else, and no route on this page takes an identifier from the request — which is what keeps "does this address have a password" a question the server never answers.
type Accounts ¶
type Accounts interface {
// IssueMagicLink mints a sign-in link, reporting whether the address
// belongs to anyone. Nothing is mailed when it does not, and nothing about
// the difference reaches the visitor.
IssueMagicLink(
ctx context.Context, email string, ttl time.Duration,
) (raw string, user account.User, existed bool, err error)
// EnsureUser creates a person, or returns the one already there. Used only
// to bootstrap the configured owner of a fresh install.
EnsureUser(ctx context.Context, email, displayName string) (account.User, error)
// ConsumeMagicLink spends a link and returns the person it proves. Unknown,
// expired and already-spent links all come back as account.ErrTokenInvalid.
ConsumeMagicLink(ctx context.Context, raw string) (account.User, error)
// AuthenticatePassword verifies an address and a password. Every failure —
// unknown address, no password set, wrong password — is the single
// account.ErrCredentialInvalid, so no caller can tell them apart or report
// a difference that is not there to report.
AuthenticatePassword(ctx context.Context, email, password string) (account.User, error)
// HasPassword reports whether the viewer's own account has one, which is
// what decides between offering to add and offering to change. By user id,
// never by address: the same question asked about an address is the
// disclosure the sign-in page is built to withhold.
HasPassword(ctx context.Context, userID uuid.UUID) (bool, error)
// SetPassword adds or replaces one. It takes a session id rather than a
// user id so that whose password it is comes from the session row, and it
// checks recent authentication itself rather than trusting this package to
// have done it.
SetPassword(
ctx context.Context, sessionID uuid.UUID, password string, proof account.Proof,
) error
// RemovePassword takes one off, leaving the emailed link. Allowed because
// the link never stopped working, so this cannot lock anybody out — and
// refusing would mean a password added once can never be taken back.
RemovePassword(ctx context.Context, sessionID uuid.UUID, proof account.Proof) error
// TeamsFor lists the teams a person may act as.
TeamsFor(ctx context.Context, userID uuid.UUID) ([]account.Membership, error)
// User returns the person a session resolved to, so the account page can
// name them. By id and never by address: a lookup by address reachable from
// a handler would answer whether somebody has an account.
User(ctx context.Context, userID uuid.UUID) (account.User, error)
// BootstrapOwner gives someone with no team one to act as, handing the
// first of them the team the install was already running as.
BootstrapOwner(ctx context.Context, teamID, teamName string, user account.User) error
// CreateSession opens a session onto a team, returning the token for the
// cookie. Membership is verified there, not trusted from here.
CreateSession(
ctx context.Context, userID uuid.UUID, teamID, userAgent, ip string, ttl time.Duration,
) (string, error)
// ResolveSession returns the session a token stands for. It is how sign-out
// everywhere learns whose sessions to end: from the cookie, never from
// anything the request says about itself.
ResolveSession(ctx context.Context, raw string) (account.Session, error)
// SwitchTeam points a session at another team. Membership is verified there,
// against the session's own user rather than against anything the request
// supplied, which is what makes a crafted team id switch into nothing.
SwitchTeam(ctx context.Context, sessionID uuid.UUID, teamID string) error
// RevokeSession ends one session — sign-out on this browser.
RevokeSession(ctx context.Context, raw string) error
// RevokeAllSessions ends every session a person holds, which is the only
// answer to a cookie that has been copied off a machine.
RevokeAllSessions(ctx context.Context, userID uuid.UUID) error
// InvitationFor reads an invitation without spending it, so a visitor who
// is not signed in can be sent a link to the address it names instead of
// being let in on the strength of holding the token.
InvitationFor(ctx context.Context, raw string) (account.Invitation, error)
// AcceptInvitation spends one. It checks the address itself, so a
// signed-in stranger holding the token is refused there rather than here.
AcceptInvitation(ctx context.Context, raw string, userID uuid.UUID) (string, account.Role, error)
// ListMembers and ListPendingInvitations are what the team page shows.
// Both are scoped by team, and the team comes from the caller's session
// rather than from the request.
ListMembers(ctx context.Context, teamID string) ([]account.Member, error)
ListPendingInvitations(ctx context.Context, teamID string) ([]account.Invitation, error)
// Invite issues an invitation and returns the token to mail. The token is
// returned rather than stored anywhere readable, so the message is the only
// place it exists.
Invite(
ctx context.Context, actor uuid.UUID, teamID, email string,
role account.Role, ttl time.Duration,
) (string, error)
// RevokeInvitation withdraws one. The mail cannot be recalled, so deleting
// the row is the only way to take an invitation back.
RevokeInvitation(ctx context.Context, actor uuid.UUID, teamID string, id uuid.UUID) error
// SetRole and RemoveMember change who may do what. Both check the actor's
// own authority and refuse to leave the team without an owner.
SetRole(ctx context.Context, actor uuid.UUID, teamID string, target uuid.UUID, role account.Role) error
RemoveMember(ctx context.Context, actor uuid.UUID, teamID string, target uuid.UUID) error
}
Accounts is the sign-in surface's view of the account service.
An interface rather than the concrete service, for the same reason Apps is one: the sign-in page has to be testable without a database, and this package has no business knowing how a person is stored.
type ActivityData ¶
type AddNodeData ¶
type AddNodeData struct {
// Settings describes what is stored. It never carries the token — see
// cluster.Settings.
Settings cluster.Settings
Configured bool
// Command is the line to run. Empty until settings exist, because a
// command with holes in it is worse than a prompt to fill them in.
Command string
Pool string
// Nodes and ClusterOK are the confirmation half.
Nodes []orchestrator.NodeInfo
ClusterOK bool
Error string
}
AddNodeData is the page that hands over a join command.
func (AddNodeData) IsNew ¶
func (d AddNodeData) IsNew(n orchestrator.NodeInfo) bool
IsNew reports whether a node joined recently enough to be worth pointing at.
type AppDetailData ¶
type AppDetailData struct {
App app.App
Siblings []app.App
Tab string
Pods []orchestrator.PodInfo
Deployments []app.Deployment
BaseURL string
// Net is the app's routing. Loaded alongside the rest rather than on its
// own page: a panel nothing links to is a panel nobody finds, which is
// exactly what happened the first time.
Net app.Networking
// ResolverName is which resolver answered the last domain check, so the
// page can attribute what it reports rather than stating it as fact.
ResolverName string
// The resource tracks, with ceilings taken from the cluster's largest
// node — what a single pod can actually be scheduled onto.
CPULimit ResourceSlider
MemoryLimit ResourceSlider
CPURequest ResourceSlider
MemoryRequest ResourceSlider
// RepoError is why the connected repository could not be read, shown on
// the source card. A private repo, a typo and an outage all land here.
RepoError string
Notice string
// Error is a refusal from the action that produced this render, shown with
// the form that caused it rather than carried through a redirect.
Error string
// Deploy on push. HooksOn is whether the section is offered at all: a Git
// app on an install with the webhook surface mounted.
HooksOn bool
Hook app.Hook
HookURL string
// HookURLPublic says HookURL is on the configured public URL rather than
// guessed from this request, which may be a tunnel nothing else can reach.
HookURLPublic bool
// HookSecret is set only on the response that issued it. It is never read
// back, so this is the one render that can show it.
HookSecret string
}
type Apps ¶
type Apps interface {
List(ctx context.Context, ownerID string) ([]app.App, error)
Count(ctx context.Context, ownerID string) (int64, error)
Get(ctx context.Context, ownerID, name string) (app.App, error)
Create(ctx context.Context, ownerID string, in app.CreateInput) (app.App, error)
Scale(ctx context.Context, ownerID, name string, replicas int32) (app.App, error)
// Update changes what the app runs and how much it may use. Everything
// here was decided at create and then frozen until this existed.
Update(ctx context.Context, ownerID, name string, in app.UpdateInput) (app.App, error)
// Branches lists a repository's branches, filtered by a query. Read with
// ls-remote, so it costs one HTTPS request and no clone.
Branches(ctx context.Context, repoURL, query string) ([]string, error)
// Directories lists what is inside a repository at a path. GitHub only —
// git cannot read a tree without fetching objects, so this asks the host.
Directories(ctx context.Context, repoURL, path string) ([]string, error)
Redeploy(ctx context.Context, ownerID, name string) error
// Rollback puts an app back on one of its earlier releases, settings and
// workload both.
Rollback(ctx context.Context, ownerID, name string, releaseID uuid.UUID) error
// CancelLiveDeployment stops whatever deploy an app is holding, and reports
// app.ErrNotFound when it is holding none.
CancelLiveDeployment(ctx context.Context, ownerID, name string) error
Delete(ctx context.Context, ownerID, name string) error
Deployments(ctx context.Context, ownerID string, appID uuid.UUID, limit int32) ([]app.Deployment, error)
// DeployActivity is deploys per day for the overview chart, with every day
// in the window present whether or not it had any.
DeployActivity(ctx context.Context, ownerID string, days int) (app.DeployActivity, error)
// Storage. Attaching holds the app to one replica and makes its deploys
// recreate rather than roll; the service refuses what it cannot honour.
AttachVolume(ctx context.Context, ownerID, appName string, in app.VolumeInput) (app.Volume, error)
ResizeVolume(ctx context.Context, ownerID, appName, volumeName string, sizeBytes int64) error
DeleteVolume(ctx context.Context, ownerID, appName, volumeName string, detach bool) error
// Environment. A secret's value never comes back out — SetVariable is how
// it is replaced, and there is no read path for it by design.
SetVariable(ctx context.Context, ownerID, appName string, in app.VariableInput) error
DeleteVariable(ctx context.Context, ownerID, appName, key string) error
// SetHealth points the readiness probe at a path, and optionally lets the
// same path restart the container.
SetHealth(ctx context.Context, ownerID, name, healthPath string, liveness bool) error
// Links are the dependencies between apps, recorded when a variable naming
// another app is written — the only moment a sealed value is readable.
Links(ctx context.Context, ownerID string) ([]app.Link, error)
RecentActivity(ctx context.Context, ownerID string, limit int32) ([]app.Activity, error)
// Projects are the canvases apps are drawn on. Project and Projects both
// create the default one when a team has none, so no caller has to decide
// whether a team has been set up yet.
Projects(ctx context.Context, ownerID string) ([]app.Project, error)
Project(ctx context.Context, ownerID, slug string) (app.Project, error)
ProjectByID(ctx context.Context, ownerID string, id uuid.UUID) (app.Project, error)
CreateProject(ctx context.Context, ownerID, name string) (app.Project, error)
ListInProject(ctx context.Context, ownerID string, projectID uuid.UUID) ([]app.App, error)
// SetPosition and ClearPositions are the canvas arrangement, which belongs
// to the team rather than to whoever last dragged something.
SetPosition(ctx context.Context, ownerID, name string, x, y int32) error
ClearPositions(ctx context.Context, ownerID string, projectID uuid.UUID) error
}
Apps is the workload lifecycle this server drives.
An interface rather than the concrete service so the dashboard can be tested without a database. It is defined here, at the consumer, which keeps the app package free of knowledge about who calls it.
type BranchData ¶
type BranchData struct {
Branches []string
// Error is why the remote could not be read, already reduced to something
// short enough to sit under a field. A private repository, a typo and an
// outage all land here and none of them is worth a red field.
Error string
// Searched distinguishes "nothing matched what you typed" from "we have
// not looked yet", which render as different things and would otherwise
// both be an empty list.
Searched bool
}
BranchData is what the branch picker swaps in.
type CanvasData ¶
type CanvasData struct {
Nodes []CanvasNode
Edges []CanvasEdge
Width int
Height int
// Empty distinguishes "no apps" from "apps that do not refer to each
// other" — the second is a normal install, the first needs a prompt.
Empty bool
// Project is the canvas being drawn, and Projects is every one the team
// has, so the switcher does not need a second request.
Project app.Project
Projects []app.Project
// Open is the app whose panel is showing, nil when none is. It is the same
// data the app page used to render, because the panel IS the app page now.
Open *AppDetailData
}
CanvasData is the whole graph.
type CanvasEdge ¶
type CanvasEdge struct {
// Path is an SVG path: out of the bottom of the dependent, up into the
// bottom of the dependency. Orthogonal rather than curved, because a
// straight line through three other cards is harder to follow than a
// corner.
Path string
Via string
// From and To name the cards the edge joins, so the script can find the
// paths that need re-routing when one of them moves.
From string
To string
}
CanvasEdge is one app's dependency on another.
type CanvasNode ¶
type CanvasNode struct {
App app.App
X, Y int
Height int
// Pinned means somebody dragged this card, so its position is stored
// rather than computed.
Pinned bool
// Volume is the storage drawn attached beneath the card. Only the first is
// shown: a second would need the card to grow, and no app in this engine
// has one yet.
Volume *app.Volume
}
CanvasNode is one app as the graph draws it.
type ClusterData ¶
type ClusterData struct {
Tab string
OK bool
Error string
Summary orchestrator.ClusterSummary
Nodes []orchestrator.NodeInfo
Pods []orchestrator.PodInfo
// CanAddNode and CanManageNodes are separate because the abilities are.
// Joining needs a key to seal a token; draining needs a credential that can
// evict across namespaces, and an install can have either without the
// other. Each gates only what it actually governs, so a link never leads
// somewhere its viewer would be refused.
CanAddNode bool
CanManageNodes bool
}
type Crumb ¶
Crumb is one segment of the breadcrumb in the top bar. An empty Href renders as the current, non-navigable segment.
type DNSRecord ¶
DNSRecord is a record somebody has to create at their provider.
Three fields because that is what every provider's form asks for. It was two paragraphs of prose before, which is the wrong shape for a value that gets retyped into another system.
type DefaultSlots ¶
type DefaultSlots struct{}
DefaultSlots is the engine's own chrome: the navigation a single-owner self-hosted install needs, and nothing else.
type DeployLogsData ¶
type DeployLogsData struct {
App string
Deploy app.DeployLogs
// Build is what produced this deployment's image, when anything did.
Build app.Build
HasBuild bool
// HTTP is what the ingress controller recorded for this app.
HTTP app.HTTPLogs
// HTTPBuckets is the whole timeline, computed before paging.
HTTPBuckets []app.HTTPBucket
// Find is the search inside a log's own text, and Method narrows the
// request list to one verb. Separate from the page's query only in name:
// they travel the same way and are read the same way.
Find string
Method string
// Methods is what this app's requests actually used, so the selector
// offers nothing that could only return an empty list.
Methods []string
// HTTPPage is the window of requests being shown. The chart above them is
// deliberately not paged: it summarises the traffic, and a summary of one
// page of it would answer a question nobody asked.
HTTPPage Page
// View is which of the three logs is showing.
View string
Previous bool
Error string
// DeployID is this deployment, so the build pane can address itself for
// the poll that follows a running build.
DeployID string
// Pane asks for one pane rather than the whole sheet.
Pane bool
}
DeployLogsData is the sheet opened from one deployment.
type DirectoryData ¶
type DirectoryData struct {
// Path is where the listing is standing, so an entry can be offered as the
// full path a build would use rather than a bare name.
Path string
// Parent is one level up, or empty at the root.
Parent string
Directories []string
// Error is why the tree could not be read, already short enough to sit
// under a field. A non-GitHub host lands here too — that is a limit worth
// stating rather than a field that quietly stops helping.
Error string
Searched bool
}
DirectoryData is what the directory picker swaps in.
type EventsData ¶
type ExtraRoutes ¶ added in v0.2.0
type ExtraRoutes struct {
// Member is behind identity and the member gate: reading, and every
// change a redeploy undoes.
Member func(r chi.Router)
// Admin is behind the admin gate: what a redeploy does not undo.
Admin func(r chi.Router)
// Owner is behind the owner gate: who can administer, and what it costs.
Owner func(r chi.Router)
}
ExtraRoutes are what a wrapping application mounts inside the engine's role gates. Each is called with the group's router once, while the handler is being built, and may register any routes it likes there.
type Flash ¶
Flash is a message waiting to be shown once.
type Hooks ¶ added in v0.2.0
type Hooks interface {
GitHook(ctx context.Context, ownerID, name string) (app.Hook, error)
EnableGitHook(ctx context.Context, ownerID, name string) (string, error)
DisableGitHook(ctx context.Context, ownerID, name string) error
DeliverPush(ctx context.Context, appID uuid.UUID, p app.Push) (string, error)
}
Hooks is deploy on push: a webhook per app that a Git host calls.
type Joiner ¶
type Joiner interface {
Settings(ctx context.Context) (cluster.Settings, error)
// DNS is how the install publishes hostnames. It sits here rather than in
// its own seam because both are one thing: install-wide cluster settings an
// owner sets once, stored together and gated together.
DNS(ctx context.Context) (cluster.DNS, error)
SetDNS(ctx context.Context, target, prefix string) error
SetJoin(ctx context.Context, serverURL, token string, by uuid.UUID) error
Command(ctx context.Context, pool string) (string, error)
}
Joiner is the dashboard's view of how a machine joins this cluster.
An interface rather than the concrete type, for the reason Apps is one: the page has to be testable without a database, and this package has no business knowing how a join token is stored.
type Logger ¶
type Logger interface {
Logs(ctx context.Context, ownerID, appName string, req app.LogRequest) (app.Logs, error)
// LogStream follows an app's container output until ctx is done. The
// iterator holds a connection open while it is read, so the handler must
// stop reading when the client goes away.
LogStream(
ctx context.Context, ownerID, appName string, req app.LogRequest,
) (iter.Seq2[orchestrator.LogLine, error], error)
DeploymentLogs(
ctx context.Context, ownerID, appName string, deployID uuid.UUID, req app.LogRequest,
) (app.DeployLogs, error)
Deployment(
ctx context.Context, ownerID, appName string, deployID uuid.UUID,
) (app.Deployment, error)
// BuildForDeployment is the build behind one deployment, when there was
// one. ErrNotFound means the app was deployed from an image nobody built.
BuildForDeployment(
ctx context.Context, ownerID string, deployID uuid.UUID,
) (app.Build, error)
// DeploymentHTTPLogs is what the ingress controller recorded for this
// app's hostnames.
DeploymentHTTPLogs(ctx context.Context, ownerID, appName string) (app.HTTPLogs, error)
}
Logger is the dashboard's view of container output.
type NavGroup ¶
type NavGroup struct {
}
NavGroup is a labelled block of navigation. An empty Heading renders the items without a header, which is how the primary group is expressed.
type NavItem ¶
type NavItem struct {
// Icon names one of the inline glyphs in layout.templ. Unknown names
// render nothing rather than a broken image.
}
NavItem is one entry in the sidebar.
type Nets ¶
type Nets interface {
Networking(ctx context.Context, ownerID, name string) (app.Networking, error)
SetNetworking(ctx context.Context, ownerID, name string, httpsOnly, cnameOnly bool) error
AddDomain(ctx context.Context, ownerID, name, host string) error
VerifyDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error
RemoveDomain(ctx context.Context, ownerID, name string, id uuid.UUID) error
// ResolverName says which resolver answers about custom domains, so the
// page can attribute what it reports rather than stating it as fact.
ResolverName() string
// AllDomains is every custom domain on the install, unsettled first.
AllDomains(ctx context.Context, ownerID string) ([]app.InstallDomain, error)
// TargetStatus reports whether the configured CNAME target resolves,
// as a sentence. Empty when there was nothing to check.
TargetStatus(ctx context.Context, target string) string
}
Nets is the dashboard's view of an app's routing.
type NetworkingData ¶
type NetworkingData struct {
App string
Net app.Networking
// UntrustedCert means this app is served over https with a certificate
// nobody will trust, because the install has no wildcard certificate.
UntrustedCert bool
// ResolverName is which resolver answered the last check.
//
// Shown because "this name does not resolve" is one resolver's opinion, and
// somebody comparing it against their own dig output needs to know whether
// the two asked the same server.
ResolverName string
// Settled reports that no domain here is still moving, which is what stops
// the polling fragment asking again.
Settled bool
Notice string
Error string
}
NetworkingData is the app's routing panel.
type NewAppData ¶
type NewAppData struct {
Error string
Form NewAppForm
// Source is what the person picked. Empty shows the picker instead of a
// form: there is no sensible default, and choosing for them is how you end
// up with a database deployed as a bare image.
Source app.Source
// Blueprint is the chosen source's settings, so the form can show what is
// already decided rather than asking again.
Blueprint app.Blueprint
Sources []app.Blueprint
// CanConfigureRegistry decides whether an unavailable Git source offers a
// way out or only names who has one. The registry page is owner-only, so
// linking a member to it would replace a dead end with a locked door.
CanConfigureRegistry bool
}
type NewAppForm ¶
type NewAppForm struct {
Name string
Image string
Port string
Replicas string
Env string
// The repository, for a source that is built rather than pulled.
RepoURL string
RepoBranch string
RepoSubdir string
}
NewAppForm preserves what the user typed so a validation error does not discard their input.
type NodeDetailData ¶
type NodeDetailData struct {
Node orchestrator.NodeInfo
Found bool
// Pods are what remains on the node. Drained is true when nothing is left
// that a drain would move, which is the condition for offering removal.
Pods []orchestrator.PodInfo
Drained bool
// CanManage is false when the orchestrator cannot take a node out of
// service, so the page explains rather than offering buttons that fail.
CanManage bool
Error string
Notice string
}
NodeDetailData is one machine and what is still running on it.
type Options ¶
type Options struct {
Orchestrator orchestrator.Orchestrator
Identity identity.Provider
Apps Apps
// Slots is optional; DefaultSlots is used when nil.
Slots SlotProvider
// Extra is routes a wrapping application mounts inside the engine's own
// role gates, so its pages get the same session, identity and CSRF
// handling as the engine's without re-implementing any of it.
Extra ExtraRoutes
// Authenticated reports whether a credential is required, purely so the
// settings page can warn when it is not.
Authenticated bool
Version string
// AppDomain and WildcardTLS describe the platform's hostname policy. The
// dashboard reads them to explain the install on the settings page; the
// per-app scheme comes from app.App.TLS, which the app service populates.
AppDomain string
WildcardTLS bool
// Joiner, when set, puts the add-node surface on the router. Left nil the
// engine offers no way to add a node, which is right for an install with
// no secret key: it could store nothing and hand out nothing.
Joiner Joiner
// Stacks, when set, puts the templates surface on the router.
Stacks Stacks
// Nets, when set, puts the per-app Networking surface on the router.
Nets Nets
// Hooks is deploy on push. Nil leaves the webhook endpoint unmounted and
// the settings section off.
Hooks Hooks
// Logs, when set, puts the log surface on the router.
Logs Logger
// Registries, when set, puts the image registry surface on the router.
// Nil leaves it off: an install with nowhere to push cannot build, and a
// form for a setting nothing reads is a worse answer than no form.
Registries Registries
// Accounts, when set, puts the sign-in surface on the router. Left nil the
// engine serves no sign-in page at all, which is right for an install
// resolved by a shared token: a form that could never issue a session is a
// worse answer than no form.
Accounts Accounts
// Mailer delivers sign-in links. Defaults to the logging transport, which
// is the break-glass path when mail breaks after accounts are switched on.
Mailer notify.Mailer
// BaseURL is the public URL links are built from. Required with Accounts,
// and deliberately not derived from the request: a link built from the Host
// header is one an attacker can point at their own server and have Yacht
// mail to a real person.
BaseURL string
// MagicLinkTTL is how long a sign-in link stays redeemable. Defaults to
// defaultMagicLinkTTL.
MagicLinkTTL time.Duration
// SessionTTL is how long a signed-in browser stays signed in. Defaults to
// defaultSessionTTL.
SessionTTL time.Duration
// MailTransport names the delivery route — "smtp", "resend" or "log" — so
// the settings page can show whether links are actually being sent.
MailTransport string
// BootstrapEmail is the address allowed to create itself on first sign-in.
//
// A link is only ever issued to an address that already has an account,
// which is what stops the sign-in form filling the user table. On a fresh
// install that leaves nobody able to get in at all: no user, so no link, so
// no user. This names the one address exempt from that, and only while it
// has no account — once it does, it is an ordinary person like anyone else.
BootstrapEmail string
// BootstrapTeamID and BootstrapTeamName are the install's configured owner
// — YACHT_OWNER_ID and YACHT_OWNER_NAME. The first person to sign in
// inherits that team, so the apps an install already deployed under it stay
// reachable once accounts are switched on. Required with Accounts.
BootstrapTeamID string
BootstrapTeamName string
Logger *slog.Logger
}
Options configures the dashboard server.
All three seams arrive here as dependencies. Nothing in this package constructs an orchestrator, resolves an identity, or decides what the chrome looks like — which is what makes the whole surface reusable by an application the engine has never heard of.
type OverviewData ¶
type OverviewData struct {
OwnerName string
ClusterOK bool
ClusterError string
AppCount int64
Summary orchestrator.ClusterSummary
Apps []app.App
// Activity is deploys per day. Zero days means the chart is not shown at
// all, which is the state an install has before it can read its own
// history — not the same as a window that genuinely had no deploys.
Activity app.DeployActivity
}
type Page ¶
type Page struct {
// Query is the search somebody typed, echoed back so the box keeps it.
Query string
// Number is the 1-based page being shown, and Total is how many there are.
Number int
Total int
// Matched is how many rows the search found, before paging.
Matched int
// From and To are the 1-based bounds of this page, for "51–100 of 412".
From int
To int
// Base is the path the controls link to, without a query string.
Base string
// Extra carries any other query parameters the surface needs kept across
// paging — a log view, a pod name — so a page link does not silently
// change what is being looked at.
Extra url.Values
}
Page is one window onto a list, and what a template needs to draw the controls around it.
Computed from the whole result set rather than from a database cursor, because both surfaces using this read from Kubernetes, which returns what it returns. Slicing here is honest about that: it makes the page small, and it does not pretend the read was.
func (Page) Filtered ¶
Filtered reports whether a search narrowed the list, which is the difference between "nothing here" and "nothing matched".
func (Page) HasPrev ¶
HasPrev and HasNext gate the controls, so a page never offers a link to somewhere that does not exist.
type PlatformDNSData ¶
type PlatformDNSData struct {
DNS cluster.DNS
Error string
// Domains is every custom domain on the install, unsettled first. This is
// where the background checker's work becomes visible: one app's page shows
// one app's domains, and nothing answered "which are stuck".
Domains []app.InstallDomain
// TargetResolves reports whether the configured CNAME target itself
// resolves. Empty means nothing was checked — no target, or no resolver.
TargetResolves string
ResolverName string
}
PlatformDNSData is the install's DNS settings, and every domain that depends on them.
type ProjectListData ¶
ProjectListData is the page listing a team's projects.
type Registries ¶
type Registries interface {
Settings(ctx context.Context) (registry.Settings, error)
Set(ctx context.Context, by uuid.UUID, host, repository, username, password string, insecure bool) error
Clear(ctx context.Context) error
// CanStore reports whether a password can be sealed. Asked here rather
// than handing the web layer an encryption key it has no other use for.
CanStore() bool
}
Registries is the dashboard's view of where images go.
type RegistryData ¶
type RegistryData struct {
Registry registry.Settings
// HasKey reports whether a password can be stored at all. The page says so
// before showing a form rather than after a submission fails.
HasKey bool
Notice string
Error string
}
RegistryData is the install's registry page.
type ResourceSlider ¶
type ResourceSlider struct {
// Name is the form field, in the unit the slider works in — millicores or
// mebibytes — rather than in Kubernetes' string grammar. The handler turns
// the number back into a string, so nothing here has to parse one.
Name string
Label string
// Value and Max are in the slider's own unit.
Value int64
Max int64
Step int64
// Display and MaxDisplay are the same numbers as people say them.
Display string
MaxDisplay string
// Unset means no limit is configured, which is a different answer from a
// small one and is drawn differently.
Unset bool
}
ResourceSlider is one track: what it is set to now, and how far it can go.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves the dashboard.
func (*Server) FlashOK ¶ added in v0.2.0
FlashOK, FlashErr and FlashWarn leave a message for the next full page render, across a redirect. Same mechanism as the engine's own actions use.
func (*Server) Handler ¶
Handler returns the HTTP routes.
Authenticated routes live in a group behind identity.Middleware rather than each checking for themselves. A per-handler check is one a handler can forget, and the forgotten one is the security bug.
func (*Server) Render ¶ added in v0.2.0
Render draws a page inside the engine's layout, with the chrome the SlotProvider supplies and whatever flash the last request left. For a wrapping application's own pages, so they look like the engine's.
func (*Server) RenderStatus ¶ added in v0.2.0
func (s *Server) RenderStatus(w http.ResponseWriter, r *http.Request, status int, page templ.Component)
RenderStatus is Render with a status other than 200 — a form re-shown with its refusal, say.
type SettingsData ¶
type SettingsData struct {
OwnerID string
OwnerName string
Authenticated bool
Version string
ClusterOK bool
AppDomain string
WildcardTLS bool
// AccountsOn, MailTransport and SessionTTL describe the sign-in posture.
// Shown because otherwise the only way to find out whether accounts are on
// — and whether links are actually being delivered — is to sign out and
// see what happens.
AccountsOn bool
MailTransport string
SessionTTL string
}
type SignInData ¶
type SignInData struct {
Email string
Error string
// Method names which of the two forms was submitted, so that the address is
// put back in the one it was typed in and the error is drawn beside it.
//
// Derived only from the route that was posted to — a fact the submitter
// chose — so it says nothing whatever about the account.
Method SignInMethod
}
SignInData is the state of the sign-in form.
Error is what the visitor did wrong or what the server refused, never anything about whether the address is registered or whether it has a password — those are the two facts this page exists to keep quiet about.
There is deliberately no Password field. A submitted password rendered back into the HTML would sit in a page somebody may screenshot, print, or paste into a bug report, and no-store reaches none of those.
type SignInMethod ¶
type SignInMethod string
SignInMethod is which form a submission came from.
const ( SignInByLink SignInMethod = "link" SignInByPassword SignInMethod = "password" )
type SlotProvider ¶
SlotProvider builds the chrome for a request.
Taking the request, rather than a fixed Slots value, is what allows the chrome to vary per principal — which is precisely what a multi-tenant wrapper needs and a single-owner engine does not.
type SlotProviderFunc ¶
SlotProviderFunc adapts a function to SlotProvider.
type Slots ¶
type Slots struct {
// Title is the document title.
Title string
// Breadcrumb is the trail in the top bar. A wrapping application prepends
// its own segments — an account or environment, say — without the engine
// needing a concept for them.
Breadcrumb []Crumb
// BrandName and BrandHref label the top-left corner. A wrapping
// application overrides these to present its own product.
BrandName string
BrandHref string
// own groups, or replace the set entirely.
Nav []NavGroup
// HeaderTools renders at the right of the top bar. This is where a
// commercial layer would put a balance indicator or usage meter.
HeaderTools templ.Component
// SidebarTop renders above the navigation — the natural home for an
// organisation or project switcher.
SidebarTop templ.Component
SidebarFooter templ.Component
// FullBleed drops the centred, padded content wrapper so the page fills
// the window and manages its own scrolling.
//
// A chrome decision rather than a page one, which is why it lives here: a
// wrapping application that replaces the layout decides for itself which
// of its pages want the whole window, and the engine's own answer is not
// binding on it.
FullBleed bool
// Banner renders above the page content, full width. Intended for
// account-level notices such as a low balance or a pending suspension.
Banner templ.Component
// Bare drops the sidebar and the top bar, leaving the page the whole
// window.
//
// For the pages somebody meets before they are let in. Emptying the
// individual slots is not the same thing: the navigation goes, but the
// sidebar column and the top bar stay, so a sign-in form sits beside an
// empty rail and under an empty header — chrome for an application the
// visitor has no access to yet.
//
// A chrome decision rather than a page one, which is why it lives here
// beside FullBleed. A wrapping application that replaces the layout decides
// for itself what its signed-out pages look like.
Bare bool
// Flash is the outcome of whatever the last request did, shown once.
//
// Filled by the server immediately before rendering rather than by a
// SlotProvider: reading it also clears it, and a provider is handed the
// request without the response it would need to do that. A wrapping
// application therefore inherits toasts without implementing anything.
Flash *Flash
}
Slots is the chrome around a page.
Every field here is an extension point. Adding a field is safe; changing the meaning of one is a breaking change for anything wrapping the engine, so treat this struct as public API.
type Stacks ¶
type Stacks interface {
DeployTemplate(ctx context.Context, ownerID, slug, stack string) (app.Project, error)
}
Stacks is the dashboard's view of deploying a template.
Separate from Apps because it is a different verb: Apps creates one workload, this creates several and the project holding them. Keeping it apart means the templates page cannot reach anything else on the app service by accident.
type Step ¶
type Step struct {
Label string
// Detail is what the last check actually observed.
//
// The reason this type exists rather than a status word. "Points elsewhere"
// is not actionable; "points at ghs.googlehosted.com" is, and the difference
// is the whole of the user's complaint about waiting without feedback.
Detail string
// Commands are what to run about it, each shown on its own line with a
// copy button. Kept out of Detail because a command inside a sentence is
// retyped by hand, and a command retyped by hand is the one with the typo.
Commands []string
// At is when this stage last changed. Zero renders nothing rather than the
// epoch.
At time.Time
State StepState
}
Step is one stage of something that arrives over time.
type StepState ¶
type StepState string
StepState is how far a stage has got. The values are the CSS class names directly, declared in assets/css/input.css under @layer components — a name assembled in Go is invisible to Tailwind's scanner, so anything returned from here has to exist in the stylesheet already.
const ( // StepWait is a stage nothing has reached. Drawn, but making no claim. StepWait StepState = "step-wait" // StepActive is the stage being worked on now. StepActive StepState = "step-active" // StepDone is settled and behind us. StepDone StepState = "step-done" // StepErr is settled and wrong, which is different from still waiting. StepErr StepState = "step-err" )
type Surfaces ¶
Surfaces is which optional pages this install actually has.
The sidebar is built from a path and a context, with no access to the server's dependencies, so without this it offers every entry regardless of whether the route behind it was mounted. A link to a 404 is a worse answer than a missing link: it says the feature exists and is broken.
This is the same failure the Networking panel had, and the DNS page had it silently from the day it was written — a page with no way in.
func SurfacesFromContext ¶
SurfacesFromContext returns the optional pages available to this request.
type TeamChoice ¶
type TeamChoice struct {
ID string
Name string
Role account.Role
// Active marks the team the session is acting as — the owner every query on
// this request is scoped by.
Active bool
}
TeamChoice is one team a person may act as, as the chrome needs it.
Public because the switcher is chrome, and chrome is the seam a wrapping application replaces: its own SlotProvider reads the same list out of the request and renders it however its product does.
func TeamsFromContext ¶
func TeamsFromContext(ctx context.Context) []TeamChoice
TeamsFromContext returns the teams the request's own session may act as.
Empty on an install with no account service, and for a request with no session: in both cases there is nothing to switch between.
type TeamPageData ¶
type TeamPageData struct {
TeamID string
TeamName string
// Viewer is the role of the person looking at the page. The page offers
// only the controls their role can actually use — an admin is not shown a
// role selector they would be refused for using.
Viewer account.Role
ViewerID uuid.UUID
Members []account.Member
Invitations []account.Invitation
// Error is a message from the action that redirected here, if it failed.
Error string
}
TeamPageData is the team management page.
type TemplateListData ¶
TemplateListData is the page offering the stacks that can be deployed.
type VolumesData ¶
type VolumesData struct {
OK bool
Error string
Volumes []orchestrator.VolumeInfo
}
Source Files
¶
- account.go
- account_templ.go
- activity.go
- assets.go
- auth.go
- auth_templ.go
- brand_templ.go
- canvas.go
- canvas_templ.go
- components.go
- components_templ.go
- domains.go
- flash.go
- format.go
- hooks.go
- layout_templ.go
- logs.go
- logs_templ.go
- networking.go
- networking_templ.go
- node_join.go
- node_join_templ.go
- node_remove.go
- node_remove_templ.go
- nodes.go
- page.go
- page_templ.go
- pages_templ.go
- password.go
- project.go
- registry.go
- registry_templ.go
- resources.go
- roles.go
- security.go
- server.go
- slots.go
- storage.go
- surfaces.go
- team.go
- team_templ.go
- template.go
- template_templ.go
Directories
¶
| Path | Synopsis |
|---|---|
|
ui
|
|
|
icon
templui component icon - version: v1.12.1 installed by templui v1.12.1 📚 Documentation: https://templui.io/docs/components/icon
|
templui component icon - version: v1.12.1 installed by templui v1.12.1 📚 Documentation: https://templui.io/docs/components/icon |
|
utils
templui util templui.go - version: v1.12.1 installed by templui v1.12.1
|
templui util templui.go - version: v1.12.1 installed by templui v1.12.1 |