Update plugin generator to generate NP plugins (#55281)

* Generate NP plugin

* Added tsconfig

* tsconfig

* Adjust sao test

* Add server side to plugin gen

* Added navigation

* add empty element

* eslint

* platform team CR

* design CR improvements

* text updates

* temp disable plugin gen tests

* eslint

* Code review fixes

* Add scss support - requires #53976 to be merged to work

* CR fixes

* comment fixes

* Don't generate eslint for internal plugins by default

* Update tests

* reenable jest test for sao

* Fix regex

* review comments

* code review

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Liza Katz 2020-02-03 19:29:59 +02:00 committed by GitHub
parent 4f4d3d753c
commit 479223b0a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 412 additions and 615 deletions

View file

@ -29,6 +29,7 @@ exports.run = function run(argv) {
const options = getopts(argv, {
alias: {
h: 'help',
i: 'internal',
},
});
@ -40,17 +41,22 @@ exports.run = function run(argv) {
if (options.help) {
console.log(
dedent(chalk`
{dim usage:} node scripts/generate-plugin {bold [name]}
generate a fresh Kibana plugin in the plugins/ directory
# {dim Usage:}
node scripts/generate-plugin {bold [name]}
Generate a fresh Kibana plugin in the plugins/ directory
# {dim Core Kibana plugins:}
node scripts/generate-plugin {bold [name]} -i
To generate a core Kibana plugin inside the src/plugins/ directory, add the -i flag.
`) + '\n'
);
process.exit(1);
}
const name = options._[0];
const isKibanaPlugin = options.internal;
const template = resolve(__dirname, './sao_template');
const kibanaPlugins = resolve(__dirname, '../../plugins');
const kibanaPlugins = resolve(__dirname, isKibanaPlugin ? '../../src/plugins' : '../../plugins');
const targetPath = resolve(kibanaPlugins, snakeCase(name));
sao({
@ -58,6 +64,8 @@ exports.run = function run(argv) {
targetPath: targetPath,
configOptions: {
name,
isKibanaPlugin,
targetPath,
},
}).catch(error => {
console.error(chalk`{red fatal error}!`);

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
interface PluginGenerator {
/**
* Run plugin generator.
*/
run: (...args: any[]) => any;
}

View file

@ -61,7 +61,8 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug
expect(stats.isDirectory()).toBe(true);
});
it(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => {
// skipped until internationalization is re-introduced
it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => {
// Link to the error that happens when the blank line is not there:
// https://github.com/elastic/kibana/pull/45044#issuecomment-530092627
const intlFile = `${generatedPath}/.i18nrc.json`;
@ -78,16 +79,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug
});
});
it(`'yarn test:server' should exit 0`, async () => {
await execa('yarn', ['test:server'], {
cwd: generatedPath,
env: {
DISABLE_JUNIT_REPORTER: '1',
},
});
});
it(`'yarn build' should exit 0`, async () => {
it.skip(`'yarn build' should exit 0`, async () => {
await execa('yarn', ['build'], { cwd: generatedPath });
});
@ -109,7 +101,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug
'--migrations.skip=true',
],
cwd: generatedPath,
wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/,
wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'),
});
await proc.stop('kibana');
});
@ -120,7 +112,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug
await execa('yarn', ['preinstall'], { cwd: generatedPath });
});
it(`'yarn lint' should exit 0`, async () => {
it.skip(`'yarn lint' should exit 0`, async () => {
await execa('yarn', ['lint'], { cwd: generatedPath });
});

View file

@ -17,21 +17,19 @@
* under the License.
*/
const { resolve, relative, dirname } = require('path');
const { relative } = require('path');
const startCase = require('lodash.startcase');
const camelCase = require('lodash.camelcase');
const snakeCase = require('lodash.snakecase');
const execa = require('execa');
const chalk = require('chalk');
const execa = require('execa');
const pkg = require('../package.json');
const kibanaPkgPath = require.resolve('../../../package.json');
const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require
const KBN_DIR = dirname(kibanaPkgPath);
module.exports = function({ name }) {
module.exports = function({ name, targetPath, isKibanaPlugin }) {
return {
prompts: {
description: {
@ -47,41 +45,38 @@ module.exports = function({ name }) {
message: 'Should an app component be generated?',
default: true,
},
generateTranslations: {
type: 'confirm',
message: 'Should translation files be generated?',
default: true,
},
generateHack: {
type: 'confirm',
message: 'Should a hack component be generated?',
default: true,
},
generateApi: {
type: 'confirm',
message: 'Should a server API be generated?',
default: true,
},
// generateTranslations: {
// type: 'confirm',
// message: 'Should translation files be generated?',
// default: true,
// },
generateScss: {
type: 'confirm',
message: 'Should SCSS be used?',
when: answers => answers.generateApp,
default: true,
},
generateEslint: {
type: 'confirm',
message: 'Would you like to use a custom eslint file?',
default: !isKibanaPlugin,
},
},
filters: {
'public/**/index.scss': 'generateScss',
'public/**/*': 'generateApp',
'translations/**/*': 'generateTranslations',
'.i18nrc.json': 'generateTranslations',
'public/hack.js': 'generateHack',
'server/**/*': 'generateApi',
'public/app.scss': 'generateScss',
'.kibana-plugin-helpers.json': 'generateScss',
// 'translations/**/*': 'generateTranslations',
// '.i18nrc.json': 'generateTranslations',
'eslintrc.js': 'generateEslint',
},
move: {
gitignore: '.gitignore',
'eslintrc.js': '.eslintrc.js',
'package_template.json': 'package.json',
},
data: answers =>
Object.assign(
@ -91,34 +86,36 @@ module.exports = function({ name }) {
camelCase,
snakeCase,
name,
isKibanaPlugin,
kbnVersion: answers.kbnVersion,
upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1),
hasUi: !!answers.generateApp,
hasServer: !!answers.generateApi,
hasScss: !!answers.generateScss,
relRoot: isKibanaPlugin ? '../../../..' : '../../..',
},
answers
),
enforceNewFolder: true,
installDependencies: false,
gitInit: true,
gitInit: !isKibanaPlugin,
async post({ log }) {
await execa('yarn', ['kbn', 'bootstrap'], {
cwd: KBN_DIR,
stdio: 'inherit',
});
const dir = relative(process.cwd(), resolve(KBN_DIR, 'plugins', snakeCase(name)));
const dir = relative(process.cwd(), targetPath);
// Apply eslint to the generated plugin
try {
await execa('yarn', ['lint', '--fix'], {
cwd: dir,
all: true,
});
await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']);
} catch (error) {
throw new Error(`Failure when running prettier on the generated output: ${error.all}`);
console.error(error);
throw new Error(
`Failure when running prettier on the generated output: ${error.all || error}`
);
}
log.success(chalk`🎉
Your plugin has been created in {bold ${dir}}. Move into that directory to run it:
Your plugin has been created in {bold ${dir}}.
{bold cd "${dir}"}
{bold yarn start}
`);
},

View file

@ -19,8 +19,6 @@
const sao = require('sao');
const templatePkg = require('../package.json');
const template = {
fromPath: __dirname,
configOptions: {
@ -32,121 +30,57 @@ function getFileContents(file) {
return file.contents.toString();
}
function getConfig(file) {
const contents = getFileContents(file).replace(/\r?\n/gm, '');
return contents.split('kibana.Plugin(')[1];
}
describe('plugin generator sao integration', () => {
test('skips files when answering no', async () => {
const res = await sao.mockPrompt(template, {
generateApp: false,
generateHack: false,
generateApi: false,
});
expect(res.fileList).not.toContain('public/app.js');
expect(res.fileList).not.toContain('public/__tests__/index.js');
expect(res.fileList).not.toContain('public/hack.js');
expect(res.fileList).not.toContain('server/routes/example.js');
expect(res.fileList).not.toContain('server/__tests__/index.js');
const uiExports = getConfig(res.files['index.js']);
expect(uiExports).not.toContain('app:');
expect(uiExports).not.toContain('hacks:');
expect(uiExports).not.toContain('init(server, options)');
expect(uiExports).not.toContain('registerFeature(');
expect(res.fileList).toContain('common/index.ts');
expect(res.fileList).not.toContain('public/index.ts');
expect(res.fileList).not.toContain('server/index.ts');
});
it('includes app when answering yes', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: false,
generateApi: false,
});
// check output files
expect(res.fileList).toContain('public/app.js');
expect(res.fileList).toContain('public/__tests__/index.js');
expect(res.fileList).not.toContain('public/hack.js');
expect(res.fileList).not.toContain('server/routes/example.js');
expect(res.fileList).not.toContain('server/__tests__/index.js');
const uiExports = getConfig(res.files['index.js']);
expect(uiExports).toContain('app:');
expect(uiExports).toContain('init(server, options)');
expect(uiExports).toContain('registerFeature(');
expect(uiExports).not.toContain('hacks:');
});
it('includes hack when answering yes', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: true,
generateApi: false,
});
// check output files
expect(res.fileList).toContain('public/app.js');
expect(res.fileList).toContain('public/__tests__/index.js');
expect(res.fileList).toContain('public/hack.js');
expect(res.fileList).not.toContain('server/routes/example.js');
expect(res.fileList).not.toContain('server/__tests__/index.js');
const uiExports = getConfig(res.files['index.js']);
expect(uiExports).toContain('app:');
expect(uiExports).toContain('hacks:');
expect(uiExports).toContain('init(server, options)');
expect(uiExports).toContain('registerFeature(');
expect(res.fileList).toContain('common/index.ts');
expect(res.fileList).toContain('public/index.ts');
expect(res.fileList).toContain('public/plugin.ts');
expect(res.fileList).toContain('public/types.ts');
expect(res.fileList).toContain('public/components/app.tsx');
expect(res.fileList).not.toContain('server/index.ts');
});
it('includes server api when answering yes', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: true,
generateApi: true,
});
// check output files
expect(res.fileList).toContain('public/app.js');
expect(res.fileList).toContain('public/__tests__/index.js');
expect(res.fileList).toContain('public/hack.js');
expect(res.fileList).toContain('server/routes/example.js');
expect(res.fileList).toContain('server/__tests__/index.js');
const uiExports = getConfig(res.files['index.js']);
expect(uiExports).toContain('app:');
expect(uiExports).toContain('hacks:');
expect(uiExports).toContain('init(server, options)');
expect(uiExports).toContain('registerFeature(');
expect(res.fileList).toContain('public/plugin.ts');
expect(res.fileList).toContain('server/plugin.ts');
expect(res.fileList).toContain('server/index.ts');
expect(res.fileList).toContain('server/types.ts');
expect(res.fileList).toContain('server/routes/index.ts');
});
it('plugin config has correct name and main path', async () => {
it('plugin package has correct title', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: true,
generateApi: true,
});
const indexContents = getFileContents(res.files['index.js']);
const nameLine = indexContents.match('name: (.*)')[1];
const mainLine = indexContents.match('main: (.*)')[1];
const contents = getFileContents(res.files['common/index.ts']);
const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1];
expect(nameLine).toContain('some_fancy_plugin');
expect(mainLine).toContain('plugins/some_fancy_plugin/app');
});
it('plugin package has correct name', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: true,
generateApi: true,
});
const packageContents = getFileContents(res.files['package.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.name).toBe('some_fancy_plugin');
expect(controllerLine).toContain('Some fancy plugin');
});
it('package has version "kibana" with master', async () => {
@ -154,10 +88,10 @@ describe('plugin generator sao integration', () => {
kbnVersion: 'master',
});
const packageContents = getFileContents(res.files['package.json']);
const packageContents = getFileContents(res.files['kibana.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.kibana.version).toBe('kibana');
expect(pkg.version).toBe('master');
});
it('package has correct version', async () => {
@ -165,39 +99,26 @@ describe('plugin generator sao integration', () => {
kbnVersion: 'v6.0.0',
});
const packageContents = getFileContents(res.files['package.json']);
const packageContents = getFileContents(res.files['kibana.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.kibana.version).toBe('v6.0.0');
});
it('package has correct templateVersion', async () => {
const res = await sao.mockPrompt(template, {
kbnVersion: 'master',
});
const packageContents = getFileContents(res.files['package.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.kibana.templateVersion).toBe(templatePkg.version);
expect(pkg.version).toBe('v6.0.0');
});
it('sample app has correct values', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateHack: true,
generateApi: true,
});
const contents = getFileContents(res.files['public/app.js']);
const controllerLine = contents.match('setRootController(.*)')[1];
const contents = getFileContents(res.files['common/index.ts']);
const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1];
expect(controllerLine).toContain('someFancyPlugin');
});
it('includes dotfiles', async () => {
const res = await sao.mockPrompt(template);
expect(res.files['.gitignore']).toBeTruthy();
expect(res.files['.eslintrc.js']).toBeTruthy();
});
});

