@grafana/create-plugin 7.15.0 → 7.16.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/codemods/additions/prompts/docs-content.md +10 -4
  3. package/dist/codemods/additions/scripts/docs.js +32 -8
  4. package/dist/codemods/additions/scripts/experimental-app-sdk.js +40 -0
  5. package/dist/codemods/migrations/manager.js +6 -0
  6. package/dist/commands/update.command.js +7 -1
  7. package/package.json +2 -1
  8. package/src/codemods/additions/prompts/docs-content.md +10 -4
  9. package/src/codemods/additions/scripts/docs.test.ts +137 -39
  10. package/src/codemods/additions/scripts/docs.ts +47 -7
  11. package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +59 -2
  12. package/src/codemods/additions/scripts/experimental-app-sdk.ts +55 -0
  13. package/src/codemods/migrations/manager.test.ts +10 -0
  14. package/src/codemods/migrations/manager.ts +10 -0
  15. package/src/commands/update.command.ts +9 -1
  16. package/templates/common/.config/docker-compose-base.yaml +1 -1
  17. package/templates/common/_package.json +6 -6
  18. package/templates/docs/app/docs/AGENTS.md +4 -0
  19. package/templates/docs/app/docs/configure.md +5 -1
  20. package/templates/docs/app/docs/dashboards.md +4 -0
  21. package/templates/docs/app/docs/index.md +6 -2
  22. package/templates/docs/app/docs/permissions.md +1 -1
  23. package/templates/docs/app/docs/troubleshooting.md +5 -1
  24. package/templates/docs/app/feature-page.md +7 -5
  25. package/templates/docs/datasource/docs/AGENTS.md +2 -0
  26. package/templates/docs/datasource/docs/configure.md +6 -2
  27. package/templates/docs/datasource/docs/dashboards.md +4 -0
  28. package/templates/docs/datasource/docs/troubleshooting.md +5 -1
  29. package/templates/docs/panel/docs/AGENTS.md +2 -0
  30. package/templates/docs/panel/docs/data-formats.md +4 -0
  31. package/templates/docs/panel/docs/examples.md +5 -1
  32. package/templates/docs/panel/docs/options.md +1 -1
@@ -125,6 +125,8 @@ interface PageStubs {
125
125
  stubs: PageStub[];
126
126
  /** Entries in `src/plugin.json` that get no page, as `name (path)`. */
127
127
  skipped: string[];
128
+ /** The file name an app's only page gets, which is still there when the app grows to more pages. */
129
+ appPageSlug?: string;
128
130
  }
129
131
 
