@marble-sh/backstage-plugin-grafana-backend 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,104 @@
1
+ # @marble-sh/backstage-plugin-grafana-backend
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 476051b: Catalog discovery no longer leaves the `defaultOwner` relation dangling, and
8
+ every discovered dashboard `Resource` now shows exactly its own dashboard.
9
+
10
+ - The entity provider creates a placeholder `Group` (`spec.type: virtual`) for
11
+ `defaultOwner` while no other catalog source defines that ref; a definition
12
+ from any other source (e.g. a hand-written catalog-info.yaml) always takes
13
+ precedence. Disable with `grafana.catalog.emitOwnerGroup: false` — also the
14
+ handover switch for replacing an existing placeholder with your own
15
+ definition (see the module README).
16
+ - New `grafana/dashboard-uid` annotation (exact, case-sensitive uid match),
17
+ supported end-to-end: `getDashboardUid` in `-common`, `DashboardFilter.uid`
18
+ in `-node`, a `uid` query parameter on the backend dashboard routes,
19
+ `ListDashboardsRequest.uid` + `DashboardsCard` support in the frontend, and
20
+ emitted by catalog discovery on every dashboard `Resource`.
21
+
22
+ ### Patch Changes
23
+
24
+ - d091ba9: Reject a non-numeric Grafana Cloud `stackId` at config-read time with an
25
+ error that explains the numeric-id-vs-slug distinction and how to look the id
26
+ up, instead of silently deriving an invalid App Platform namespace that fails
27
+ every refresh with 403 "invalid namespace". Documented the numeric requirement
28
+ and both lookup methods (Cloud portal URL, `GET /api/frontend/settings`) in
29
+ the backend README and the config schema.
30
+ - Updated dependencies [476051b]
31
+ - Updated dependencies [d091ba9]
32
+ - @marble-sh/backstage-plugin-grafana-common@1.1.0
33
+ - @marble-sh/backstage-plugin-grafana-node@1.1.0
34
+
35
+ ## 1.0.2
36
+
37
+ ### Patch Changes
38
+
39
+ - d5daa0c: Fixed: `release:publish` now actually invokes `scripts/release-publish.mjs`.
40
+ The previous fix added the script but left the release script running
41
+ `changeset publish`, so 1.0.1 was published with the same raw `workspace:^`
42
+ and `backstage:^` ranges as 1.0.0 and remains uninstallable outside this
43
+ monorepo. This release is the first one packed with Yarn (materialized
44
+ dependency ranges).
45
+ - Updated dependencies [d5daa0c]
46
+ - @marble-sh/backstage-plugin-grafana-common@1.0.2
47
+ - @marble-sh/backstage-plugin-grafana-node@1.0.2
48
+
49
+ ## 1.0.1
50
+
51
+ ### Patch Changes
52
+
53
+ - 577eaca: Fixed: published manifests now carry real semver dependency ranges. Versions
54
+ 0.2.0 and 1.0.0 were published via `changeset publish` (plain `npm publish`),
55
+ which skips Yarn's pack hooks and leaked the raw `workspace:^` and
56
+ `backstage:^` protocols into the registry manifests, making the packages
57
+ uninstallable outside this monorepo. Releases now publish `yarn pack` tarballs
58
+ through the npm CLI.
59
+ - Updated dependencies [577eaca]
60
+ - @marble-sh/backstage-plugin-grafana-common@1.0.1
61
+ - @marble-sh/backstage-plugin-grafana-node@1.0.1
62
+
63
+ ## 1.0.0
64
+
65
+ ### Major Changes
66
+
67
+ - b179a8f: Bumping to version 1.0.0, General Release!
68
+
69
+ ### Patch Changes
70
+
71
+ - Updated dependencies [b179a8f]
72
+ - @marble-sh/backstage-plugin-grafana-common@1.0.0
73
+ - @marble-sh/backstage-plugin-grafana-node@1.0.0
74
+
75
+ ## 0.2.0
76
+
77
+ ### Minor Changes
78
+
79
+ - 72afd3d: Initial release of the Grafana plugin suite: a read-only, backend-centric
80
+ Grafana integration built on Backstage's new backend system, with a frontend
81
+ that supports both the legacy and the new frontend systems.
82
+
83
+ - `grafana-common` — shared entity annotations and data-transfer types.
84
+ - `grafana-node` — shared Grafana HTTP client (App Platform APIs, with folder
85
+ resolution), instance config reader + schema, and filters. Dashboard
86
+ selection supports comma-separated multi-value queries, and per-instance
87
+ flags can disable dashboards, alerts, or folder resolution.
88
+ - `grafana-backend` — read-only REST API with cache/database storage,
89
+ scheduled refresh, and `allowOnDemandRefresh` / `fetchOnDemand` flags to
90
+ make Grafana traffic fully deterministic. Discovery scoping and scaffolder
91
+ guard-rail options round out the configuration surface.
92
+ - `grafana` — entity dashboard/alert cards and content plus a standalone
93
+ instances page, for both frontend systems (new system via the `/alpha`
94
+ export).
95
+ - `catalog-backend-module-grafana` — discovers Grafana instances and dashboards
96
+ as catalog `Resource` entities with dependency relations.
97
+ - `scaffolder-backend-module-grafana` — a `grafana:dashboard:create` scaffolder
98
+ action.
99
+
100
+ ### Patch Changes
101
+
102
+ - Updated dependencies [72afd3d]
103
+ - @marble-sh/backstage-plugin-grafana-common@0.2.0
104
+ - @marble-sh/backstage-plugin-grafana-node@0.2.0
package/README.md CHANGED
@@ -95,7 +95,7 @@ grafana:
95
95
  | `token` | yes | Service-account token used as a Bearer token. Read-only (Viewer) permissions are enough. Marked secret. |