View file

@ -1,9 +0,0 @@
{
"paths": {
"<%= camelCase(name) %>": "./"
},
"translations": [
"translations/zh-CN.json"
]
}

View file

@ -1,3 +0,0 @@
{
"styleSheetToCompile": "public/app.scss"
}

View file

@ -6,34 +6,7 @@
---
## development
## Development
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following yarn scripts.
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment.
- `yarn kbn bootstrap`
Install dependencies and crosslink Kibana and all projects/plugins.
> ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change.
- `yarn start`
Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana`
```
yarn start --elasticsearch.hosts http://localhost:9220
```
- `yarn build`
Build a distributable archive of your plugin.
- `yarn test:browser`
Run the browser tests in a real web browser.
- `yarn test:mocha`
Run the server tests using mocha.
For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks checkout the `package.json` file, or run `yarn run`.

View file

@ -0,0 +1,2 @@
export const PLUGIN_ID = '<%= camelCase(name) %>';
export const PLUGIN_NAME = '<%= name %>';

View file

@ -1,24 +1,9 @@
module.exports = {
root: true,
module.exports = {
root: true,
extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'],
settings: {
'import/resolver': {
'@kbn/eslint-import-resolver-kibana': {
rootPackageName: '<%= snakeCase(name) %>',
},
},
},
overrides: [
{
files: ['**/public/**/*'],
settings: {
'import/resolver': {
'@kbn/eslint-import-resolver-kibana': {
forceNode: false,
rootPackageName: '<%= snakeCase(name) %>',
},
},
},
},
]
};
<%_ if (!isKibanaPlugin) { -%>
rules: {
"@kbn/eslint/require-license-header": "off"
}
<%_ } -%>
};

View file

@ -1,6 +0,0 @@
npm-debug.log*
node_modules
/build/
<%_ if (generateScss) { -%>
/public/app.css
<%_ } -%>

View file

@ -1,89 +0,0 @@
<% if (generateScss) { -%>
import { resolve } from 'path';
import { existsSync } from 'fs';
<% } -%>
<% if (generateApp) { -%>
import { i18n } from '@kbn/i18n';
<% } -%>
<% if (generateApi) { -%>
import exampleRoute from './server/routes/example';
<% } -%>
export default function (kibana) {
return new kibana.Plugin({
require: ['elasticsearch'],
name: '<%= snakeCase(name) %>',
uiExports: {
<%_ if (generateApp) { -%>
app: {
title: '<%= startCase(name) %>',
description: '<%= description %>',
main: 'plugins/<%= snakeCase(name) %>/app',
},
<%_ } -%>
<%_ if (generateHack) { -%>
hacks: [
'plugins/<%= snakeCase(name) %>/hack'
],
<%_ } -%>
<%_ if (generateScss) { -%>
styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)),
<%_ } -%>
},
config(Joi) {
return Joi.object({
enabled: Joi.boolean().default(true),
}).default();
},
<%_ if (generateApi || generateApp) { -%>
// eslint-disable-next-line no-unused-vars
init(server, options) {
<%_ if (generateApp) { -%>
const xpackMainPlugin = server.plugins.xpack_main;
if (xpackMainPlugin) {
const featureId = '<%= snakeCase(name) %>';
xpackMainPlugin.registerFeature({
id: featureId,
name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', {
defaultMessage: '<%= name %>',
}),
navLinkId: featureId,
icon: 'questionInCircle',
app: [featureId, 'kibana'],
catalogue: [],
privileges: {
all: {
api: [],
savedObject: {
all: [],
read: [],
},
ui: ['show'],
},
read: {
api: [],
savedObject: {
all: [],
read: [],
},
ui: ['show'],
},
},
});
}
<%_ } -%>
<%_ if (generateApi) { -%>
// Add server routes and initialize the plugin here
exampleRoute(server);
<%_ } -%>
}
<%_ } -%>
});
}

View file

@ -0,0 +1,8 @@
{
"id": "<%= camelCase(name) %>",
"version": "<%= kbnVersion %>",
"server": <%= hasServer %>,
"ui": <%= hasUi %>,
"requiredPlugins": ["navigation"],
"optionalPlugins": []
}

View file

@ -1,41 +0,0 @@
{
"name": "<%= snakeCase(name) %>",
"version": "0.0.0",
"description": "<%= description %>",
"main": "index.js",
"kibana": {
"version": "<%= (kbnVersion === 'master') ? 'kibana' : kbnVersion %>",
"templateVersion": "<%= templateVersion %>"
},
"scripts": {
"preinstall": "node ../../preinstall_check",
"kbn": "node ../../scripts/kbn",
"es": "node ../../scripts/es",
"lint": "eslint .",
"start": "plugin-helpers start",
"test:server": "plugin-helpers test:server",
"test:browser": "plugin-helpers test:browser",
"build": "plugin-helpers build"
},
<%_ if (generateTranslations) { _%>
"dependencies": {
"@kbn/i18n": "link:../../packages/kbn-i18n"
},
<%_ } _%>
"devDependencies": {
"@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana",
"@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana",
"@kbn/expect": "link:../../packages/kbn-expect",
"@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jest": "^22.4.1",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-no-unsanitized": "^3.0.2",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-react": "^7.12.4"
}
}

View file

@ -1,7 +0,0 @@
import expect from '@kbn/expect';
describe('suite', () => {
it('is a test', () => {
expect(true).to.equal(true);
});
});

View file

@ -1,45 +0,0 @@
import React from 'react';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { render, unmountComponentAtNode } from 'react-dom';
<%_ if (generateTranslations) { _%>
import { I18nProvider } from '@kbn/i18n/react';
<%_ } _%>
import { Main } from './components/main';
const app = uiModules.get('apps/<%= camelCase(name) %>');
app.config($locationProvider => {
$locationProvider.html5Mode({
enabled: false,
requireBase: false,
rewriteLinks: false,
});
});
app.config(stateManagementConfigProvider =>
stateManagementConfigProvider.disable()
);
function RootController($scope, $element, $http) {
const domNode = $element[0];
// render react to DOM
<%_ if (generateTranslations) { _%>
render(
<I18nProvider>
<Main title="<%= name %>" httpClient={$http} />
</I18nProvider>,
domNode
);
<%_ } else { _%>
render(<Main title="<%= name %>" httpClient={$http} />, domNode);
<%_ } _%>
// unmount react on controller destroy
$scope.$on('$destroy', () => {
unmountComponentAtNode(domNode);
});
}
chrome.setRootController('<%= camelCase(name) %>', RootController);

View file

@ -0,0 +1,25 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public';
import { AppPluginStartDependencies } from './types';
import { <%= upperCamelCaseName %>App } from './components/app';
export const renderApp = (
{ notifications, http }: CoreStart,
{ navigation }: AppPluginStartDependencies,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<<%= upperCamelCaseName %>App
basename={appBasePath}
notifications={notifications}
http={http}
navigation={navigation}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,129 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
EuiButton,
EuiHorizontalRule,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageHeader,
EuiTitle,
EuiText,
} from '@elastic/eui';
import { CoreStart } from '<%= relRoot %>/../src/core/public';
import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public';
import { PLUGIN_ID, PLUGIN_NAME } from '../../common';
interface <%= upperCamelCaseName %>AppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
navigation: NavigationPublicPluginStart;
}
export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => {
// Use React hooks to manage state.
const [timestamp, setTimestamp] = useState<string | undefined>();
const onClickHandler = () => {
<%_ if (generateApi) { -%>
// Use the core http service to make a response to the server API.
http.get('/api/<%= snakeCase(name) %>/example').then(res => {
setTimestamp(res.time);
// Use the core notifications service to display a success message.
notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', {
defaultMessage: 'Data updated',
}));
});
<%_ } else { -%>
setTimestamp(new Date().toISOString());
notifications.toasts.addSuccess(PLUGIN_NAME);
<%_ } -%>
};
// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
return (
<Router basename={basename}>
<I18nProvider>
<>
<navigation.ui.TopNavMenu appName={ PLUGIN_ID } showSearchBar={true} />
<EuiPage restrictWidth="1000px">
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>
<FormattedMessage
id="<%= camelCase(name) %>.helloWorldText"
defaultMessage="{name}"
values={{ name: PLUGIN_NAME }}
/>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="<%= camelCase(name) %>.congratulationsTitle"
defaultMessage="Congratulations, you have successfully created a new Kibana Plugin!"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<p>
<FormattedMessage
id="<%= camelCase(name) %>.content"
defaultMessage="Look through the generated code and check out the plugin development documentation."
/>
</p>
<EuiHorizontalRule/>
<p>
<FormattedMessage
id="<%= camelCase(name) %>.timestampText"
defaultMessage="Last timestamp: {time}"
values={{ time: timestamp ? timestamp : 'Unknown' }}
/>
</p>
<EuiButton type="primary" size="s" onClick={onClickHandler}>
<FormattedMessage id="<%= camelCase(name) %>.buttonText" defaultMessage="<%_ if (generateApi) { -%>Get data<%_ } else { -%>Click me<%_ } -%>" />
</EuiButton>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</>
</I18nProvider>
</Router>
);
};

View file

@ -1 +0,0 @@
export { Main } from './main';

View file

@ -1,97 +0,0 @@
import React from 'react';
import {
EuiPage,
EuiPageHeader,
EuiTitle,
EuiPageBody,
EuiPageContent,
EuiPageContentHeader,
EuiPageContentBody,
EuiText
} from '@elastic/eui';
<%_ if (generateTranslations) { _%>
import { FormattedMessage } from '@kbn/i18n/react';
<%_ } _%>
export class Main extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
/*
FOR EXAMPLE PURPOSES ONLY. There are much better ways to
manage state and update your UI than this.
*/
const { httpClient } = this.props;
httpClient.get('../api/<%= name %>/example').then((resp) => {
this.setState({ time: resp.data.time });
});
}
render() {
const { title } = this.props;
return (
<EuiPage>
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.helloWorldText"
defaultMessage="{title} Hello World!"
values={{ title }}
/>
<%_ } else { _%>
{title} Hello World!
<%_ } _%>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.congratulationsTitle"
defaultMessage="Congratulations"
/>
<%_ } else { _%>
Congratulations
<%_ } _%>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<h3>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.congratulationsText"
defaultMessage="You have successfully created your first Kibana Plugin!"
/>
<%_ } else { _%>
You have successfully created your first Kibana Plugin!
<%_ } _%>
</h3>
<p>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.serverTimeText"
defaultMessage="The server time (via API call) is {time}"
values={{ time: this.state.time || 'NO API CALL YET' }}
/>
<%_ } else { _%>
The server time (via API call) is {this.state.time || 'NO API CALL YET'}
<%_ } _%>
</p>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}

View file

@ -1,7 +0,0 @@
import $ from 'jquery';
$(document.body).on('keypress', function (event) {
if (event.which === 58) {
alert('boo!');
}
});

View file

@ -0,0 +1,16 @@
<%_ if (hasScss) { -%>
import './index.scss';
<%_ } -%>
import { <%= upperCamelCaseName %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new <%= upperCamelCaseName %>Plugin();
}
export {
<%= upperCamelCaseName %>PluginSetup,
<%= upperCamelCaseName %>PluginStart,
} from './types';

View file

@ -0,0 +1,42 @@
import { i18n } from '@kbn/i18n';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public';
import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types';
import { PLUGIN_NAME } from '../common';
export class <%= upperCamelCaseName %>Plugin
implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> {
public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup {
// Register an application into the side navigation menu
core.application.register({
id: '<%= camelCase(name) %>',
title: PLUGIN_NAME,
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in kibana.json
const [coreStart, depsStart] = await core.getStartServices();
// Render the application
return renderApp(coreStart, depsStart as AppPluginStartDependencies, params);
},
});
// Return methods that should be available to other plugins
return {
getGreeting() {
return i18n.translate('<%= camelCase(name) %>.greetingText', {
defaultMessage: 'Hello from {name}!',
values: {
name: PLUGIN_NAME,
},
});
},
};
}
public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,11 @@
import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public';
export interface <%= upperCamelCaseName %>PluginSetup {
getGreeting: () => string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginStart {}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart
};

View file

@ -1,7 +0,0 @@
import expect from '@kbn/expect';
describe('suite', () => {
it('is a test', () => {
expect(true).to.equal(true);
});
});

View file

@ -0,0 +1,15 @@
import { PluginInitializerContext } from '<%= relRoot %>/src/core/server';
import { <%= upperCamelCaseName %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin(initializerContext: PluginInitializerContext) {
return new <%= upperCamelCaseName %>Plugin(initializerContext);
}
export {
<%= upperCamelCaseName %>PluginSetup,
<%= upperCamelCaseName %>PluginStart,
} from './types';

View file

@ -0,0 +1,30 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server';
import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types';
import { defineRoutes } from './routes';
export class <%= upperCamelCaseName %>Plugin
implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(core: CoreSetup) {
this.logger.debug('<%= name %>: Setup');
const router = core.http.createRouter();
// Register server side APIs
defineRoutes(router);
return {};
}
public start(core: CoreStart) {
this.logger.debug('<%= name %>: Started');
return {};
}
public stop() {}
}

View file

@ -1,11 +0,0 @@
export default function (server) {
server.route({
path: '/api/<%= name %>/example',
method: 'GET',
handler() {
return { time: (new Date()).toISOString() };
}
});
}

View file

@ -0,0 +1,17 @@
import { IRouter } from '<%= relRoot %>/../src/core/server';
export function defineRoutes(router: IRouter) {
router.get(
{
path: '/api/<%= snakeCase(name) %>/example',
validate: false,
},
async (context, request, response) => {
return response.ok({
body: {
time: new Date().toISOString(),
},
});
}
);
}

View file

@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginStart {}

View file

@ -1,84 +0,0 @@
{
"formats": {
"number": {
"currency": {
"style": "currency"
},
"percent": {
"style": "percent"
}
},
"date": {
"short": {
"month": "numeric",
"day": "numeric",
"year": "2-digit"
},
"medium": {
"month": "short",
"day": "numeric",
"year": "numeric"
},
"long": {
"month": "long",
"day": "numeric",
"year": "numeric"
},
"full": {
"weekday": "long",
"month": "long",
"day": "numeric",
"year": "numeric"
}
},
"time": {
"short": {
"hour": "numeric",
"minute": "numeric"
},
"medium": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric"
},
"long": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
},
"full": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
}
},
"relative": {
"years": {
"units": "year"
},
"months": {
"units": "month"
},
"days": {
"units": "day"
},
"hours": {
"units": "hour"
},
"minutes": {
"units": "minute"
},
"seconds": {
"units": "second"
}
}
},
"messages": {
"<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。",
"<%= camelCase(name) %>.congratulationsTitle": "恭喜!",
"<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!",
"<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}"
}
}

View file

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["**/*", "index.js.d.ts"],
"exclude": ["sao_template/template/*"]
}