Dependency graph
Dependency graph workspace
https://fve.meiermade.com/components/page-examples/dependency-graph?state=ready&item=customers
Relay
Workspace
Customer analytics
Dependencies
Production · Customer analytics · 6 dependencies
Selected dependency
source.customers
- Operation
- Fetch customer records
- Latest execution
- run-2401
- Environment
- Production
Dependencies
- source.customersSucceeded
- source.ordersSucceeded
- warehouse.customersSucceeded
- warehouse.ordersFailed
- reporting.revenueBlocked
- reporting.retentionSucceeded
- No dependencies match your search.
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