Operations dashboard workspace
https://fve.meiermade.com/components/page-examples/operations-dashboard?state=loading Workspace
Northwind Outdoor
Loading operations dashboard
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open Acme.Components.Documentation
open type Html
open type Svg
open type Datastar
type ExamplePage =
| DependencyGraph
| ExecutionDetail
| FinancialReporting
| Messaging
| Operations
| Scheduling
| MediaManagement
type ReviewState =
| Setup
| Ready
| Loading
| Empty
| Failed
type ExampleQuery =
{ state: ReviewState
item: string
view: string
range: string }
let defaultQuery =
{ state = Ready
item = ""
view = ""
range = "" }
let slug =
function
| DependencyGraph -> "dependency-graph"
| ExecutionDetail -> "execution-detail"
| FinancialReporting -> "financial-reporting"
| Messaging -> "messaging"
| Operations -> "operations-dashboard"
| Scheduling -> "scheduling"
| MediaManagement -> "media-management"
let title =
function
| DependencyGraph -> "Dependency graph"
| ExecutionDetail -> "Execution detail"
| FinancialReporting -> "Financial reporting"
| Messaging -> "Messaging"
| Operations -> "Operations dashboard"
| Scheduling -> "Scheduling"
| MediaManagement -> "Media management"
let url page =
"/components/page-examples/" + slug page
let stateKey =
function
| Setup -> "setup"
| Ready -> "ready"
| Loading -> "loading"
| Empty -> "empty"
| Failed -> "error"
let queryUrl page query =
let pairs =
[ "state", stateKey query.state
"item", query.item
"view", query.view
"range", query.range ]
url page
+ "?"
+ (pairs
|> List.filter (snd >> String.IsNullOrEmpty >> not)
|> List.map (fun (key, value) -> key + "=" + Uri.EscapeDataString value)
|> String.concat "&")
let link (href: string) (label: string) : HtmlElement =
a {
_href href
_class
"font-medium text-[var(--fve-brand-text)] underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-[var(--fve-brand-ring)]"
label
}
let section (heading: string) (content: HtmlElement) =
Section.create (SectionHeader.create heading |> SectionHeader.withDivider) content
|> Section.render id
let details (fields: (string * string) list) =
DescriptionList.create [ for label, value in fields -> DetailField.text label value ]
|> DescriptionList.withColumns DescriptionListColumns.Three
|> DescriptionList.render
let reviewStates page =
(if page = Operations then [ Setup, "Setup" ] else [])
@ [ Ready, "Populated"; Loading, "Loading"; Empty, "Empty"; Failed, "Error" ]
let stateContent page query content =
match query.state with
| Setup
| Ready -> content
| Loading ->
div {
_class "p-8"
LoadingIndicator.create ("Loading " + (title page).ToLowerInvariant())
|> LoadingIndicator.withVisibleLabel
|> LoadingIndicator.render
}
| Empty ->
EmptyState.create "Nothing to show yet" "Change the current selection or return to the populated workspace."
|> EmptyState.withActions (link (queryUrl page { query with state = Ready }) "Return to workspace")
|> EmptyState.render
| Failed ->
Notice.create
"workspace-error"
"This view could not be loaded"
(p { "Your selection is preserved. Try loading it again." })
|> Notice.withTone Tone.Critical
|> Notice.withActions (link (queryUrl page { query with state = Ready }) "Try again")
|> Notice.render
/// Place durable workspace identity above the scrollable destination list.
let sideNavWorkspace (name: string) =
div {
p {
_class "text-xs font-semibold uppercase tracking-wide text-[var(--fve-muted-text)]"
"Workspace"
}
p {
_class "mt-1 truncate text-sm font-semibold"
name
}
}
let workspace page heading subtitle canvas actions content =
let product, workspaceName, destinations =
match page with
| DependencyGraph
| ExecutionDetail ->
"Relay", "Customer analytics", [ DependencyGraph, "Dependencies"; ExecutionDetail, "Executions" ]
| FinancialReporting -> "Ledger", "Northwind Studio", [ FinancialReporting, "Overview" ]
| Messaging -> "Gather", "Northwind Outdoor", [ Messaging, "Messages" ]
| _ ->
"Fieldwork", "Northwind Outdoor", [ Operations, "Overview"; Scheduling, "Schedule"; MediaManagement, "Photos" ]
let items =
[ for destination, label in destinations -> SideNavItem.create destination label ]
let navigation =
SideNav.create
(slug page + "-nav")
(product + " navigation")
(SideNavHeader.create product)
[ SideNavSection.ungrouped items ]
|> SideNav.withCurrent page
|> SideNav.withWidth SideNavWidth.Narrow
|> SideNav.withContext (sideNavWorkspace workspaceName)
|> SideNav.withMobileContext (sideNavWorkspace workspaceName)
|> SideNav.withFooter (
div {
_class "flex min-w-0 items-center gap-3"
Avatar.create "Andy Meier" "AM" |> Avatar.render
p {
_class "truncate text-sm font-medium"
"Andy Meier"
}
}
)
let header =
PageHeader.create heading
|> PageHeader.withSubtitle subtitle
|> PageHeader.withActions (ActionCluster.create (slug page + "-actions") actions)
let topBar =
PageTopBar.create ()
|> PageTopBar.withContent (
div {
_class "flex min-h-[var(--fve-shell-bar-min-height)] items-center px-4 sm:px-6 lg:px-8"
Breadcrumbs.create
(slug page + "-breadcrumbs")
(product + " breadcrumb")
[ BreadcrumbItem.create page product; BreadcrumbItem.create page heading ]
|> Breadcrumbs.render url
}
)
let body =
Page.create header content
|> Page.withTopBar topBar
|> Page.withWidth PageWidth.Full
|> Page.withBodyLayout (
if canvas then
PageBodyLayout.Canvas
else
PageBodyLayout.Padded
)
|> Page.render id
AppShell.create (slug page + "-shell") navigation body
|> AppShell.withTheme (
ComponentsTheme.sky
|> ComponentsTheme.withDensity Density.Compact
|> ComponentsTheme.withControlSize ControlSize.Small)
|> AppShell.withBoundary AppShellBoundary.Container
|> AppShell.asPreview (product + " workspace")
|> AppShell.render url
type ScheduledEvent =
{ id: string
name: string
date: DateOnly
time: string
start: TimeOnly
finish: TimeOnly
people: string
kind: string
photo: string }
let scheduledEvents =
[ { id = "lesson-201"
name = "Coastal trail lesson"
date = DateOnly(2026, 9, 17)
time = "10:00–11:30 AM"
start = TimeOnly(10, 0)
finish = TimeOnly(11, 30)
people = "Maya and Sam · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-303" }
{ id = "camp-017"
name = "Beginner riding camp"
date = DateOnly(2026, 9, 17)
time = "10:30 AM–12:00 PM"
start = TimeOnly(10, 30)
finish = TimeOnly(12, 0)
people = "3 riders · Instructor: Jordan"
kind = "Camp"
photo = "photo-301" }
{ id = "lesson-202"
name = "Cornering fundamentals"
date = DateOnly(2026, 9, 18)
time = "9:00–10:00 AM"
start = TimeOnly(9, 0)
finish = TimeOnly(10, 0)
people = "Riley · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-302" }
{ id = "ride-019"
name = "Open track practice"
date = DateOnly(2026, 9, 19)
time = "1:00–3:00 PM"
start = TimeOnly(13, 0)
finish = TimeOnly(15, 0)
people = "6 riders · Supervisor: Jordan"
kind = "Practice"
photo = "photo-304" }
{ id = "return-020"
name = "Equipment return"
date = DateOnly(2026, 9, 24)
time = "4:00–4:30 PM"
start = TimeOnly(16, 0)
finish = TimeOnly(16, 30)
people = "Equipment team · Andy Meier"
kind = "Equipment"
photo = "photo-304" } ]
let eventUrl event =
queryUrl Scheduling { defaultQuery with item = event.id }
let eventTable events =
Table.create
"Upcoming events"
[ Table.column "Event" (fun (event: ScheduledEvent) -> link (eventUrl event) event.name)
|> Table.asRowHeader
|> Table.asMobilePrimary
Table.column "Date" (fun event ->
text (event.date.ToString("MMM d", Globalization.CultureInfo.InvariantCulture)))
Table.column "Time" (fun event -> text event.time)
Table.column "People" (fun event -> text event.people) ]
events
|> Table.withMobileLayout TableMobileLayout.Records
|> Table.withDensity Density.Compact
|> Table.render
let operations query =
let content =
div {
_class "grid gap-8"
div {
_class "grid gap-6 border-b border-[var(--fve-border)] pb-6 sm:grid-cols-3"
for label, value, detail in
[ "Today's sessions", "2", "One lesson · One camp"
"Riders expected", "5", "September 17"
"Upcoming sessions", "4", "Through September 19" ] do
Metric.text label value |> Metric.withDescription detail |> Metric.render
}
section
"Today's schedule"
(eventTable (scheduledEvents |> List.filter (fun event -> event.date = DateOnly(2026, 9, 17))))
div {
_class "grid gap-8 lg:grid-cols-2"
section
"Next up"
(div {
_class "grid gap-4"
h2 {
_class "text-lg font-semibold"
"Coastal trail lesson"
}
p {
_class "text-sm text-[var(--fve-muted-text)]"
"10:00 AM · Maya and Sam are arriving for their first trail session."
}
link (eventUrl scheduledEvents.Head) "Review lesson details"
})
}
if query.state = Setup then
FirstSteps.create
"fieldwork-first-steps"
"Finish your workspace"
[ FirstStep.create "schedule" "Review this week's schedule"
|> FirstStep.withAction (link (url Scheduling) "Open schedule")
FirstStep.create "photos" "Prepare your session photographs"
|> FirstStep.withDescription "Review descriptions before sharing a collection."
|> FirstStep.withAction (link (url MediaManagement) "Open photos") ]
|> FirstSteps.render
}
workspace
Operations
"Good morning, Andy"
"Thursday, September 17 · Northwind Outdoor"
false
[ ApplicationAction.link (url Scheduling) "View schedule"
|> ApplicationAction.withVariant ButtonVariant.Primary ]
(stateContent Operations query content)
div { _class "h-[44rem]"; operations defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/operations-dashboard?state=ready"
|> Fixture.browser "page-workspace" "Workspace" "/components/page-examples/operations-dashboard?state=ready"
|> Fixture.render