130
132
  interface PageStub {
@@ -218,6 +220,11 @@ export function setupDocsScaffolding(opts: DocsSetupOptions): Context {
218
220
  // a re-run has no pages to compare against, so it only reports what the first scaffold left out
219
221
  skippedPages: hasPages ? [] : pageStubs.skipped,
220
222
  };
223
+ const { appPageSlug } = pageStubs;
224
+ if (appPageSlug && report.missingPageStubs.length > 0) {
225
+ // the author may have moved it into features/ or turned it into a folder
226
+ report.appNamedPage = findPage(context, docsPath, [appPageSlug, `features/${appPageSlug}`]);
227
+ }
221
228
 
222
229
  // left alone when it exists, since the author may have customized it
223
230
  const workflowPath = '.github/workflows/validate-docs.yml';
@@ -251,13 +258,26 @@ interface PageReport {
251
258
  missingPageStubs: string[];
252
259
  /** Entries in plugin.json that got no page on the first scaffold, with the reason. */
253
260
  skippedPages: string[];
261
+ /** The page named after the app, when the app has grown past one page and pages are missing. */
262
+ appNamedPage?: string;
254
263
  }
255
264
 
256
265
  // a page counts as there as a file or as a folder, so authors can turn a page into a folder
257
266
  function pageExists(context: Context, docsPath: string, pageNames: string[]): boolean {
258
- return pageNames.some(
259
- (name) => context.doesFileExist(`${docsPath}/${name}.md`) || context.doesFileExist(`${docsPath}/${name}`)
260
- );
267
+ return findPage(context, docsPath, pageNames) !== undefined;
268
+ }
269
+
270
+ /** The path of the first page that exists, as `<name>.md` or as the folder `<name>/`. */
271
+ function findPage(context: Context, docsPath: string, pageNames: string[]): string | undefined {
272
+ for (const name of pageNames) {
273
+ if (context.doesFileExist(`${docsPath}/${name}.md`)) {
274
+ return `${docsPath}/${name}.md`;
275
+ }
276
+ if (context.doesFileExist(`${docsPath}/${name}`)) {
277
+ return `${docsPath}/${name}/`;
278
+ }
279
+ }
280
+ return undefined;
261
281
  }
262
282
 
263
283
  /**
@@ -305,7 +325,7 @@ function buildNextStepsMessage(opts: {
305
325
  report: PageReport;
306
326
  }): ContextMessage {
307
327
  const { docsPath, packageManagerName, createPluginCmd, pluginActions, linkedRootFiles } = opts;
308
- const { missingFeaturePages, missingPageStubs, skippedPages } = opts.report;
328
+ const { missingFeaturePages, missingPageStubs, skippedPages, appNamedPage } = opts.report;
309
329
  const run = (script: string) => output.formatCode(`${packageManagerName} run ${script}`);
310
330
 
311
331
  const steps: NextStep[] = [
@@ -350,7 +370,9 @@ function buildNextStepsMessage(opts: {
350
370
  if (missingPageStubs.length > 0) {
351
371
  warnings.push({
352
372
  title: `src/plugin.json has pages with no docs page, such as ${missingPageStubs[0]}`,
353
- detail: `If they need docs, add them as described under Optional pages in ${docsPath}/AGENTS.md. If you deleted them on purpose, ignore this.`,
373
+ detail: appNamedPage
374
+ ? `${appNamedPage} is named after the app, which happens while the app has one page. Rename it after the page it documents, then add pages for the others as described under Optional pages in ${docsPath}/AGENTS.md. If you deleted pages on purpose, ignore this.`
375
+ : `If they need docs, add them as described under Optional pages in ${docsPath}/AGENTS.md. If you deleted them on purpose, ignore this.`,
354
376
  });
355
377
  }
356
378
  if (skippedPages.length > 0) {
@@ -494,7 +516,11 @@ const TEMPLATE_DEFAULT_PAGES: Record<string, string> = {
494
516
  const CONFIG_PAGE_SLUGS = ['configuration', 'configure'];
495
517
 
496
518
  interface AppFeaturePage {
519
+ /** The name in plugin.json, which is how the app's own navigation labels the page. */
497
520
  name: string;
521
+ /** What the docs call the page, which is the app's name when it has no other page. */
522
+ title: string;
523
+ isOnlyPage?: boolean;
498
524
  slug: string;
499
525
  path: string;
500
526
  role?: string;
@@ -531,7 +557,7 @@ function getAppFeaturePages(
531
557
  const role = typeof include.role === 'string' && include.role !== 'Viewer' ? include.role : undefined;
532
558
  // an in-app page named like the config page takes its place, the way data sources name it
533
559
  if (!hasConfigPage && !configPage && CONFIG_PAGE_SLUGS.includes(slug)) {
534
- configPage = { name, slug: 'configure', path, role };
560
+ configPage = { name, title: name, slug: 'configure', path, role };
535
561
  continue;
536
562
  }
537
563
  if (takenSlugs.has(slug)) {
@@ -540,11 +566,22 @@ function getAppFeaturePages(
540
566
  continue;
541
567
  }
542
568
  takenSlugs.add(slug);
543
- pages.push({ name, slug, path, role });
569
+ pages.push({ name, title: name, slug, path, role });
570
+ }
571
+
572
+ // a single page is the whole app, and its navigation label is often a generic one such as Overview or Home
573
+ const appSlug = getAppPageSlug(pluginJson);
574
+ if (pages.length === 1 && appSlug) {
575
+ pages[0] = { ...pages[0], title: pluginJson.name as string, slug: appSlug, isOnlyPage: true };
544
576
  }
545
577
  return { pages, configPage, skipped };
546
578
  }
547
579
 
580
+ function getAppPageSlug(pluginJson: PluginJson): string | undefined {
581
+ const slug = slugify(pluginJson.name ?? '');
582
+ return slug && !getReservedPageSlugs().includes(slug) ? slug : undefined;
583
+ }
584
+
548
585
  function slugify(name: string): string {
549
586
  return name
550
587
  .normalize('NFKD')
@@ -583,6 +620,7 @@ function getAppFeaturePageStubs(pluginJson: PluginJson, featureFlags: Record<str
583
620
  });
584
621
  }
585
622
 
623
+ const appPageSlug = getAppPageSlug(pluginJson);
586
624
  const isInFolder = pages.length > MAX_FLAT_PAGES;
587
625
  const folder = isInFolder ? 'features/' : '';
588
626
  if (isInFolder) {
@@ -610,6 +648,8 @@ function getAppFeaturePageStubs(pluginJson: PluginJson, featureFlags: Record<str
610
648
  },
611
649
  stubs,
612
650
  skipped,
651
+ // a page that is already called like the app is the page the author wants, not one to rename
652
+ appPageSlug: pages.some((page) => page.slug === appPageSlug) ? undefined : appPageSlug,
613
653
  };
614
654
  }
615
655
 
@@ -245,6 +245,62 @@ describe('experimental-app-sdk addition', () => {
245
245
  });
246
246
  });
247
247
 
