Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create-Plugin: Simplify Prompts #1018

Merged
merged 15 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,22 @@ jobs:
matrix:
include:
- workingDir: 'myorg-nobackend-app'
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginDescription='This is a sample app.' --pluginType='app' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginType='app' --no-hasBackend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incredibly nitty comment which you are welcome to resolve as "go away David" - but maybe cleaner if the args are in the same order as the prompts

hasBackend: false
- workingDir: 'myorg-backend-app'
cmd: create-plugin --pluginName='backend' --orgName='myorg' --pluginDescription='This is a sample backend app.' --pluginType='app' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='backend' --orgName='myorg' --pluginType='app' --hasBackend
hasBackend: true
- workingDir: 'myorg-nobackend-panel'
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginDescription='This is a sample panel.' --pluginType='panel' --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginType='panel'
hasBackend: false
- workingDir: 'myorg-nobackend-datasource'
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginDescription='This is a sample datasource.' --pluginType='datasource' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='no-backend' --orgName='myorg' --pluginType='datasource' --no-hasBackend
hasBackend: false
- workingDir: 'myorg-backend-datasource'
cmd: create-plugin --pluginName='backend' --orgName='myorg' --pluginDescription='This is a sample backend datasource.' --pluginType='datasource' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='backend' --orgName='myorg' --pluginType='datasource' --hasBackend
hasBackend: true
- workingDir: 'myorg-nobackendscenes-app'
cmd: create-plugin --pluginName='no-backend-scenes' --orgName='myorg' --pluginDescription='This is a sample scenes app.' --pluginType='scenesapp' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow
cmd: create-plugin --pluginName='no-backend-scenes' --orgName='myorg' --pluginType='scenesapp' --no-hasBackend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing scenes with backend?

hasBackend: false
steps:
- name: Setup .npmrc file for NPM registry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"backend": true,
"executable": "gpx_{{ snakeCase pluginName }}",
"info": {
"description": "{{ sentenceCase pluginDescription }}",
"description": "",
"author": {
"name": "{{ sentenceCase orgName }}"
},
Expand Down
12 changes: 6 additions & 6 deletions packages/create-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"dev-scenes-app": "nodemon --exec 'npm run generate-scenes-app'",
"dev-panel": "nodemon --exec 'npm run generate-panel'",
"dev-datasource": "nodemon --exec 'npm run generate-datasource'",
"generate-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginDescription='This is a sample app.' --pluginType='app' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-scenes-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample scenesapp' --orgName='sample-org' --pluginDescription='This is a sample scenes app.' --pluginType='scenesapp' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-app-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginDescription='This is a sample backend app.' --pluginType='app' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-panel": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample panel' --orgName='sample-org' --pluginDescription='This is a sample panel.' --pluginType='panel' --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-datasource": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginDescription='This is a sample datasource.' --pluginType='datasource' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-datasource-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginDescription='This is a sample datasource.' --pluginType='datasource' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
"generate-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginType='app' --no-hasBackend",
"generate-scenes-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample scenesapp' --orgName='sample-org' --pluginType='scenesapp' --no-hasBackend",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing scenes backend option?

"generate-app-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginType='app' --hasBackend",
"generate-panel": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample panel' --orgName='sample-org' --pluginType='panel'",
"generate-datasource": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginType='datasource' --no-hasBackend",
"generate-datasource-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginType='datasource' --hasBackend",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
"lint:fix": "npm run lint -- --fix",
"test": "vitest",
Expand Down
58 changes: 31 additions & 27 deletions packages/create-plugin/src/commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import minimist from 'minimist';
import chalk from 'chalk';
import { mkdir, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, TEMPLATE_PATHS } from '../constants.js';
import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants.js';
import { printError } from '../utils/utils.console.js';
import { directoryExists, getExportFileName, isFile } from '../utils/utils.files.js';
import { getExportPath } from '../utils/utils.path.js';
Expand All @@ -28,21 +28,31 @@ export const generate = async (argv: minimist.ParsedArgs) => {
}

const actions = getTemplateActions({ templateData, exportPath });
const { changes, failures } = await generateFiles({ actions });

const failures = await generateFiles({ actions });
const changes = [
`Scaffolded ${templateData.pluginId} ${templateData.pluginType} plugin ${
templateData.hasBackend ? '(with Go backend)' : ''
}`,
'Added basic e2e test with Playwright',
jackw marked this conversation as resolved.
Show resolved Hide resolved
`${provisioningMsg[templateData.pluginType]}`,
'Configured Development environment (Docker)',
jackw marked this conversation as resolved.
Show resolved Hide resolved
'Added default GitHub actions for CI, releases and Grafana compatibility',
];
console.log('');
changes.forEach((change) => {
console.log(`${chalk.green('✔︎ ++')} ${change.path}`);
console.log(`${chalk.green('✔︎')} ${change}`);
});

failures.forEach((failure) => {
console.log({ failure });
jackw marked this conversation as resolved.
Show resolved Hide resolved
printError(`${failure.error}`);
});

if (templateData.hasBackend) {
await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
}
await execPostScaffoldFunction(prettifyFiles, { targetPath: exportPath });

console.log('\n');
printGenerateSuccessMessage(templateData);
};