96
96
  | `title` | no | Human-readable title. Defaults to `name`. |
97
97
  | `namespace` | no | App Platform namespace. Defaults to `default` (self-hosted) or `stacks-<stackId>` (cloud). |
98
- | `stackId` | no | Grafana Cloud stack id, used to derive the namespace. |
98
+ | `stackId` | no | **Numeric** Grafana Cloud stack id (not the stack slug), used to derive the namespace (see below). |
99
99
  | `apis` | no | Override or disable the API used per data type (see below). |
100
100
  | `resolveFolders` | no | `false` skips the `/api/folders` folder lookup (see below). Defaults to `true`. |
101
101
 
@@ -166,6 +166,21 @@ Grafana's App Platform APIs are namespaced:
166
166
  - Self-hosted, other organizations → `org-<id>` (set `namespace` explicitly)
167
167
  - Grafana Cloud → `stacks-<stackId>` (set `stackId`, or `namespace` directly)
168
168
 
169
+ `stackId` is the **numeric** stack id, not the stack slug — for a stack at
170
+ `myorg.grafana.net`, `myorg` is the slug, not the id. A non-numeric `stackId`
171
+ is rejected at startup. Two ways to find the id:
172
+
173
+ - In the Grafana Cloud portal, open the stack; the URL is
174
+ `grafana.com/orgs/<org>/stacks/<id>`.
175
+ - Ask the instance itself, using the same service-account token this plugin
176
+ uses:
177
+
178
+ ```bash
179
+ curl -s -H "Authorization: Bearer $TOKEN" \
180
+ https://<slug>.grafana.net/api/frontend/settings | jq -r .namespace
181
+ # → "stacks-1216502" — the number is the stackId
182
+ ```
183
+
169
184
  ## REST API
170
185
 
171
186
  All routes are mounted under `/api/grafana` and (except `/health`) require a
@@ -187,6 +202,8 @@ valid Backstage credential.
187
202
  - `tag` — repeatable; only dashboards carrying **all** given tags are returned.
