Custom Diagram Creator

Architecture

How a diagram definition becomes live status, and how diagrams are stored.

Overview

Architecture overview The app runs in the browser. Its status engine queries Grail with DQL and the SLO API; its lookup store reads diagrams with DQL and writes them to the Resource Store. An AI agent with dtctl can register diagrams in the same lookup. Custom Diagram Creator React · Strato · React Flow (in the browser) Diagram list open · duplicate · upload · download · delete Editor canvas · floating toolbar · docked panels Status engine per-cycle cache · 4 queries in flight abort on new cycle · auto-refresh Lookup store rows of base64 JSON · whole-table upload optimistic concurrency on updatedAt Grail Query API DQL: entities · problems · KPIs load "/lookups/…/diagrams" SLO API start / poll evaluations Grail Resource Store lookup:upload (overwrite) /lookups/custom-diagram-creator/diagrams AI agent + dtctl skill/SKILL.md · same lookup format DQL SLO save read
The app runs entirely in the browser and talks to platform APIs with the user's permissions.
LayerFilesResponsibility
Modelui/app/model/zod schema (single source of truth), component type catalog (componentTypes.ts), runtime status types, factories and the sample diagram
Servicesui/app/services/DQL execution and validators, problem query builder, status engine, SLO client, lookup store, icons, time expressions
Hooksui/app/hooks/Live status cycle, auto-refresh, undo/redo
Canvasui/app/canvas/React Flow wrapper, nodes, edges, floating toolbars, status colors
Panels and pagesui/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.

ValidatorUsed forRule
assertColumns(["id"])Entity componentmust return id; a missing name is a warning
assertColumns(["id", "name"])Container (entities)both columns are mandatory
assertSingleValue()KPI connectionfirst row; chosen column or first numeric; arrays use the last value
assertTable()KPI blockat 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:

dql
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 matches dt.davis.problems by that active interval — a problem keeps one record with its latest state, whose timestamp is its last update — and the explicit filter states it. event.status is the state today, shown as still active or closed.
  • dedup keeps 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 through toString(…). id_classic is 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, $entityNames and $endpointNames are 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.
ts
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 load until 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:json pattern 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 updatedAt it loaded; a different value on save opens the Overwrite / Reload dialog.
  • Bootstrap and samples: UNKNOWN_TABULAR_FILE on first load creates the table with the sample diagrams (ui/app/samples/). A hidden metadata row (id 00000000-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.
dpl
JSON{STRING:id, STRING:name, STRING:description, STRING:owner, STRING:createdAt, STRING:updatedAt, BOOLEAN:deleted, STRING:payload}:row

Editing model

  • Chrome: React Flow NodeToolbar and EdgeToolbar for the floating toolbars and two NodeResizeControl grips per node.
  • Docked panels: editors and details sit next to the canvas; editors use Strato Tabs and Accordion.
  • 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

TopicDecision
EntitiesSmartscape 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)
IconsOnly real exports of @dynatrace/strato-icons, e.g. ApplicationsIcon and HostsIcon
Problem linksview-problem intent of dynatrace.davis.problems with event.id and event.kind
JSON Schemazod 4's built-in z.toJSONSchema