* Update the slugs to improve google analytics drilldown tracking * more slug updates * Fix some formatting issues in building blocks * update paths Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # dev_docs/key_concepts/persistable_state.mdx # legacy_rfcs/README.md
345 lines
13 KiB
Plaintext
345 lines
13 KiB
Plaintext
---
|
||
id: kibDevAnatomyOfAPlugin
|
||
slug: /kibana-dev-docs/key-concepts/anatomy-of-a-plugin
|
||
title: Anatomy of a plugin
|
||
summary: Anatomy of a Kibana plugin.
|
||
date: 2021-08-03
|
||
tags: ['kibana', 'onboarding', 'dev']
|
||
---
|
||
|
||
Pre-reading material:
|
||
|
||
- <DocLink id="kibPlatformIntro" />
|
||
|
||
## The anatomy of a plugin
|
||
|
||
Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code,
|
||
or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly,
|
||
and you interact with Core and other plugins in the same way.
|
||
|
||
The basic file structure of a Kibana plugin named demo that has both client-side and server-side code would be:
|
||
|
||
```
|
||
plugins/
|
||
demo
|
||
kibana.json
|
||
tsconfig.json
|
||
public
|
||
index.ts
|
||
plugin.ts
|
||
server
|
||
index.ts
|
||
plugin.ts
|
||
common
|
||
index.ts
|
||
jest.config.js
|
||
```
|
||
|
||
### kibana.json
|
||
|
||
`kibana.json` is a static manifest file that is used to identify the plugin and to specify if this plugin has server-side code, browser-side code, or both:
|
||
|
||
```json
|
||
{
|
||
"id": "examplePluginId",
|
||
"version": "1.0.0",
|
||
"kibanaVersion": "7.14.0",
|
||
"server": true,
|
||
"ui": true,
|
||
"configPath": "path/to/config",
|
||
"type": "standard",
|
||
"owner": {
|
||
"name": "App Services",
|
||
"githubTeam": "kibana-app-services"
|
||
},
|
||
"description": "A description about this plugin!",
|
||
"requiredPlugins": ["data"],
|
||
"optionalPlugins": ["alerting"]
|
||
"requiredBundles": ["anotherPlugin"]
|
||
}
|
||
```
|
||
|
||
`id` - [Required] The id of your plugin can be anything, though it should be fairly unique, as every plugin in an installation needs to be unique. It must be snakeCase.
|
||
|
||
`version` - [Required] Note the version of your plugin. For internal plugins that don't specify a `kibanaVersion`, this will have to match the version of Kibana or ci will fail. Because teams won't want to be bumping this number for every release, internal plugins should set `kibanaVersion` to "kibana", and set this to anything.
|
||
|
||
`kibanaVersion` - [Optional] If your plugin is only compatible with a specific version of Kibana, put it here. Internal, first-party plugins should set this to "kibana", otherwise they will need to bump this value, or the one in `version`, every time the Kibana version is bumped. When [#61087](https://github.com/elastic/kibana/issues/61087) is fixed, we will stop requiring this field for internal plugins.
|
||
|
||
`server` - [Optional] If your plugin contains server-side code, this must be true.
|
||
|
||
`ui` - [Optional] If your plugin contains client-side code, this must be true.
|
||
|
||
`configPath` - [Optional] Every plugin might allow Kibana users to adjust configuration via kibana.yml file. If your plugin needs to read config from `kibana.yml , you should declare what property name it should have access to.
|
||
|
||
`type` - [Optional] Should be either `preboot` or `standard` which specifies the type of plugin. Default value, if unspecified, is `standard`. There are two types of plugins:
|
||
|
||
- preboot plugins are bootstrapped to prepare the environment before Kibana starts.
|
||
- standard plugins define Kibana functionality while Kibana is running.
|
||
|
||
`owner` - [Required] Help users of your plugin know who manages this plugin and how to get in touch. This is required for internal plugins. `Owner.name` should be the name of the team that manages this plugin. This should match the team that owns this code in the [CODEOWNERS](https://github.com/elastic/kibana/blob/master/.github/CODEOWNERS) file (however, this is not currently enforced). Internal teams should also use a [GitHub team alias](https://github.com/orgs/elastic/teams) for `owner.githubTeam`. While many teams can contribute to a plugin, only a single team should be the primary owner.
|
||
|
||
`description` - [Required] Give your plugin a description to help other developers understand what it does. This is required for internal plugins.
|
||
|
||
`requiredPlugins` - [Optional] If your plugin requires any other plugins to work, you must list them here by id. If any of the required plugins are disabled or not installed, then your plugin will be disabled.
|
||
|
||
`optionalPlugins` - [Optional] If your plugin has an optional dependency on other plugins, you must list them here by id. If any of the optional plugins are disabled or not installed, your plugin will still load, however that plugin's API contract will be undefined in the second parameter of the setup and start functions.
|
||
|
||
`requiredBundles` - [Required in certain situations] Don't worry about getting this right. The build optimizer will complain if any of these values are incorrect.
|
||
|
||
<DocCallOut>
|
||
You don't need to declare a dependency on a plugin if you only wish to access its types.
|
||
</DocCallOut>
|
||
|
||
### tsconfig.json
|
||
|
||
If you are developing in TypeScript (which we recommend), you will need to add a `tsconfig.json` file. Here is an example file that you would use if adding a plugin into the `examples` directory.
|
||
|
||
```json
|
||
{
|
||
"extends": "../../tsconfig.json", // Extend kibana/tsconfig.json
|
||
"compilerOptions": {
|
||
"outDir": "./target/types"
|
||
},
|
||
"include": [
|
||
"index.ts",
|
||
"../../typings/**/*",
|
||
// The following paths are optional, based on whether you have common code,
|
||
// or are building a client-side-only or server-side-only plugin.
|
||
"common/**/*.ts",
|
||
"public/**/*.ts",
|
||
"public/**/*.tsx",
|
||
"server/**/*.ts"
|
||
],
|
||
"exclude": [],
|
||
// If you import another plugin's types, point to their `tsconfig.json` file.
|
||
"references": [{ "path": "../../src/core/tsconfig.json" }]
|
||
}
|
||
```
|
||
|
||
### public/index.ts
|
||
|
||
`public/index.ts` is the entry point into the client-side code of this plugin. Everything exported from this file will be a part of the plugins <DocLink id="kibPlatformIntro" section="public-plugin-api" text="public API"/>. If the plugin only exists to export static utilities, consider using a package. Otherwise, this file must export a function named plugin, which will receive a standard set of
|
||
core capabilities as an argument. It should return an instance of its plugin class for Kibana to load.
|
||
|
||
```ts
|
||
import type { PluginInitializerContext } from 'kibana/server';
|
||
import { DemoPlugin } from './plugin';
|
||
|
||
export function plugin(initializerContext: PluginInitializerContext) {
|
||
return new DemoPlugin(initializerContext);
|
||
}
|
||
```
|
||
|
||
<DocCallOut title="Best practices for every top level index.ts file">
|
||
|
||
1. When possible, use
|
||
|
||
```
|
||
export type { AType } from '...'`
|
||
```
|
||
|
||
instead of
|
||
|
||
```
|
||
export { AType } from '...'`.
|
||
```
|
||
|
||
Using the non-`type` variation will increase the bundle size unnecessarily and may unwillingly provide access to the implementation of `AType` class.
|
||
|
||
2. Don't use `export *` in these top level index.ts files
|
||
|
||
</DocCallOut>
|
||
|
||
### public/plugin.ts
|
||
|
||
`public/plugin.ts` is the client-side plugin definition itself. Technically speaking, it does not need to be a class or even a separate file from the entry
|
||
point, but all plugins at Elastic should be consistent in this way.
|
||
|
||
```ts
|
||
import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server';
|
||
|
||
export class DemoPlugin implements Plugin {
|
||
constructor(initializerContext: PluginInitializerContext) {}
|
||
|
||
public setup(core: CoreSetup) {
|
||
// called when plugin is setting up during Kibana's startup sequence
|
||
}
|
||
|
||
public start(core: CoreStart) {
|
||
// called after all plugins are set up
|
||
}
|
||
|
||
public stop() {
|
||
// called when plugin is torn down during Kibana's shutdown sequence
|
||
}
|
||
}
|
||
```
|
||
|
||
### server/index.ts
|
||
|
||
`server/index.ts` is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point:
|
||
|
||
### server/plugin.ts
|
||
|
||
`server/plugin.ts` is the server-side plugin definition. The shape of this plugin is the same as it’s client-side counter-part:
|
||
|
||
```ts
|
||
import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server';
|
||
|
||
export class DemoPlugin implements Plugin {
|
||
constructor(initializerContext: PluginInitializerContext) {}
|
||
|
||
public setup(core: CoreSetup) {
|
||
// called when plugin is setting up during Kibana's startup sequence
|
||
}
|
||
|
||
public start(core: CoreStart) {
|
||
// called after all plugins are set up
|
||
}
|
||
|
||
public stop() {
|
||
// called when plugin is torn down during Kibana's shutdown sequence
|
||
}
|
||
}
|
||
```
|
||
|
||
Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain
|
||
considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built.
|
||
|
||
### common/index.ts
|
||
|
||
`common/index.ts` is the entry-point into code that can be used both server-side or client side.
|
||
|
||
### jest.config.js
|
||
|
||
If you are adding unit tests (which we recommend), you will need to add a `jest.config.js` file. Here is an example file that you would use if adding a plugin into the `examples` directory.
|
||
|
||
```js
|
||
module.exports = {
|
||
// Default Jest settings, defined in kbn-test package
|
||
preset: '@kbn/test',
|
||
// The root of the directory containing package.json
|
||
rootDir: '../../..',
|
||
// The directory which Jest should use to search for files in
|
||
roots: ['<rootDir>/src/plugins/demo'],
|
||
// The directory where Jest should output plugin coverage details, e.g. html report
|
||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/demo',
|
||
// A list of reporter names that Jest uses when writing coverage reports, default: ["json"]
|
||
// "text" is available in console and is good for quick check
|
||
// "html" helps to dig into specific files and fix coverage
|
||
coverageReporters: ['text', 'html'],
|
||
// An array of regexp pattern strings that matched files to include/exclude for code coverage
|
||
collectCoverageFrom: ['<rootDir>/src/plugins/demo/{common,public,server}/**/*.{ts,tsx}'],
|
||
};
|
||
```
|
||
|
||
## How plugin's interact with each other, and Core
|
||
|
||
The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin.
|
||
For example, the core http service exposes a function createRouter to all plugin setup functions. To use this function to register an HTTP route handler,
|
||
a plugin just accesses it off of the first argument:
|
||
|
||
```ts
|
||
import type { CoreSetup } from 'kibana/server';
|
||
|
||
export class DemoPlugin {
|
||
public setup(core: CoreSetup) {
|
||
const router = core.http.createRouter();
|
||
// handler is called when '/path' resource is requested with `GET` method
|
||
router.get({ path: '/path', validate: false }, (context, req, res) =>
|
||
res.ok({ content: 'ok' })
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
Unlike core, capabilities exposed by plugins are not automatically injected into all plugins.
|
||
Instead, if a plugin wishes to use the public interface provided by another plugin, it must first declare that plugin as a
|
||
dependency in it’s kibana.json manifest file.
|
||
|
||
** foobar plugin.ts: **
|
||
|
||
```ts
|
||
import type { Plugin } from 'kibana/server';
|
||
// [1]
|
||
export interface FoobarPluginSetup {
|
||
getFoo(): string;
|
||
}
|
||
|
||
// [1]
|
||
export interface FoobarPluginStart {
|
||
getBar(): string;
|
||
}
|
||
|
||
export class MyPlugin implements Plugin<FoobarPluginSetup, FoobarPluginStart> {
|
||
public setup(): FoobarPluginSetup {
|
||
return {
|
||
getFoo() {
|
||
return 'foo';
|
||
},
|
||
};
|
||
}
|
||
|
||
public start(): FoobarPluginStart {
|
||
return {
|
||
getBar() {
|
||
return 'bar';
|
||
},
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
[1] We highly encourage plugin authors to explicitly declare public interfaces for their plugins.
|
||
|
||
** demo kibana.json**
|
||
|
||
```
|
||
{
|
||
"id": "demo",
|
||
"requiredPlugins": ["foobar"],
|
||
"server": true,
|
||
"ui": true
|
||
}
|
||
```
|
||
|
||
With that specified in the plugin manifest, the appropriate interfaces are then available via the second argument of setup and/or start:
|
||
|
||
```ts
|
||
import type { CoreSetup, CoreStart } from 'kibana/server';
|
||
import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server';
|
||
|
||
// [1]
|
||
interface DemoSetupPlugins {
|
||
foobar: FoobarPluginSetup;
|
||
}
|
||
|
||
interface DemoStartPlugins {
|
||
foobar: FoobarPluginStart;
|
||
}
|
||
|
||
export class DemoPlugin {
|
||
// [2]
|
||
public setup(core: CoreSetup, plugins: DemoSetupPlugins) {
|
||
const { foobar } = plugins;
|
||
foobar.getFoo(); // 'foo'
|
||
foobar.getBar(); // throws because getBar does not exist
|
||
}
|
||
|
||
//[3]
|
||
public start(core: CoreStart, plugins: DemoStartPlugins) {
|
||
const { foobar } = plugins;
|
||
foobar.getFoo(); // throws because getFoo does not exist
|
||
foobar.getBar(); // 'bar'
|
||
}
|
||
|
||
public stop() {}
|
||
}
|
||
```
|
||
|
||
[1] The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID.
|
||
|
||
[2] These manually constructed types should then be used to specify the type of the second argument to the plugin.
|
||
|
||
[3] Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle.
|