feat(dashboardAPI): resolve Grafana folder by name (fixes stale folderUid 400s)
A pinned folderUid goes stale whenever Grafana is rebuilt — the same-named folder returns with a fresh uid and every dashboard upsert then 400s "folder not found", silently dropping all generated dashboards. Add a folderTitle config field: when set, resolveFolderUid() looks the folder up by name (GET /api/folders), creates it if absent (POST /api/folders), caches the uid for the process, and falls back to the configured folderUid on any failure (never worse than the pinned behavior). The emit handlers (registerChild/regenerateDashboard/emitDashboardsFor) are now async and await the resolution. folderUid retained as an explicit override/fallback. Locked by slice48-folder-resolve-by-name; existing emit tests made async. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ function resolveChildNode(childId, ctx) {
|
||||
|
||||
// Shared emit path used by both child.register (auto, deploy-driven) and
|
||||
// regenerate-dashboard (manual). `trigger` distinguishes the two for logs.
|
||||
function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
async function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
const dashboards = source.generateDashboardsForGraph(childSource, {
|
||||
includeChildren: Boolean(msg.includeChildren ?? true),
|
||||
});
|
||||
@@ -34,6 +34,13 @@ function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
const token = source.config?.grafanaConnector?.bearerToken;
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
// Resolve the folder by name (creating it if missing) so a rebuilt Grafana's
|
||||
// fresh folder uid never strands the upserts on a stale pinned uid. Falls
|
||||
// back to the configured folderUid on any failure.
|
||||
const folderUid = typeof source.resolveFolderUid === 'function'
|
||||
? await source.resolveFolderUid()
|
||||
: (source.config?.grafanaConnector?.folderUid || undefined);
|
||||
|
||||
for (const dash of dashboards) {
|
||||
ctx.send({
|
||||
...msg,
|
||||
@@ -43,7 +50,7 @@ function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
headers,
|
||||
payload: source.buildUpsertRequest({
|
||||
dashboard: dash.dashboard,
|
||||
folderUid: source.config?.grafanaConnector?.folderUid || undefined,
|
||||
folderUid: folderUid || undefined,
|
||||
overwrite: true,
|
||||
}),
|
||||
meta: {
|
||||
@@ -74,7 +81,7 @@ function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
||||
// payload's `diff` indicates that NEITHER the dashboardAPI itself NOR this
|
||||
// child NOR its grandchildren changed, skip composition and log no-diff. The
|
||||
// first call after startup (no cached diff yet) regenerates unconditionally.
|
||||
function registerChild(source, msg, ctx) {
|
||||
async function registerChild(source, msg, ctx) {
|
||||
const childSource = resolveChildSource(msg.payload, ctx);
|
||||
if (!childSource?.config) {
|
||||
throw new Error('Missing or invalid child node');
|
||||
@@ -99,13 +106,13 @@ function registerChild(source, msg, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
emitDashboardsFor(source, childSource, ctx, msg, 'child.register');
|
||||
await emitDashboardsFor(source, childSource, ctx, msg, 'child.register');
|
||||
}
|
||||
|
||||
// On regenerate-dashboard: re-emit dashboards for every cached child source,
|
||||
// bypassing the diff predicate. Useful as an operator escape hatch when
|
||||
// auto-regen missed an edge case or when the operator just wants to refresh.
|
||||
function regenerateDashboard(source, msg, ctx) {
|
||||
async function regenerateDashboard(source, msg, ctx) {
|
||||
const cached = source.cachedChildSources?.() || [];
|
||||
if (source.logger?.info) {
|
||||
source.logger.info({
|
||||
@@ -116,7 +123,7 @@ function regenerateDashboard(source, msg, ctx) {
|
||||
});
|
||||
}
|
||||
for (const childSource of cached) {
|
||||
emitDashboardsFor(source, childSource, ctx, msg, 'manual');
|
||||
await emitDashboardsFor(source, childSource, ctx, msg, 'manual');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user