188
203
  - `query` — comma-separated, case-insensitive title substrings; dashboards
189
204
  matching **any** value are returned.
205
+ - `uid` — only the dashboard with exactly this uid (case-sensitive) is
206
+ returned; combines with the other filters.
190
207
  - `labelSelector` — `key=value,key2=value2`; only alerts matching **all** pairs.
191
208
  - `instance` — (on `/dashboards` and `/alerts`) restrict to a single instance.
192
209
  - `refresh` — `true` or `1` (or the bare flag) to bypass the store and read
package/dist/index.d.ts CHANGED
@@ -65,6 +65,8 @@ type GetDashboardsOptions = {
65
65
  tags?: string[];
66
66
  /** Comma-separated title substrings; dashboards matching any are returned. */
67
67
  query?: string;
68
+ /** Only return the dashboard with exactly this uid (case-sensitive). */
69
+ uid?: string;
68
70
  /** Force a live fetch, bypassing the store. */
69
71
  refresh?: boolean;
70
72
  };
@@ -33,7 +33,8 @@ class DefaultGrafanaService {
33
33
  result.push(
34
34
  ...backstagePluginGrafanaNode.filterDashboards(snapshot.dashboards, {
35
35
  tags: options.tags,
36
- query: options.query
36
+ query: options.query,
37
+ uid: options.uid
37
38
  })
38
39
  );
39
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GrafanaService.cjs.js","sources":["../../src/service/GrafanaService.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport {\n GrafanaAlert,\n GrafanaDashboard,\n GrafanaInstanceInfo,\n} from '@marble-sh/backstage-plugin-grafana-common';\nimport {\n filterAlerts,\n filterDashboards,\n GrafanaClient,\n GrafanaInstanceConfig,\n} from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaSnapshot, GrafanaStore } from '../store/GrafanaStore';\n\n/**\n * A configured instance together with the client used to read from it.\n *\n * @public\n */\nexport type GrafanaInstance = {\n config: GrafanaInstanceConfig;\n client: GrafanaClient;\n};\n\n/**\n * Options for reading dashboards through the service.\n *\n * @public\n */\nexport type GetDashboardsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return dashboards carrying all of these tags. */\n tags?: string[];\n /** Comma-separated title substrings; dashboards matching any are returned. */\n query?: string;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Options for reading alerts through the service.\n *\n * @public\n */\nexport type GetAlertsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return alerts whose labels match all of these `key=value` pairs. */\n labelSelector?: Record<string, string>;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Reads dashboards and alerts from the configured Grafana instances, backed by a\n * {@link GrafanaStore} for caching and periodic refresh.\n *\n * @public\n */\nexport interface GrafanaService {\n /** Returns the configured Grafana instances. */\n getInstances(): GrafanaInstanceInfo[];\n /** Returns dashboards for one or all instances, honoring the store and filters. */\n getDashboards(options: GetDashboardsOptions): Promise<GrafanaDashboard[]>;\n /** Returns alerts for one or all instances, honoring the store and filters. */\n getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;\n /** Refreshes a single instance, or all instances when no name is given. */\n refresh(instanceName?: string): Promise<void>;\n}\n\n/**\n * The default {@link GrafanaService} implementation.\n *\n * @public\n */\nexport class DefaultGrafanaService implements GrafanaService {\n private readonly instances: Map<string, GrafanaInstance>;\n private readonly store: GrafanaStore;\n private readonly logger: LoggerService;\n private readonly fetchOnDemand: boolean;\n\n constructor(options: {\n instances: GrafanaInstance[];\n store: GrafanaStore;\n logger: LoggerService;\n /**\n * Whether a store miss triggers a live Grafana read (default `true`).\n * When `false`, misses resolve to an empty snapshot and Grafana is only\n * contacted by explicit {@link DefaultGrafanaService.refresh} calls (the\n * schedule, the refresh endpoints, or a `refresh: true` read option).\n */\n fetchOnDemand?: boolean;\n }) {\n this.instances = new Map(\n options.instances.map(instance => [instance.config.name, instance]),\n );\n this.store = options.store;\n this.logger = options.logger;\n this.fetchOnDemand = options.fetchOnDemand ?? true;\n }\n\n /** {@inheritDoc GrafanaService.getInstances} */\n getInstances(): GrafanaInstanceInfo[] {\n return [...this.instances.values()].map(({ config }) => ({\n name: config.name,\n title: config.title,\n url: config.baseUrl,\n }));\n }\n\n /** {@inheritDoc GrafanaService.getDashboards} */\n async getDashboards(\n options: GetDashboardsOptions,\n ): Promise<GrafanaDashboard[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaDashboard[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterDashboards(snapshot.dashboards, {\n tags: options.tags,\n query: options.query,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.getAlerts} */\n async getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaAlert[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterAlerts(snapshot.alerts, {\n labelSelector: options.labelSelector,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.refresh} */\n async refresh(instanceName?: string): Promise<void> {\n if (instanceName) {\n await this.refreshInstance(instanceName);\n return;\n }\n for (const name of this.instances.keys()) {\n try {\n await this.refreshInstance(name);\n } catch (error) {\n this.logger.warn(\n `Failed to refresh Grafana instance '${name}'`,\n error as Error,\n );\n }\n }\n }\n\n private resolveNames(instanceName?: string): string[] {\n if (instanceName) {\n this.mustGet(instanceName);\n return [instanceName];\n }\n return [...this.instances.keys()];\n }\n\n private mustGet(instanceName: string): GrafanaInstance {\n const instance = this.instances.get(instanceName);\n if (!instance) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${instanceName}'`,\n );\n }\n return instance;\n }\n\n private async snapshotFor(\n instanceName: string,\n refresh?: boolean,\n ): Promise<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>> {\n if (!refresh) {\n const cached = await this.store.get(instanceName);\n if (cached) {\n return cached;\n }\n if (!this.fetchOnDemand) {\n // Serve the miss as empty rather than reaching for Grafana; nothing\n // is stored, so results fill in as soon as a refresh runs.\n return { dashboards: [], alerts: [] };\n }\n }\n return this.refreshInstance(instanceName);\n }\n\n private async refreshInstance(\n instanceName: string,\n ): Promise<GrafanaSnapshot> {\n const { client } = this.mustGet(instanceName);\n const [dashboards, alerts] = await Promise.all([\n client.listDashboards(),\n client.listAlerts(),\n ]);\n const snapshot: GrafanaSnapshot = {\n dashboards,\n alerts,\n fetchedAt: new Date().toISOString(),\n };\n await this.store.set(instanceName, snapshot);\n this.logger.debug(\n `Refreshed Grafana instance '${instanceName}': ${dashboards.length} dashboards, ${alerts.length} alerts`,\n );\n return snapshot;\n }\n}\n"],"names":["filterDashboards","filterAlerts","NotFoundError"],"mappings":"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"}
1
+ {"version":3,"file":"GrafanaService.cjs.js","sources":["../../src/service/GrafanaService.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport {\n GrafanaAlert,\n GrafanaDashboard,\n GrafanaInstanceInfo,\n} from '@marble-sh/backstage-plugin-grafana-common';\nimport {\n filterAlerts,\n filterDashboards,\n GrafanaClient,\n GrafanaInstanceConfig,\n} from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaSnapshot, GrafanaStore } from '../store/GrafanaStore';\n\n/**\n * A configured instance together with the client used to read from it.\n *\n * @public\n */\nexport type GrafanaInstance = {\n config: GrafanaInstanceConfig;\n client: GrafanaClient;\n};\n\n/**\n * Options for reading dashboards through the service.\n *\n * @public\n */\nexport type GetDashboardsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return dashboards carrying all of these tags. */\n tags?: string[];\n /** Comma-separated title substrings; dashboards matching any are returned. */\n query?: string;\n /** Only return the dashboard with exactly this uid (case-sensitive). */\n uid?: string;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Options for reading alerts through the service.\n *\n * @public\n */\nexport type GetAlertsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return alerts whose labels match all of these `key=value` pairs. */\n labelSelector?: Record<string, string>;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Reads dashboards and alerts from the configured Grafana instances, backed by a\n * {@link GrafanaStore} for caching and periodic refresh.\n *\n * @public\n */\nexport interface GrafanaService {\n /** Returns the configured Grafana instances. */\n getInstances(): GrafanaInstanceInfo[];\n /** Returns dashboards for one or all instances, honoring the store and filters. */\n getDashboards(options: GetDashboardsOptions): Promise<GrafanaDashboard[]>;\n /** Returns alerts for one or all instances, honoring the store and filters. */\n getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;\n /** Refreshes a single instance, or all instances when no name is given. */\n refresh(instanceName?: string): Promise<void>;\n}\n\n/**\n * The default {@link GrafanaService} implementation.\n *\n * @public\n */\nexport class DefaultGrafanaService implements GrafanaService {\n private readonly instances: Map<string, GrafanaInstance>;\n private readonly store: GrafanaStore;\n private readonly logger: LoggerService;\n private readonly fetchOnDemand: boolean;\n\n constructor(options: {\n instances: GrafanaInstance[];\n store: GrafanaStore;\n logger: LoggerService;\n /**\n * Whether a store miss triggers a live Grafana read (default `true`).\n * When `false`, misses resolve to an empty snapshot and Grafana is only\n * contacted by explicit {@link DefaultGrafanaService.refresh} calls (the\n * schedule, the refresh endpoints, or a `refresh: true` read option).\n */\n fetchOnDemand?: boolean;\n }) {\n this.instances = new Map(\n options.instances.map(instance => [instance.config.name, instance]),\n );\n this.store = options.store;\n this.logger = options.logger;\n this.fetchOnDemand = options.fetchOnDemand ?? true;\n }\n\n /** {@inheritDoc GrafanaService.getInstances} */\n getInstances(): GrafanaInstanceInfo[] {\n return [...this.instances.values()].map(({ config }) => ({\n name: config.name,\n title: config.title,\n url: config.baseUrl,\n }));\n }\n\n /** {@inheritDoc GrafanaService.getDashboards} */\n async getDashboards(\n options: GetDashboardsOptions,\n ): Promise<GrafanaDashboard[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaDashboard[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterDashboards(snapshot.dashboards, {\n tags: options.tags,\n query: options.query,\n uid: options.uid,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.getAlerts} */\n async getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaAlert[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterAlerts(snapshot.alerts, {\n labelSelector: options.labelSelector,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.refresh} */\n async refresh(instanceName?: string): Promise<void> {\n if (instanceName) {\n await this.refreshInstance(instanceName);\n return;\n }\n for (const name of this.instances.keys()) {\n try {\n await this.refreshInstance(name);\n } catch (error) {\n this.logger.warn(\n `Failed to refresh Grafana instance '${name}'`,\n error as Error,\n );\n }\n }\n }\n\n private resolveNames(instanceName?: string): string[] {\n if (instanceName) {\n this.mustGet(instanceName);\n return [instanceName];\n }\n return [...this.instances.keys()];\n }\n\n private mustGet(instanceName: string): GrafanaInstance {\n const instance = this.instances.get(instanceName);\n if (!instance) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${instanceName}'`,\n );\n }\n return instance;\n }\n\n private async snapshotFor(\n instanceName: string,\n refresh?: boolean,\n ): Promise<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>> {\n if (!refresh) {\n const cached = await this.store.get(instanceName);\n if (cached) {\n return cached;\n }\n if (!this.fetchOnDemand) {\n // Serve the miss as empty rather than reaching for Grafana; nothing\n // is stored, so results fill in as soon as a refresh runs.\n return { dashboards: [], alerts: [] };\n }\n }\n return this.refreshInstance(instanceName);\n }\n\n private async refreshInstance(\n instanceName: string,\n ): Promise<GrafanaSnapshot> {\n const { client } = this.mustGet(instanceName);\n const [dashboards, alerts] = await Promise.all([\n client.listDashboards(),\n client.listAlerts(),\n ]);\n const snapshot: GrafanaSnapshot = {\n dashboards,\n alerts,\n fetchedAt: new Date().toISOString(),\n };\n await this.store.set(instanceName, snapshot);\n this.logger.debug(\n `Refreshed Grafana instance '${instanceName}': ${dashboards.length} dashboards, ${alerts.length} alerts`,\n );\n return snapshot;\n }\n}\n"],"names":["filterDashboards","filterAlerts","NotFoundError"],"mappings":"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"}
@@ -42,6 +42,7 @@ async function createRouter(options) {
42
42
  instanceName: req.params.name,
43
43
  tags: toArray(req.query.tag),
44
44
  query: toString(req.query.query),
45
+ uid: toString(req.query.uid),
45
46
  refresh: toRefresh(req.query.refresh)
46
47
  });
47
48
  res.json({ items });
@@ -64,6 +65,7 @@ async function createRouter(options) {
64
65
  instanceName: toString(req.query.instance),
65
66
  tags: toArray(req.query.tag),
66
67
  query: toString(req.query.query),
68
+ uid: toString(req.query.uid),
67
69
  refresh: toRefresh(req.query.refresh)
68
70
  });
69
71
  res.json({ items });
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { NotAllowedError } from '@backstage/errors';\nimport { parseLabelSelector } from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaService } from './GrafanaService';\n\nconst toArray = (value: unknown): string[] | undefined => {\n if (value === undefined) {\n return undefined;\n }\n return Array.isArray(value) ? value.map(String) : [String(value)];\n};\n\nconst toString = (value: unknown): string | undefined =>\n value === undefined ? undefined : String(value);\n\n// Accepts ?refresh=true, ?refresh=1, and the bare ?refresh flag.\nconst toBoolean = (value: unknown): boolean =>\n value !== undefined && ['true', '1', ''].includes(String(value));\n\n/**\n * Creates the Express router that exposes the read-only Grafana REST API.\n *\n * All routes are relative to the plugin base path (`/api/grafana`).\n *\n * @public\n */\nexport async function createRouter(options: {\n grafanaService: GrafanaService;\n /**\n * Whether callers may force live Grafana reads (default `true`). When\n * `false`, `refresh` query parameters are ignored and the `POST …/refresh`\n * routes respond 403.\n */\n allowOnDemandRefresh?: boolean;\n}): Promise<express.Router> {\n const { grafanaService } = options;\n const allowOnDemandRefresh = options.allowOnDemandRefresh ?? true;\n const router = Router();\n router.use(express.json());\n\n const toRefresh = (value: unknown): boolean =>\n allowOnDemandRefresh && toBoolean(value);\n\n const assertRefreshAllowed = () => {\n if (!allowOnDemandRefresh) {\n throw new NotAllowedError(\n 'On-demand refresh is disabled by configuration (grafana.allowOnDemandRefresh)',\n );\n }\n };\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/instances', (_req, res) => {\n res.json({ items: grafanaService.getInstances() });\n });\n\n router.get('/instances/:name/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: req.params.name,\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/instances/:name/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: req.params.name,\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/instances/:name/refresh', async (req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh(req.params.name);\n res.json({ status: 'ok' });\n });\n\n router.get('/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: toString(req.query.instance),\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: toString(req.query.instance),\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/refresh', async (_req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh();\n res.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["Router","express","NotAllowedError","parseLabelSelector"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { NotAllowedError } from '@backstage/errors';\nimport { parseLabelSelector } from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaService } from './GrafanaService';\n\nconst toArray = (value: unknown): string[] | undefined => {\n if (value === undefined) {\n return undefined;\n }\n return Array.isArray(value) ? value.map(String) : [String(value)];\n};\n\nconst toString = (value: unknown): string | undefined =>\n value === undefined ? undefined : String(value);\n\n// Accepts ?refresh=true, ?refresh=1, and the bare ?refresh flag.\nconst toBoolean = (value: unknown): boolean =>\n value !== undefined && ['true', '1', ''].includes(String(value));\n\n/**\n * Creates the Express router that exposes the read-only Grafana REST API.\n *\n * All routes are relative to the plugin base path (`/api/grafana`).\n *\n * @public\n */\nexport async function createRouter(options: {\n grafanaService: GrafanaService;\n /**\n * Whether callers may force live Grafana reads (default `true`). When\n * `false`, `refresh` query parameters are ignored and the `POST …/refresh`\n * routes respond 403.\n */\n allowOnDemandRefresh?: boolean;\n}): Promise<express.Router> {\n const { grafanaService } = options;\n const allowOnDemandRefresh = options.allowOnDemandRefresh ?? true;\n const router = Router();\n router.use(express.json());\n\n const toRefresh = (value: unknown): boolean =>\n allowOnDemandRefresh && toBoolean(value);\n\n const assertRefreshAllowed = () => {\n if (!allowOnDemandRefresh) {\n throw new NotAllowedError(\n 'On-demand refresh is disabled by configuration (grafana.allowOnDemandRefresh)',\n );\n }\n };\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/instances', (_req, res) => {\n res.json({ items: grafanaService.getInstances() });\n });\n\n router.get('/instances/:name/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: req.params.name,\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n uid: toString(req.query.uid),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/instances/:name/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: req.params.name,\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/instances/:name/refresh', async (req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh(req.params.name);\n res.json({ status: 'ok' });\n });\n\n router.get('/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: toString(req.query.instance),\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n uid: toString(req.query.uid),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: toString(req.query.instance),\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/refresh', async (_req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh();\n res.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["Router","express","NotAllowedError","parseLabelSelector"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marble-sh/backstage-plugin-grafana-backend",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Backend for the Grafana Backstage plugin: reads dashboards and alerts from Grafana, caches them, and exposes a read-only REST API",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,19 +33,19 @@
33
33
  "test": "backstage-cli package test"
34
34
  },
35
35
  "dependencies": {
36
- "@backstage/backend-plugin-api": "backstage:^",
37
- "@backstage/config": "backstage:^",
38
- "@backstage/errors": "backstage:^",
39
- "@backstage/types": "backstage:^",
40
- "@marble-sh/backstage-plugin-grafana-common": "workspace:^",
41
- "@marble-sh/backstage-plugin-grafana-node": "workspace:^",
36
+ "@backstage/backend-plugin-api": "^1.10.0",
37
+ "@backstage/config": "^1.3.8",
38
+ "@backstage/errors": "^1.3.1",
39
+ "@backstage/types": "^1.2.2",
40
+ "@marble-sh/backstage-plugin-grafana-common": "^1.1.0",
41
+ "@marble-sh/backstage-plugin-grafana-node": "^1.1.0",
42
42
  "express": "^4.17.1",
43
43
  "express-promise-router": "^4.1.0",
44
44
  "knex": "^3.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@backstage/backend-test-utils": "backstage:^",
48
- "@backstage/cli": "backstage:^",
47
+ "@backstage/backend-test-utils": "^1.11.6",
48
+ "@backstage/cli": "^0.36.5",
49
49
  "@types/express": "^4.17.6",
50
50
  "@types/supertest": "^2.0.12",
51
51
  "supertest": "^6.2.4"
@@ -84,4 +84,4 @@
84
84
  ]
85
85
  }
86
86
  }
87
- }
87
+ }