248
+ describe('grafana version override', () => {
249
+ it('adds the image and version build args', () => {
250
+ const result = appSdk(createAppContext());
251
+
252
+ const compose = result.getFile('docker-compose.yaml') ?? '';
253
+ expect(compose).toContain('grafana_image: ${GRAFANA_IMAGE:-grafana}');
254
+ expect(compose).toContain('grafana_version: ${GRAFANA_VERSION:-nightly}');
255
+ });
256
+
257
+ it('adds the build args even when the base compose file already enables the toggles', () => {
258
+ const context = createAppContext();
259
+ context.addFile(
260
+ '.config/docker-compose-base.yaml',
261
+ `services:
262
+ grafana:
263
+ environment:
264
+ GF_FEATURE_TOGGLES_ENABLE: appplugins.loadAppManifest,appplugins.registerAPIServer,grafana.useRouterMiddleware
265
+ `
266
+ );
267
+
268
+ const result = appSdk(context);
269
+
270
+ expect(result.getFile('docker-compose.yaml')).toContain('grafana_version: ${GRAFANA_VERSION:-nightly}');
271
+ });
272
+
273
+ it('keeps build args the user already set', () => {
274
+ const context = createAppContext({
275
+ compose: `services:
276
+ grafana:
277
+ extends:
278
+ file: .config/docker-compose-base.yaml
279
+ service: grafana
280
+ build:
281
+ args:
282
+ grafana_version: 12.0.0
283
+ `,
284
+ });
285
+
286
+ const result = appSdk(context);
287
+
288
+ const compose = result.getFile('docker-compose.yaml') ?? '';
289
+ expect(compose).toContain('grafana_version: 12.0.0');
290
+ expect(compose).not.toContain('GRAFANA_VERSION');
291
+ expect(compose).toContain('grafana_image: ${GRAFANA_IMAGE:-grafana}');
292
+ });
293
+
294
+ it('is idempotent', () => {
295
+ const once = appSdk(createAppContext());
296
+ const compose = once.getFile('docker-compose.yaml');
297
+
298
+ const twice = appSdk(once);
299
+
300
+ expect(twice.getFile('docker-compose.yaml')).toBe(compose);
301
+ });
302
+ });
303
+
248
304
  describe('feature toggle', () => {
249
305
  it('enables the app-sdk manifest toggle', () => {
250
306
  const context = createAppContext();
@@ -328,7 +384,7 @@ describe('experimental-app-sdk addition', () => {
328
384
 
329
385
  const result = appSdk(context);
330
386
 
331
- expect(result.getFile('docker-compose.yaml')).toBe(STOCK_COMPOSE);
387
+ expect(result.getFile('docker-compose.yaml')).not.toContain('GF_FEATURE_TOGGLES_ENABLE');
332
388
  });
333
389
 
334
390
  it('leaves a list-style environment block alone', () => {
@@ -341,7 +397,8 @@ describe('experimental-app-sdk addition', () => {
341
397
 
342
398
  const result = appSdk(context);
343
399
 
344
- expect(result.getFile('docker-compose.yaml')).toBe(listCompose);
400
+ expect(result.getFile('docker-compose.yaml')).toContain('- GF_FEATURE_TOGGLES_ENABLE=someOtherToggle');
401
+ expect(result.getFile('docker-compose.yaml')).not.toContain('appplugins.loadAppManifest');
345
402
  // The rest of the addition still applies.
346
403
  expect(result.doesFileExist('kinds/config.cue')).toBe(true);
347
404
  });
@@ -59,6 +59,7 @@ export default function appSdk(context: Context): Context {
59
59
  referenceAgentInstructions(context, data);
60
60
  addGenerateScript(context);
61
61
  addFeatureToggle(context);
62
+ addGrafanaVersionOverride(context);
62
63
  wireGoBackend(context, data);
63
64
 
64
65
  // Only guide the user when we actually scaffolded something; a re-run should stay quiet.
@@ -240,6 +241,60 @@ function addFeatureToggle(context: Context) {
240
241
  context.updateFile(composePath, stringify(composeData, { lineWidth: 120, singleQuote: true }));
241
242
  }
242
243
 
244
+ // The app-sdk needs a newer Grafana than the default the plugin was scaffolded with, so the root
245
+ // compose file overrides the build args, while still letting GRAFANA_IMAGE/GRAFANA_VERSION win.
246
+ const GRAFANA_BUILD_ARGS: Record<string, string> = {
247
+ grafana_image: '${GRAFANA_IMAGE:-grafana}',
248
+ grafana_version: '${GRAFANA_VERSION:-nightly}',
249
+ };
250
+
251
+ /**
252
+ * Overrides the default Grafana image and version in the root docker-compose.yaml. Build args the
253
+ * user has already set are left alone.
254
+ */
255
+ function addGrafanaVersionOverride(context: Context) {
256
+ const composePath = 'docker-compose.yaml';
257
+ const composeContent = context.getFile(composePath);
258
+
259
+ if (!composeContent) {
260
+ additionsDebug(`Could not find ${composePath}. Skipping the Grafana version override.`);
261
+ return;
262
+ }
263
+
264
+ const composeData = parseDocument(composeContent);
265
+
266
+ if (!(composeData.getIn(['services', 'grafana']) instanceof YAMLMap)) {
267
+ additionsDebug(`Could not find the grafana service in ${composePath}. Skipping the Grafana version override.`);
268
+ return;
269
+ }
270
+
271
+ const build = composeData.getIn(['services', 'grafana', 'build']);
272
+ if (build !== undefined && !(build instanceof YAMLMap)) {
273
+ additionsDebug(`services.grafana.build in ${composePath} is not a mapping. Skipping the Grafana version override.`);
274
+ return;
275
+ }
276
+
277
+ const args = composeData.getIn(['services', 'grafana', 'build', 'args']);
278
+ if (args !== undefined && !(args instanceof YAMLMap)) {
279
+ additionsDebug(
280
+ `services.grafana.build.args in ${composePath} is not a mapping. Skipping the Grafana version override.`
281
+ );
282
+ return;
283
+ }
284
+
285
+ let changed = false;
286
+ for (const [name, value] of Object.entries(GRAFANA_BUILD_ARGS)) {
287
+ if (!composeData.hasIn(['services', 'grafana', 'build', 'args', name])) {
288
+ composeData.setIn(['services', 'grafana', 'build', 'args', name], value);
289
+ changed = true;
290
+ }
291
+ }
292
+
293
+ if (changed) {
294
+ context.updateFile(composePath, stringify(composeData, { lineWidth: 120, singleQuote: true }));
295
+ }
296
+ }
297
+
243
298
  /**
244
299
  * Wires the generated kinds into the Go backend, for app plugins that have one. Go code generation
245
300
  * itself is enabled when kinds/config.cue is first scaffolded, in addTemplateFiles.
@@ -339,5 +339,15 @@ describe('Migrations', () => {
339
339
 
340
340
  expect(setRootConfig).not.toHaveBeenCalled();
341
341
  });
342
+
343
+ it('should NOT update version in ".config/.cprc.json" if no migrations made changes', async () => {
344
+ migrationOneFn.mockImplementation(async (context: Context) => context);
345
+ migrationTwoFn.mockImplementation(async (context: Context) => context);
346
+
347
+ const didMigrationsChangeFiles = await runMigrations(migrations);
348
+
349
+ expect(didMigrationsChangeFiles).toBe(false);
350
+ expect(setRootConfig).not.toHaveBeenCalled();
351
+ });
342
352
  });
343
353
  });
@@ -57,21 +57,31 @@ export async function runMigrations(migrations: Migration[], options: RunMigrati
57
57
 
58
58
  output.log({ title: 'Running the following migrations:', body: migrationListBody });
59
59
 
60
+ let hasAnyChanges = false;
61
+
60
62
  // run migrations sequentially in version order where lowest version runs first
61
63
  for (const migration of migrations) {
62
64
  const context = await runCodemod(migration, options.codemodOptions);
63
65
  const shouldCommit = options.commitEachMigration && context.hasChanges();
64
66
 
67
+ hasAnyChanges ||= context.hasChanges();
68
+
65
69
  if (shouldCommit) {
66
70
  // for conventional commits we need to add a newline between the title and the description
67
71
  await gitCommitNoVerify(`chore: run create-plugin migration - ${migration.name}\n\n${migration.description}`);
68
72
  }
69
73
  }
70
74
 
75
+ if (!hasAnyChanges) {
76
+ return false;
77
+ }
78
+
71
79
  await setRootConfig({ version: CURRENT_APP_VERSION });
72
80
 
73
81
  // Nothing to commit when only unreleased migrations re-ran on a plugin already on this version.
74
82
  if (options.commitEachMigration && !(await isGitDirectoryClean())) {
75
83
  await gitCommitNoVerify(`chore: update .config/.cprc.json to version ${CURRENT_APP_VERSION}.`);
76
84
  }
85
+
86
+ return true;
77
87
  }
@@ -43,10 +43,18 @@ export const update = async (argv: minimist.ParsedArgs) => {
43
43
 
44
44
  // filter out minimist internal properties (_ and $0) before passing to codemod
45
45
  const { _, $0, ...codemodOptions } = argv;
46
- await runMigrations(migrations, {
46
+ const didMigrationsChangeFiles = await runMigrations(migrations, {
47
47
  commitEachMigration: !!argv.commit,
48
48
  codemodOptions,
49
49
  });
50
+
51
+ if (!didMigrationsChangeFiles) {
52
+ output.log({
53
+ title: 'No migrations resulted in changes.',
54
+ });
55
+ process.exit(0);
56
+ }
57
+
50
58
  output.success({
51
59
  title: `Successfully updated create-plugin from ${version} to ${CURRENT_APP_VERSION}.`,
52
60
  });
@@ -7,7 +7,7 @@ services:
7
7
  context: .
8
8
  args:
9
9
  grafana_image: ${GRAFANA_IMAGE:-{{~grafanaImage~}} }
10
- grafana_version: ${GRAFANA_VERSION:-13.2.3}
10
+ grafana_version: ${GRAFANA_VERSION:-13.2.4}
11
11
  development: ${DEVELOPMENT:-false}
12
12
  anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
13
13
  ports:
@@ -17,7 +17,7 @@
17
17
  "license": "Apache-2.0",
18
18
  "devDependencies": {
19
19
  "@grafana/eslint-config": "^10.0.0",
20
- "@grafana/plugin-e2e": "^3.15.0",
20
+ "@grafana/plugin-e2e": "^3.15.1",
21
21
  "@grafana/sign-plugin": "^3.3.3",
22
22
  "@grafana/tsconfig": "^2.2.0",
23
23
  "@playwright/test": "^1.62.1",{{#if useExperimentalRspack}}
@@ -77,11 +77,11 @@
77
77
  },
78
78
  "dependencies": {
79
79
  "@emotion/css": "11.10.6",
80
- "@grafana/data": "13.2.3",
81
- "@grafana/i18n": "13.2.3",
82
- "@grafana/runtime": "13.2.3",
83
- "@grafana/ui": "13.2.3",
84
- "@grafana/schema": "13.2.3",{{#if_eq pluginType "scenesapp" }}
80
+ "@grafana/data": "13.2.4",
81
+ "@grafana/i18n": "13.2.4",
82
+ "@grafana/runtime": "13.2.4",
83
+ "@grafana/ui": "13.2.4",
84
+ "@grafana/schema": "13.2.4",{{#if_eq pluginType "scenesapp" }}
85
85
  "@grafana/scenes": "{{ scenesVersion }}",{{/if_eq}}
86
86
  "react": "^19.0.0",
87
87
  "react-dom": "^19.0.0"{{#if isAppType}},
@@ -52,6 +52,8 @@ a `sidebar_position` that puts it where it belongs and link it from `index.md`.
52
52
  | An `includes` entry of type `dashboard` | `dashboards.md` | What each bundled dashboard shows and how to import it. |
53
53
  | `roles` or `iam` in `src/plugin.json` | `permissions.md` | What each role grants, default assignments and what the app can access. |
54
54
 
55
+ An app with a single page names that page after the app, because its navigation label is often a generic one such as Overview or Home. Keep that name when you add a second page only if it still describes the page, and otherwise rename the page after what it shows.
56
+
55
57
  When an app page grows too long, turn it into a folder of the same name with an `index.md` and
56
58
  split it into pages there. Keep every page within three folder levels of this one.
57
59
 
@@ -106,6 +108,8 @@ To fill a section:
106
108
  Briefs are not shown in the preview or the catalog, so a section you haven't written looks like a bare
107
109
  heading. `docs:validate` lists every section that still has only its brief.
108
110
 
111
+ Text between `<!-- snippet:... -->` and `<!-- /snippet:... -->` markers is maintained by Grafana. Never edit inside it - refer to `snippets.md` in the guides.
112
+
109
113
  ## Validate
110
114
 
111
115
  ```bash
@@ -20,12 +20,16 @@ Before you configure the app, ensure you have:
20
20
 
21
21
  ## Open the configuration page
22
22
 
23
+ <!-- snippet:open-app-config -->
24
+
23
25
  To open the {{{pluginName}}} configuration page:
24
26
 
25
27
  1. Click **Administration** > **Plugins and data** > **Plugins** in the left-side menu.
26
28
  1. Enter `{{{pluginName}}}` in the search bar.
27
29
  1. Select **{{{pluginName}}}**.
28
30
 
31
+ <!-- /snippet:open-app-config -->
32
+
29
33
  <!-- section-brief
30
34
  Fill this in: After the list above, add one sentence that names the tab to click, using the `title` of the page passed to `addConfigPage` in `src/module.ts(x)`, such as "Then click the **Configuration** tab." Keep it a sentence rather than another step of the list. If the app adds more than one config page, name each tab and what it holds.
31
35
  -->
@@ -38,7 +42,7 @@ Fill this in: An H3 for each section of the config page, named and ordered as th
38
42
  | Setting | Description | Required |
39
43
  | ------- | ----------- | -------- |
40
44
 
41
- Use the field labels from the config page, in bold. If the config page has no settings of its own, such as when it only turns the app on or off, say so in one sentence and document what its buttons do instead of a table. Find the settings in the components passed to `addConfigPage` and the settings type, including `jsonData` and `secureJsonData`. In a description, say when a field only appears for some choice and when it affects another setting. Use a `> [!NOTE]` callout for a setting that's only available in Grafana Cloud or Grafana Enterprise.
45
+ Use the field labels from the config page, in bold. If the config page has no settings of its own, such as when it only turns the app on or off, say so in one sentence and document what its buttons do instead of a table. Find the settings in the components passed to `addConfigPage` and the settings type, including `jsonData` and `secureJsonData`. When a field's default matches none of the values it offers, such as a different case, give what the page actually does with it. In a description, say when a field only appears for some choice and when it affects another setting. Use a `> [!NOTE]` callout for a setting that's only available in Grafana Cloud or Grafana Enterprise.
42
46
 
43
47
  End with the button that saves the settings and what the reader sees when it works.
44
48
  -->
@@ -12,6 +12,8 @@ sidebar_position: 6
12
12
 
13
13
  ## Import a dashboard
14
14
 
15
+ <!-- snippet:import-app-dashboard -->
16
+
15
17
  To import a dashboard that ships with {{{pluginName}}}:
16
18
 
17
19
  1. Click **Administration** > **Plugins and data** > **Plugins** in the left-side menu.
@@ -22,6 +24,8 @@ To import a dashboard that ships with {{{pluginName}}}:
22
24
 
23
25
  Imported dashboards don't update when you upgrade the plugin. When a plugin upgrade ships a newer version of a dashboard, click **Update** next to it. This overwrites any changes you made to it.
24
26
 
27
+ <!-- /snippet:import-app-dashboard -->
28
+
25
29
  ## Included dashboards
26
30
 
27
31
  <!-- section-brief
@@ -24,6 +24,8 @@ Fill this in: Keep the list above. Add a bullet in the same **Label:** form for
24
24
 
25
25
  ## Enable the app
26
26
 
27
+ <!-- snippet:enable-app -->
28
+
27
29
  To enable {{{pluginName}}}:
28
30
 
29
31
  1. Click **Administration** > **Plugins and data** > **Plugins** in the left-side menu.
@@ -31,8 +33,10 @@ To enable {{{pluginName}}}:
31
33
  1. Select **{{{pluginName}}}**.
32
34
  1. Click **Enable**.
33
35
 
36
+ <!-- /snippet:enable-app -->
37
+
34
38
  <!-- section-brief
35
- Fill this in: If `src/plugin.json` sets `"autoEnabled": true`, the app is enabled when it's installed, so remove this section. Otherwise keep the steps above and remove only this brief.
39
+ Fill this in: If `src/plugin.json` sets `"autoEnabled": true`, the app is enabled when it's installed, so remove this section, including the steps and their snippet markers. Otherwise keep the steps above and remove only this brief.
36
40
  -->
37
41
  {{#if hasConfigPage}}
38
42
 
@@ -56,7 +60,7 @@ Fill this in: List the jobs someone lands here to do, phrased as tasks rather th
56
60
  - [Change the app settings](./configure.md)
57
61
  {{/if}}
58
62
  {{#if defaultFeaturePage}}
59
- - [Find your way around {{{defaultFeaturePage.name}}}](./{{defaultFeaturePage.file}}.md)
63
+ - [Find your way around {{{defaultFeaturePage.title}}}](./{{defaultFeaturePage.file}}.md)
60
64
  {{/if}}
61
65
  {{#if hasDashboards}}
62
66
  - [Import the dashboards that ship with the app](./dashboards.md)
@@ -19,7 +19,7 @@ Fill this in: A table with one row per entry in `roles` in `src/plugin.json`:
19
19
  | Role | Description | Granted by default to |
20
20
  | ---- | ----------- | --------------------- |
21
21
 
22
- Use the role's display name, or its name if it has none, and take the defaults from `grants`. After the table, say what each role lets a user do in the app, in terms of its pages and actions rather than the raw permission names.
22
+ Use the role's display name, or its name if it has none, and take the defaults from `grants`. After the table, say what each role lets a user do in the app, based on the role's permissions and the `role` and `action` gates on `includes` entries in `src/plugin.json`, together with any checks in the app's code, not on the role's name. Describe access in terms of pages and actions rather than raw permission names.
23
23
 
24
24
  If `src/plugin.json` has no `roles`, remove this section and Assign a role below.
25
25
  -->
@@ -32,7 +32,9 @@ Open each H2 with one sentence on when its errors occur. Add an H3 per symptom,
32
32
  For errors in the app, open the developer tools in your browser and check the console.
33
33
  {{#if hasBackend}}
34
34
 
35
- In self-managed Grafana, you can also find more detail in the Grafana server logs:
35
+ <!-- snippet:server-debug-logs -->
36
+
37
+ In self-managed Grafana, the Grafana server logs show more detail:
36
38
 
37
39
  1. Set the log level to `debug` in the Grafana configuration file:
38
40
 
@@ -44,6 +46,8 @@ In self-managed Grafana, you can also find more detail in the Grafana server log
44
46
  1. Restart Grafana and reproduce the error.
45
47
  1. Look for log lines with `logger=plugin.{{{pluginId}}}`.
46
48
  1. Set the log level back to `info` when you're done, to keep the logs small.
49
+
50
+ <!-- /snippet:server-debug-logs -->
47
51
  {{/if}}
48
52
 
49
53
  ## Get additional help
@@ -1,14 +1,16 @@
1
1
  ---
2
- title: "{{{yamlEscape page.name}}}"
3
- description: "Use the {{{yamlEscape page.name}}} page in {{{yamlEscape pluginName}}}."
2
+ title: "{{{yamlEscape page.title}}}"
3
+ description: "{{#if page.isOnlyPage}}Use {{{yamlEscape pluginName}}} in Grafana.{{else}}Use the {{{yamlEscape page.title}}} page in {{{yamlEscape pluginName}}}.{{/if}}"
4
4
  keywords:
5
5
  - "{{{yamlEscape pluginName}}}"
6
- - "{{{yamlEscape page.name}}}"
6
+ {{#unless page.isOnlyPage}}
7
+ - "{{{yamlEscape page.title}}}"
8
+ {{/unless}}
7
9
  sidebar_position: {{page.position}}
8
10
  ---
9
11
 
10
12
  <!-- section-brief
11
- Fill this in: One or two sentences on what the {{{page.name}}} page is for and when someone opens it. The app shows it at `{{{page.path}}}`. Pair it with a screenshot of the page if `info.screenshots` in `src/plugin.json` has one.
13
+ Fill this in: One or two sentences on what {{#if page.isOnlyPage}}{{{pluginName}}} shows{{else}}the {{{page.title}}} page is for{{/if}} and when someone opens it. The app shows it at `{{{page.path}}}`, which its navigation calls {{{page.name}}}. Pair it with a screenshot of the page if `info.screenshots` in `src/plugin.json` has one.
12
14
  -->
13
15
  {{#if page.role}}
14
16
 
@@ -22,5 +24,5 @@ Fill this in: Keep the bullet above. Add a bullet in the same **Label:** form on
22
24
  {{/if}}
23
25
 
24
26
  <!-- section-brief
25
- Fill this in: Replace this brief with one H2 per task someone does on the {{{page.name}}} page, named after the task, such as "Filter by service" or "Compare two time ranges". Under each, give the steps with the UI labels in bold, and say what each part of the page shows when it helps the task. Find the tasks in the component that renders `{{{page.path}}}`, including its tabs and drilldowns, which `src/plugin.json` doesn't list. Describe UI that only appears behind a feature flag or a condition as "when X is enabled".
27
+ Fill this in: Replace this brief with one H2 per task someone does {{#if page.isOnlyPage}}in {{{pluginName}}}{{else}}on the {{{page.title}}} page{{/if}}, named after the task, such as "Filter by service" or "Compare two time ranges". Under each, give the steps with the UI labels in bold, and say what each part of the page shows when it helps the task. Find the tasks in the component that renders `{{{page.path}}}`, including its tabs and drilldowns, which `src/plugin.json` doesn't list. Describe UI that only appears behind a feature flag or a condition as "when X is enabled".
26
28
  -->
@@ -102,6 +102,8 @@ To fill a section:
102
102
  Briefs are not shown in the preview or the catalog, so a section you haven't written looks like a bare
103
103
  heading. `docs:validate` lists every section that still has only its brief.
104
104
 
105
+ Text between `<!-- snippet:... -->` and `<!-- /snippet:... -->` markers is maintained by Grafana. Never edit inside it - refer to `snippets.md` in the guides.
106
+
105
107
  ## Validate
106
108
 
107
109
  ```bash
@@ -24,13 +24,15 @@ Fill this in: Keep the list above. Add a bullet in the same **Label:** form for
24
24
  ## Set up access in {{{pluginName}}}
25
25
 
26
26
  <!-- section-brief
27
- Fill this in: Steps the reader follows in the service {{{pluginName}}} connects to before adding the data source, such as creating a user, an API token or a cloud role. Take what's needed from the credential fields in the config editor, and work out the least access that works from what the backend calls, such as API endpoints, SQL statements or settings it changes. Say what fails if a permission is missing. Change the heading if necessary to name the task, such as "Create a read-only user". Leave the Grafana fields to Configure settings below.
27
+ Fill this in: Steps the reader follows in the service {{{pluginName}}} connects to before adding the data source, such as creating a user, an API token or a cloud role. Take what's needed from the credential fields in the config editor, and work out the least access that works from what the backend calls, such as API endpoints, SQL statements or settings it changes. Say what fails if a permission is missing. Take each permission from what the source calls, not from general knowledge of the service. Change the heading if necessary to name the task, such as "Create a read-only user". Leave the Grafana fields to Configure settings below.
28
28
 
29
29
  If the reader only needs credentials they already have, remove this section and keep them as a bullet in Before you begin.
30
30
  -->
31
31
 
32
32
  ## Add the data source
33
33
 
34
+ <!-- snippet:add-data-source -->
35
+
34
36
  To add the {{{pluginName}}} data source:
35
37
 
36
38
  1. Click **Connections** in the left-side menu.
@@ -39,6 +41,8 @@ To add the {{{pluginName}}} data source:
39
41
  1. Select **{{{pluginName}}}**.
40
42
  1. Click **Add new data source**.
41
43
 
44
+ <!-- /snippet:add-data-source -->
45
+
42
46
  ## Configure settings
43
47
 
44
48
  <!-- section-brief
@@ -47,7 +51,7 @@ Fill this in: An H3 for each section of the config editor, named and ordered as
47
51
  | Setting | Description | Required |
48
52
  | ------- | ----------- | -------- |
49
53
 
50
- Start the first table with **Name** and **Default**. Use the field labels from the config editor, in bold. Find the settings in the config editor component and the options type, including `jsonData` and `secureJsonData`.
54
+ Start the first table with **Name** and **Default**. Use the field labels from the config editor, in bold. Find the settings in the config editor component and the options type, including `jsonData` and `secureJsonData`. When a field's default matches none of the values it offers, such as a different case, give what the editor actually does with it.
51
55
 
52
56
  In a description, say when a field only appears for some choice, such as a protocol, and when it affects another setting, such as a port that must change when TLS is turned on. If source shows the editor lacks an option readers expect, such as TLS settings, say so and what to do instead. For each authentication method, say when to use it. Use a `> [!NOTE]` callout for a method or setting that's only available in Grafana Cloud or Grafana Enterprise.
53
57
 
@@ -12,6 +12,8 @@ sidebar_position: 7
12
12
 
13
13
  ## Import a dashboard
14
14
 
15
+ <!-- snippet:import-dashboard -->
16
+
15
17
  To import a dashboard that ships with {{{pluginName}}}:
16
18
 
17
19
  1. Click **Connections** > **Data sources** in the left-side menu.
@@ -21,6 +23,8 @@ To import a dashboard that ships with {{{pluginName}}}:
21
23
 
22
24
  Imported dashboards don't update when you upgrade the plugin. When a plugin upgrade ships a newer version of a dashboard, click **Update** next to it. This overwrites any changes you made to it.
23
25
 
26
+ <!-- /snippet:import-dashboard -->
27
+
24
28
  ## Included dashboards
25
29
 
26
30
  <!-- section-brief
@@ -40,7 +40,9 @@ Open each H2 with one sentence on when its errors occur. Add an H3 per symptom,
40
40
  To see the raw request and response for a panel, open the panel editor and click **Query inspector**.
41
41
  {{#if hasBackend}}
42
42
 
43
- In self-managed Grafana, you can also find more detail in the Grafana server logs:
43
+ <!-- snippet:server-debug-logs -->
44
+
45
+ In self-managed Grafana, the Grafana server logs show more detail:
44
46
 
45
47
  1. Set the log level to `debug` in the Grafana configuration file:
46
48
 
@@ -52,6 +54,8 @@ In self-managed Grafana, you can also find more detail in the Grafana server log
52
54
  1. Restart Grafana and reproduce the error.
53
55
  1. Look for log lines with `logger=plugin.{{{pluginId}}}`.
54
56
  1. Set the log level back to `info` when you're done, to keep the logs small.
57
+
58
+ <!-- /snippet:server-debug-logs -->
55
59
  {{/if}}
56
60
 
57
61
  ## Get additional help
@@ -92,6 +92,8 @@ To fill a section:
92
92
  Briefs are not shown in the preview or the catalog, so a section you haven't written looks like a bare
93
93
  heading. `docs:validate` lists every section that still has only its brief.
94
94
 
95
+ Text between `<!-- snippet:... -->` and `<!-- /snippet:... -->` markers is maintained by Grafana. Never edit inside it - refer to `snippets.md` in the guides.
96
+
95
97
  ## Validate
96
98
 
97
99
  ```bash
@@ -24,4 +24,8 @@ To see the data the panel receives, open the panel editor, click **Query inspect
24
24
 
25
25
  ## Reshape data that doesn't match
26
26
 
27
+ <!-- snippet:reshape-with-transformations -->
28
+
27
29
  If a query returns data in a different shape, you can reshape it without changing the query. Select the **Transform data** tab in the panel editor and add a transformation. For more information, refer to [Transform data](https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/).
30
+
31
+ <!-- /snippet:reshape-with-transformations -->