Expand Down Expand Up @@ -88,24 +98,14 @@ function getTemplateActions({ exportPath, templateData }: { exportPath: string;
[]
);

// Copy over Github workflow files (if selected)
const ciWorkflowActions = templateData.hasGithubWorkflows
? getActionsForTemplateFolder({
folderPath: TEMPLATE_PATHS.ciWorkflows,
exportPath,
templateData,
})
: [];

const isCompatibleWorkflowActions = templateData.hasGithubLevitateWorkflow
? getActionsForTemplateFolder({
folderPath: TEMPLATE_PATHS.isCompatibleWorkflow,
exportPath,
templateData,
})
: [];
// Copy over Github workflow files
const ciWorkflowActions = getActionsForTemplateFolder({
folderPath: TEMPLATE_PATHS.ciWorkflows,
exportPath,
templateData,
});

return [...pluginActions, ...ciWorkflowActions, ...isCompatibleWorkflowActions];
return [...pluginActions, ...ciWorkflowActions];
}

function getActionsForTemplateFolder({
Expand Down Expand Up @@ -141,7 +141,6 @@ function getActionsForTemplateFolder({

async function generateFiles({ actions }: { actions: any[] }) {
const failures = [];
const changes = [];
for (const action of actions) {
try {
const rootDir = path.dirname(action.path);
Expand All @@ -152,9 +151,6 @@ async function generateFiles({ actions }: { actions: any[] }) {

const rendered = renderTemplateFromFile(action.templateFile, action.data);
await writeFile(action.path, rendered);
changes.push({
path: action.path,
});
} catch (error) {
let message;
if (error instanceof Error) {
Expand All @@ -168,7 +164,7 @@ async function generateFiles({ actions }: { actions: any[] }) {
});
}
}
return { failures, changes };
return failures;
}

type AsyncFunction<T> = (...args: any[]) => Promise<T>;
Expand All @@ -180,6 +176,14 @@ async function execPostScaffoldFunction<T>(fn: AsyncFunction<T>, ...args: Parame
console.log(`${chalk.green('✔︎')} ${resultMsg}`);
}
} catch (error) {
console.log('error', error);
jackw marked this conversation as resolved.
Show resolved Hide resolved
printError(`${error}`);
}
}

const provisioningMsg = {
[PLUGIN_TYPES.app]: 'Provisioning provided for app',
[PLUGIN_TYPES.datasource]: 'Provisioning provided for data source instance',
[PLUGIN_TYPES.panel]: 'Provisioning provided for basic dashboard and TestData data source instance',
[PLUGIN_TYPES.scenes]: 'Provisioning provided for app and TestData data source instance',
jackw marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { machine } from 'node:os';
import { TemplateData } from '../../types.js';
import { displayAsMarkdown } from '../../utils/utils.console.js';
import { normalizeId } from '../../utils/utils.handlebars.js';
import { getPackageManagerFromUserAgent } from '../../utils/utils.packageManager.js';
import { TemplateData } from '../../types.js';

export function printGenerateSuccessMessage(answers: TemplateData) {
const directory = normalizeId(answers.pluginName, answers.orgName, answers.pluginType);
const { packageManagerName } = getPackageManagerFromUserAgent();

const commands = [
`- \`cd ./${directory}\``,
`- \`${packageManagerName} install\` to install frontend dependencies.`,
`- \`${packageManagerName} exec playwright install chromium\` to install e2e test dependencies.`,
`- \`${packageManagerName} run dev\` to build (and watch) the plugin frontend code.`,
...(answers.hasBackend
? [
'- `mage -v build:backend` to build the plugin backend code. Rerun this command every time you edit your backend files.',
`- ${getBackendCmd()} to build the plugin backend code. Rerun this command every time you edit your backend files.`,
]
: []),
'- `docker-compose up` to start a grafana development server.',
Expand All @@ -34,3 +36,12 @@ _Note: We strongly recommend creating a new Git repository by running \`git init

console.log(displayAsMarkdown(msg));
}

function getBackendCmd() {
const platform = machine();
if (platform === 'arm64') {
return '`mage -v build:linuxARM64`';
}

return '`mage -v build:linux`';
}
94 changes: 50 additions & 44 deletions packages/create-plugin/src/commands/generate/prompt-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ export async function promptUser(argv: minimist.ParsedArgs) {
let answers: Partial<GenerateCliArgs> = {};
const enquirer = new Enquirer();

for (const prompt of prompts) {
const { name, shouldPrompt } = prompt;
for (const p of prompts) {
const prompt = p(answers);

if (argv.hasOwnProperty(name)) {
answers = { ...answers, [name]: argv[name] };
if (argv.hasOwnProperty(prompt.name)) {
answers = { ...answers, [prompt.name]: argv[prompt.name] };
} else {
if (typeof shouldPrompt === 'function' && !shouldPrompt(answers)) {
if (typeof prompt.shouldPrompt === 'function' && !prompt.shouldPrompt(answers)) {
continue;
} else {
const result = await enquirer.prompt(prompt);
Expand All @@ -36,7 +36,7 @@ type Prompt = {
};

type Choice = {
name: string;
name?: string;
message?: string;
value?: unknown;
hint?: string;
Expand All @@ -45,58 +45,64 @@ type Choice = {
disabled?: boolean | string;
};

const prompts: Prompt[] = [
{
const prompts: Array<(answers: Partial<GenerateCliArgs>) => Prompt> = [
() => ({
name: 'pluginType',
type: 'select',
choices: [
{
message: 'App (Custom pages, UI Extensions and bundling other plugins)',
jackw marked this conversation as resolved.
Show resolved Hide resolved
value: PLUGIN_TYPES.app,
},
{
message: 'Data source (Query data from a custom source)',
jackw marked this conversation as resolved.
Show resolved Hide resolved
value: PLUGIN_TYPES.datasource,
},
{
message: 'Panel (New visualization for data or a widget)',
jackw marked this conversation as resolved.
Show resolved Hide resolved
value: PLUGIN_TYPES.panel,
},
{
message: 'App with Scenes (Create dynamic dashboards in app pages)',
jackw marked this conversation as resolved.
Show resolved Hide resolved
value: PLUGIN_TYPES.scenes,
},
],
message: 'Select a plugin type',
}),
(answers) => {
const isAppType = answers.pluginType === PLUGIN_TYPES.app || answers.pluginType === PLUGIN_TYPES.scenes;
const message = isAppType
? 'Does your plugin require a backend to support server-side functionality (calling external APIs, custom backend logic, advanced authentication, etc)?'
: 'Does your plugin require a backend to support server-side functionality (alerting, advanced authentication, public dashboards, etc)?';
jackw marked this conversation as resolved.
Show resolved Hide resolved

return {
name: 'hasBackend',
type: 'confirm',
message: message,
initial: false,
shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
};
},
() => ({
name: 'pluginName',
type: 'input',
message: 'What is going to be the name of your plugin?',
message: 'Enter a name for your plugin',
validate: (value: string) => {
if (/.+/.test(value)) {
return true;
}
return 'Plugin name is required';
},
},
{
}),
() => ({
name: 'orgName',
type: 'input',
message: 'What is the organization name of your plugin?',
message: 'Enter your organization name (usually your Grafana Cloud org)',
validate: (value: string) => {
if (/.+/.test(value)) {
return true;
}
return 'Organization name is required';
},
},
{
name: 'pluginDescription',
type: 'input',
message: 'How would you describe your plugin?',
initial: '',
},
{
name: 'pluginType',
type: 'select',
choices: [PLUGIN_TYPES.app, PLUGIN_TYPES.datasource, PLUGIN_TYPES.panel, PLUGIN_TYPES.scenes],
message: 'What type of plugin would you like?',
},
{
name: 'hasBackend',
type: 'confirm',
message: 'Do you want a backend part of your plugin?',
initial: false,
shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
},
{
name: 'hasGithubWorkflows',
type: 'confirm',
message: 'Do you want to add Github CI and Release workflows?',
initial: false,
},
{
name: 'hasGithubLevitateWorkflow',
type: 'confirm',
message: 'Do you want to add a Github workflow for automatically checking "Grafana API compatibility" on PRs?',
initial: false,
},
}),
];
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ export async function updateGoSdkAndModules(exportPath: string) {
}

try {
const version = await getLatestSdkVersion(exportPath);
await updateSdk(exportPath);
await updateGoMod(exportPath);
return `Updated Grafana go sdk to ${version} (latest)`;
} catch {
throw new Error(
'There was an error trying to update the grafana go sdk. Please run `go get github.com/grafana/grafana-plugin-sdk-go` manually in your plugin directory.'
);
}
return 'Grafana go sdk updated successfully.';
}

function updateSdk(exportPath: string): Promise<void> {
Expand Down Expand Up @@ -55,3 +56,17 @@ function updateGoMod(exportPath: string): Promise<void> {
});
});
}

function getLatestSdkVersion(exportPath: string): Promise<string> {
return new Promise(async (resolve, reject) => {
// run go list SDK_GO_MODULE@latest to get the latest version number
const command = `go list -m -json ${SDK_GO_MODULE}@latest`;
exec(command, { cwd: exportPath }, (error, stdout) => {
if (error) {
reject();
jackw marked this conversation as resolved.
Show resolved Hide resolved
}
const version = JSON.parse(stdout).Version;
resolve(version);
});
});
}
5 changes: 3 additions & 2 deletions packages/create-plugin/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ export const TEMPLATE_PATHS: Record<string, string> = {
common: path.join(TEMPLATES_DIR, 'common'),
datasource: path.join(TEMPLATES_DIR, 'datasource'),
panel: path.join(TEMPLATES_DIR, 'panel'),
ciWorkflows: path.join(TEMPLATES_DIR, 'github', 'ci'),
ciWorkflows: path.join(TEMPLATES_DIR, '.github'),
isCompatibleWorkflow: path.join(TEMPLATES_DIR, 'github', 'is-compatible'),
};

export enum PLUGIN_TYPES {
app = 'app',
panel = 'panel',
datasource = 'datasource',
secretsmanager = 'secretsmanager',
// TODO: Don't understand why this is here. Cannot create a secretsmanager or a renderer.
// secretsmanager = 'secretsmanager',
scenes = 'scenesapp',
}

Expand Down
Loading
Loading