Tutorial
Build an Admin App with DashWind
Use Marionette's frontend/dashwind package to build an admin screen with a sidebar, KPIs, a resource table, row actions, and a settings form.
Step 1
Prepare the project
Create an empty Go module and add Marionette.
mkdir marionette-dashwind-admin
cd marionette-dashwind-admin
go mod init example.com/marionette-dashwind-admin
go get github.com/YoshihideShirai/marionette@latest Step 2
Decide what DashWind owns
DashWind provides higher-level admin building blocks. Register DaisyUI/Tailwind and default shell CSS with dw.Use,
create the shared layout with dw.Shell, render KPIs with dw.MetricGrid, and build list pages with dw.ResourcePage.
Step 3
Create main.go
Paste this code into main.go. The single file demonstrates state, routes, the DashWind shell, and Action-based updates.
package main
import (
"fmt"
mb "github.com/YoshihideShirai/marionette/backend"
mf "github.com/YoshihideShirai/marionette/frontend"
dw "github.com/YoshihideShirai/marionette/frontend/dashwind"
)
type customer struct {
ID, Name, Plan, Status string
MRR int
}
var seedCustomers = []customer{
{"cus_1001", "Acme Robotics", "Enterprise", "Active", 12800},
{"cus_1002", "Northwind Health", "Growth", "Review", 6400},
{"cus_1003", "Sora Logistics", "Starter", "Active", 2200},
}
var navigation = dw.Navigation{{Label: "Workspace", Items: []dw.NavItem{
{Path: "/", Label: "Dashboard", Icon: "▦"},
{Path: "/customers", Label: "Customers", Icon: "☷", Badge: "3"},
{Path: "/settings", Label: "Settings", Icon: "⚙"},
}}}
func main() {
app := mb.New()
app.SetGlobal("customers", append([]customer(nil), seedCustomers...))
// DashWind registers the DaisyUI/Tailwind template and shell CSS.
dw.Use(app, dw.Options{Theme: "corporate"})
app.Page("/", func(ctx *mb.Context) mf.Node {
return shell("/", dashboard(ctx))
}, mb.WithTitle("Admin Dashboard"))
app.Page("/customers", func(ctx *mb.Context) mf.Node {
return shell("/customers", customersPage(ctx))
}, mb.WithTitle("Customers - Admin Dashboard"))
app.Page("/settings", func(ctx *mb.Context) mf.Node {
return shell("/settings", settingsPage())
}, mb.WithTitle("Settings - Admin Dashboard"))
app.Action("customers/toggle", func(ctx *mb.Context) mf.Node {
id := ctx.FormValue("id")
ctx.UpdateGlobal("customers", func(old any) any {
customers := append([]customer(nil), old.([]customer)...)
for i := range customers {
if customers[i].ID == id {
if customers[i].Status == "Active" {
customers[i].Status = "Review"
} else {
customers[i].Status = "Active"
}
}
}
return customers
})
return dw.ShellContent(dw.DefaultMainTargetID, customersPage(ctx))
})
if err := app.Run("127.0.0.1:8080"); err != nil {
panic(err)
}
}
func shell(currentPath string, body mf.Node) mf.Node {
return dw.Shell(dw.ShellProps{
CurrentPath: currentPath,
Brand: dw.Brand{Title: "Revenue Ops", Subtitle: "DashWind admin", Mark: "R", Href: "/"},
Navigation: navigation,
SearchPlaceholder: "Search customers",
User: dw.UserMenu{Name: "Admin User", Email: "admin@example.com", Initials: "AU"},
}, body)
}
func dashboard(ctx *mb.Context) mf.Node {
customers := ctx.GetGlobal("customers").([]customer)
return mf.DivProps(mf.ElementProps{Class: "space-y-6"},
dw.PageHeader(dw.PageHeaderProps{Title: "Dashboard", Description: "Monitor subscription health and revenue."}),
dw.MetricGrid(dw.MetricGridProps{Items: []dw.Metric{
{Title: "Customers", Value: fmt.Sprint(len(customers)), Description: "Total accounts", Trend: "+2 this week", TrendTone: dw.ToneSuccess},
{Title: "MRR", Value: fmt.Sprintf("$%d", totalMRR(customers)), Description: "Monthly recurring revenue", Trend: "12% growth", TrendTone: dw.ToneSuccess},
{Title: "Needs review", Value: fmt.Sprint(countByStatus(customers, "Review")), Description: "Accounts to follow up", Trend: "Prioritize today", TrendTone: dw.ToneWarning},
}}),
dw.CardPanel(dw.CardPanelProps{Title: "Next actions", Description: "Use Actions to update only the main region."}, mf.Text("Open Customers and toggle a status.")),
)
}
func customersPage(ctx *mb.Context) mf.Node {
customers := ctx.GetGlobal("customers").([]customer)
return dw.ResourcePage(dw.ResourcePageProps[customer]{
Title: "Customers",
Description: "List, inspect, and update customer status.",
Rows: customers,
Columns: []dw.Column[customer]{
{Header: "Name", Sortable: true, Cell: func(c customer) mf.Node { return mf.Text(c.Name) }},
{Header: "Plan", Cell: func(c customer) mf.Node { return mf.Text(c.Plan) }},
{Header: "MRR", Class: "text-right", Cell: func(c customer) mf.Node { return mf.Text(fmt.Sprintf("$%d", c.MRR)) }},
{Header: "Status", Cell: func(c customer) mf.Node { return mf.Badge(mf.BadgeProps{Label: c.Status, Props: mf.ComponentProps{Variant: "primary"}}) }},
},
RowActions: func(c customer) []dw.Action {
return []dw.Action{{
Label: "Toggle",
Action: "/customers/toggle",
Target: "#" + dw.DefaultMainTargetID,
Swap: "outerHTML",
Class: "btn-sm btn-outline",
Fields: map[string]string{"id": c.ID},
}}
},
})
}
func settingsPage() mf.Node {
return dw.SettingsSection(dw.SettingsSectionProps{
Title: "Workspace settings",
Description: "Start with preset fields, then replace them with your own handlers.",
Fields: []dw.Field{
{Name: "workspace", Label: "Workspace name", Value: "Revenue Ops", Required: true},
{Name: "timezone", Label: "Timezone", Value: "UTC"},
},
SubmitLabel: "Save settings",
})
}
func totalMRR(customers []customer) int {
total := 0
for _, c := range customers {
total += c.MRR
}
return total
}
func countByStatus(customers []customer, status string) int {
count := 0
for _, c := range customers {
if c.Status == status {
count++
}
}
return count
} Step 4
Run and inspect the UI
Start the local server and open the dashboard in your browser.
go run .
# open http://127.0.0.1:8080 Step 5
Read the implementation in order
- Use
dw.Useto register DashWind styles on the app - Define a shared sidebar and topbar with
navigationandshell - Place KPI cards and supporting cards in
dashboard - Assemble
ResourcePage, column definitions, and row actions incustomersPage - Update state in
app.Actionand replace onlydw.ShellContentwith anouterHTMLswap
Step 6
Where to extend it
- Replace the
SetGlobalseed data with a repository layer when you introduce a database - Add authentication by checking the session at the top of each
app.Pagehandler and returning a login page when needed - For larger lists, pass filter and search forms into
ResourcePage, then recalculate query state in Actions - Customize branding with
dw.Options{CustomCSS: "..."}or by switching DaisyUI themes
Next, continue with Run as Desktop App (Optional) to launch the same admin UI inside a desktop WebView.