@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.
- package/CHANGELOG.md +18 -0
- package/dist/codemods/additions/prompts/docs-content.md +10 -4
- package/dist/codemods/additions/scripts/docs.js +32 -8
- package/dist/codemods/additions/scripts/experimental-app-sdk.js +40 -0
- package/dist/codemods/migrations/manager.js +6 -0
- package/dist/commands/update.command.js +7 -1
- package/package.json +2 -1
- package/src/codemods/additions/prompts/docs-content.md +10 -4
- package/src/codemods/additions/scripts/docs.test.ts +137 -39
- package/src/codemods/additions/scripts/docs.ts +47 -7
- package/src/codemods/additions/scripts/experimental-app-sdk.test.ts +59 -2
- package/src/codemods/additions/scripts/experimental-app-sdk.ts +55 -0
- package/src/codemods/migrations/manager.test.ts +10 -0
- package/src/codemods/migrations/manager.ts +10 -0
- package/src/commands/update.command.ts +9 -1
- package/templates/common/.config/docker-compose-base.yaml +1 -1
- package/templates/common/_package.json +6 -6
- package/templates/docs/app/docs/AGENTS.md +4 -0
- package/templates/docs/app/docs/configure.md +5 -1
- package/templates/docs/app/docs/dashboards.md +4 -0
- package/templates/docs/app/docs/index.md +6 -2
- package/templates/docs/app/docs/permissions.md +1 -1
- package/templates/docs/app/docs/troubleshooting.md +5 -1
- package/templates/docs/app/feature-page.md +7 -5
- package/templates/docs/datasource/docs/AGENTS.md +2 -0
- package/templates/docs/datasource/docs/configure.md +6 -2
- package/templates/docs/datasource/docs/dashboards.md +4 -0
- package/templates/docs/datasource/docs/troubleshooting.md +5 -1
- package/templates/docs/panel/docs/AGENTS.md +2 -0
- package/templates/docs/panel/docs/data-formats.md +4 -0
- package/templates/docs/panel/docs/examples.md +5 -1
- 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
|
|
259
|
-
|
|
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:
|
|
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')).
|
|
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')).
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
-
"@grafana/i18n": "13.2.
|
|
82
|
-
"@grafana/runtime": "13.2.
|
|
83
|
-
"@grafana/ui": "13.2.
|
|
84
|
-
"@grafana/schema": "13.2.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
-
description: "Use the {{{yamlEscape page.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 -->
|