Dependency graph workspace
https://fve.meiermade.com/components/page-examples/dependency-graph?state=empty&item=order-load Workspace
Customer analytics
Nothing to show yet
Change the current selection or return to the populated workspace.
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 DependencyNode =
{ key: string
name: string
operation: string
status: string
x: int
y: int
run: string }
let dependencyNodes =
[ { key = "customers"
name = "source.customers"
operation = "Fetch customer records"
status = "Succeeded"
x = 1
y = 2
run = "run-2401" }
{ key = "orders"
name = "source.orders"
operation = "Fetch recent orders"
status = "Succeeded"
x = 1
y = 15
run = "run-2402" }
{ key = "customer-load"
name = "warehouse.customers"
operation = "Load customer dimension"
status = "Succeeded"
x = 20
y = 2
run = "run-2407" }
{ key = "order-load"
name = "warehouse.orders"
operation = "Load order facts"
status = "Failed"
x = 20
y = 15
run = "run-2408" }
{ key = "revenue"
name = "reporting.revenue"
operation = "Build revenue summary"
status = "Blocked"
x = 39
y = 2
run = "run-2410" }
{ key = "retention"
name = "reporting.retention"
operation = "Calculate returning customers"
status = "Succeeded"
x = 39
y = 15
run = "run-2411" } ]
let nodeStatus status =
Status.create status
|> Status.withTone (
match status with
| "Succeeded" -> Tone.Positive
| "Failed" -> Tone.Critical
| _ -> Tone.Warning
)
|> Status.render
let selectedNode query =
dependencyNodes
|> List.tryFind (fun node -> node.key = query.item || node.run = query.item)
|> Option.defaultValue dependencyNodes[2]
let graphContent query =
let selected = selectedNode query
let nodeUrl node =
queryUrl DependencyGraph { query with item = node.key }
let matches (node: DependencyNode) =
"('"
+ node.name
+ " "
+ node.status
+ "').toLowerCase().includes($_graphQuery.toLowerCase())"
div {
_dataSignals "{_graphQuery:'', _graphZoom:1}"
_class "flex h-full min-h-0 flex-col"
div {
_class "flex flex-wrap items-end gap-3 border-y border-[var(--fve-border)] px-4 py-3 sm:px-6 lg:px-8"
div {
_class "min-w-0 flex-1"
Input.create "graph-search" "Search dependencies"
|> Input.withType InputType.Search
|> Input.withAttributes [ _dataBind "_graph-query" ]
|> Input.render
}
Button.create "Zoom out"
|> Button.withAttributes [ _dataOn ("click", "$_graphZoom = Math.max(0.75, $_graphZoom - 0.25)") ]
|> Button.render
Button.create "Zoom in"
|> Button.withAttributes [ _dataOn ("click", "$_graphZoom = Math.min(1.5, $_graphZoom + 0.25)") ]
|> Button.render
Button.create "Reset view"
|> Button.withAttributes [ _dataOn ("click", "$_graphZoom = 1; $_graphQuery = ''") ]
|> Button.render
output {
_ariaLabel "Graph zoom"
_class "text-sm tabular-nums"
_dataText "Math.round($_graphZoom * 100) + '%'"
"100%"
}
}
div {
_class "min-h-0 flex-1 overflow-auto"
div {
_role "region"
_ariaLabel "Dependency canvas"
_tabindex 0
_class "overflow-auto border-b border-[var(--fve-border)] bg-[var(--fve-surface-subtle)]"
div {
_style "width:54rem;height:25rem"
_dataAttr (
"style",
"'width:' + (54 * $_graphZoom) + 'rem;height:' + (25 * $_graphZoom) + 'rem'"
)
div {
_class "relative"
_style "width:54rem;height:25rem;transform-origin:top left"
_dataAttr (
"style",
"'width:54rem;height:25rem;transform-origin:top left;transform:scale(' + $_graphZoom + ')'"
)
svg {
_viewBox "0 0 864 400"
_ariaHidden "true"
_class "absolute inset-0 h-full w-full"
defs {
raw
"""<marker id="dependency-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0 0L8 4L0 8Z" fill="var(--fve-muted-text)"/></marker>"""
}
for d in
[ "M240 88H318"
"M240 296H318"
"M544 88H622"
"M544 296H578V100H622"
"M544 88H566V296H622" ] do
path {
_d d
_fill "none"
_stroke "var(--fve-muted-text)"
_strokeWidth 1.5
_attr ("marker-end", "url(#dependency-arrow)")
}
}
for node in dependencyNodes do
a {
_href (nodeUrl node)
_ariaLabel (node.name + ", " + node.status)
if node.key = selected.key then
_ariaCurrent "true"
_class
"absolute grid justify-items-start gap-2 data-[match=false]:opacity-30 rounded-xl border border-[var(--fve-border)] bg-[var(--fve-surface)] p-4 shadow-sm outline-none hover:ring-2 hover:ring-[var(--fve-brand-ring)] focus-visible:ring-2 focus-visible:ring-[var(--fve-brand-ring)] aria-current:ring-2 aria-current:ring-[var(--fve-brand-ring)]"
_style $"left:{node.x}rem;top:{node.y}rem;width:14rem"
_dataAttr ("data-match", matches node)
strong {
_class "break-all font-mono text-sm"
node.name
}
span {
_class "text-xs text-[var(--fve-muted-text)]"
node.operation
}
nodeStatus node.status
}
}
}
}
div {
_class "grid gap-6 p-4 lg:grid-cols-2 sm:p-6 lg:p-8"
section
"Selected dependency"
(div {
_class "grid justify-items-start gap-4"
h2 {
_class "break-all font-mono text-base font-semibold"
selected.name
}
details
[ "Operation", selected.operation
"Latest execution", selected.run
"Environment", "Production" ]
nodeStatus selected.status
link
(queryUrl
ExecutionDetail
{ defaultQuery with
item = selected.run })
"Inspect execution"
})
section
"Dependencies"
(ul {
_class "divide-y divide-[var(--fve-border)]"
for node in dependencyNodes do
li {
_dataShow (matches node)
_class "flex flex-wrap items-center justify-between gap-2 py-3 text-sm"
link (nodeUrl node) node.name
nodeStatus node.status
}
li {
_dataShow
"!['source.customers succeeded','source.orders succeeded','warehouse.customers succeeded','warehouse.orders failed','reporting.revenue blocked','reporting.retention succeeded'].some(value => value.includes($_graphQuery.toLowerCase()))"
_style "display:none"
_role "status"
_class "py-3 text-sm"
"No dependencies match your search."
}
})
}
}
}
let dependencyGraph query =
workspace
DependencyGraph
"Dependencies"
"Production · Customer analytics · 6 dependencies"
true
[ ApplicationAction.link
(queryUrl
ExecutionDetail
{ defaultQuery with
item = (selectedNode query).run })
"Inspect execution" ]
(stateContent DependencyGraph query (graphContent query))
div { _class "h-[44rem]"; dependencyGraph defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/dependency-graph?state=ready"
|> Fixture.browser "page-workspace" "Workspace" "/components/page-examples/dependency-graph?state=ready"
|> Fixture.render