Architecture
How a diagram definition becomes live status, and how diagrams are stored.
Overview
| Layer | Files | Responsibility |
|---|---|---|
| Model | ui/app/model/ | zod schema (single source of truth), component type catalog (componentTypes.ts), runtime status types, factories and the sample diagram |
| Services | ui/app/services/ | DQL execution and validators, problem query builder, status engine, SLO client, lookup store, icons, time expressions |
| Hooks | ui/app/hooks/ | Live status cycle, auto-refresh, undo/redo |
| Canvas | ui/app/canvas/ | React Flow wrapper, nodes, edges, floating toolbars, status colors |
| Panels and pages | ui/app/panels/, pages/, toolbar/ | Docked editors, details, editor toolbar, palette, list and editor pages |
Nodes and edges read their status from a React context instead of their data, so a refresh never rewrites the diagram itself.
Running DQL
services/dql.ts wraps @dynatrace-sdk/client-query: queryExecute with an ISO timeframe and the user's time zone, then queryPoll until the query succeeds or fails. Results are normalized to records, columns and Grail types — long values arrive as strings, so numeric detection uses the types.
| Validator | Used for | Rule |
|---|---|---|
assertColumns(["id"]) | Entity component | must return id; a missing name is a warning |
assertColumns(["id", "name"]) | Container (entities) | both columns are mandatory |
assertSingleValue() | KPI connection | first row; chosen column or first numeric; arrays use the last value |
assertTable() | KPI block | at least one column |
The timeframe selector keeps expressions such as now()-2h; the query API needs ISO timestamps, so they are resolved at the start of every refresh cycle and relative timeframes move forward with auto-refresh.
Problem queries
One query per component (or per container), with the timeframe and ids inlined so the details panel can show exactly what ran:
fetch dt.davis.problems, from: "<from>", to: "<to>"
| filter not(dt.davis.is_duplicate)
| dedup event.id, sort: {timestamp desc}
| filter event.start <= toTimestamp("<to>")
and coalesce(event.end, now()) >= toTimestamp("<from>")
| filter iAny(in(affected_entity_ids[], array(<ids>)))
or iAny(in(toString(smartscape.affected_entity.ids[]), array(<ids>)))
or iAny(in(toString(smartscape.affected_entities[][id]), array(<ids>)))
| filter <match> // optional
| fields event.id, event.kind, display_id, event.name, event.status, event.category,
event.start, event.end, affected_entity_ids, smartscape.affected_entity.ids, smartscape.affected_entities,
root_cause_entity_id, root_cause_entity_name
| sort event.start desc
| limit 1000- It counts the problems open at any time during the timeframe: started before its end and not closed before its start (an open problem has no
event.end). Grail already matchesdt.davis.problemsby that active interval — a problem keeps one record with its latest state, whosetimestampis its last update — and the explicit filter states it.event.statusis the state today, shown as still active or closed. dedupkeeps one row per problem.- The ids are looked up in every field environments use for affected entities: classic ids (
affected_entity_ids), Smartscape ids (smartscape.affected_entity.ids) and Smartscape entity records (smartscape.affected_entities, a list of{ id, name, type }— in some environments the only one filled). Smartscape ids only match throughtoString(…).id_classicis matched too when the entity query returns it. - Containers run a single problem query for all their ids and group the problems per child in memory.
Status engine
- Triggers: load, timeframe change, manual refresh, auto-refresh, and changes made in the editor panel (for that element only).
- Entity components: picked entities go straight into the problem query — no entity query runs. Only components without a selection run their
entityDql. - KPIs: each KPI item runs its own query (cached like the rest) and is turned into lines by its value column, name mode and row limit. In entity components the placeholders
$entityIds,$entityNamesand$endpointNamesare filled first with the picked entities — or with the rows of the entity query, which then runs only once for both the problems and the KPIs. - Cache: during a cycle, results are cached by timeframe + query, so identical queries run once.
- Concurrency: at most four queries in flight.
- Cancellation: a full refresh aborts the previous cycle with an
AbortController. - Progressive rendering: each element is painted as soon as its queries finish and keeps its previous color while refreshing.
function evalThreshold(v: number, t: Threshold): "pass" | "warning" | "failing" {
const bad = (lim: number | null) => lim !== null && (t.direction === "above" ? v > lim : v < lim);
if (bad(t.failing)) return "failing";
if (bad(t.warning)) return "warning";
return "pass";
}SLO containers start and poll SLO evaluations and map SUCCESS / WARNING / FAILURE / ERROR to pass, warning, failing and no data.
Persistence
Diagrams live in the Grail lookup /lookups/custom-diagram-creator/diagrams, written through lookupDataClient from @dynatrace-sdk/client-resource-store.
- Read with
load "…"queries; the list never loads the payload column. - Write replaces the whole table: load all rows, upsert one (or several, for uploads), upload JSONL with
overwrite: true. - Read after write: an upload shows up in DQL after a short delay, so every write polls
loaduntil it returns exactly the rows written (up to 20 s). The list, the next save and its concurrency check always read fresh data. - Typed parse pattern: with a plain
JSON:jsonpattern Grail turns ISO dates into timestamps (with nanoseconds) and would alter any name that looks like a date, so every column is typed explicitly. - Concurrency: the editor keeps the
updatedAtit loaded; a different value on save opens the Overwrite / Reload dialog. - Bootstrap and samples:
UNKNOWN_TABULAR_FILEon first load creates the table with the sample diagrams (ui/app/samples/). A hidden metadata row (id00000000-0000-4000-8000-000000000000,deleted: true) records which samples the table has received, so samples added in later versions arrive once and deleted samples don't come back. Samples a later version drops are removed while nobody has saved them (createdAt=updatedAt). - Limits: 100 MB per lookup file; the app warns above 5 MB per diagram.
JSON{STRING:id, STRING:name, STRING:description, STRING:owner, STRING:createdAt, STRING:updatedAt, BOOLEAN:deleted, STRING:payload}:rowEditing model
- Chrome: React Flow
NodeToolbarandEdgeToolbarfor the floating toolbars and twoNodeResizeControlgrips per node. - Docked panels: editors and details sit next to the canvas; editors use Strato
TabsandAccordion. - Live apply: each change is validated with zod and pushed to the canvas with a commit mode — canvas only, status refresh after 900 ms (thresholds, filters) or immediately (Run). Pending refreshes run when the panel closes or switches.
- Undo: the first change of a panel session records one snapshot; undo and redo remount the open panel with the restored data.
Decisions verified against a live environment
| Topic | Decision |
|---|---|
| Entities | Smartscape templates; fetch dt.entity.service returned UNKNOWN_DATA_OBJECT in a new environment |
| SLO SDK | @dynatrace-sdk/client-service-level-objectives (there is no client-slo package) |
| Icons | Only real exports of @dynatrace/strato-icons, e.g. ApplicationsIcon and HostsIcon |
| Problem links | view-problem intent of dynatrace.davis.problems with event.id and event.kind |
| JSON Schema | zod 4's built-in z.toJSONSchema |