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

Enhancements pattern #62998

Closed
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions examples/custom_greetings/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Custom Greetings

This plugin is an example of adding custom implementations to a registry supplied by another plugin.

It follows the best practice of also emitting direct accessors to these implementations on this
plugin's start contract.

This
```ts
const casualGreeter = customGreetings.getCasualGreeter();
```

should be preferred to

```ts
const casualGreeter = greeting.getGreeter('CASUAL_GREETER');
```

becuase:
- the accessing plugin doesn't need to handle the possibility of `casualGreeter` being undefined
- it's more obvious that the plugin accessing this greeter should list `customGreetings` as a plugin dependency
- if the specific implementation had a specialized type, it can be accessed this way, in lieu of supporting
typescript generics on the generic getter (e.g. `greeting.getGreeter<T>(id)`), which is error prone.
10 changes: 10 additions & 0 deletions examples/custom_greetings/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "customGreetings",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["custom_greetings"],
"server": false,
"ui": true,
"requiredPlugins": ["greeting"],
"optionalPlugins": []
}
17 changes: 17 additions & 0 deletions examples/custom_greetings/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "custom_greetings",
"version": "1.0.0",
"main": "target/examples/custom_greetings",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.7.2"
}
}
24 changes: 24 additions & 0 deletions examples/custom_greetings/public/index.ts
Original file line number Diff line number Diff line change
@@ -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.
*/

import { CustomGreetingsPlugin } from './plugin';

export const plugin = () => new CustomGreetingsPlugin();

export { CustomGreetingsStart } from './plugin';
79 changes: 79 additions & 0 deletions examples/custom_greetings/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 { CoreSetup, Plugin } from 'kibana/public';

import { GreetingStart, GreetingSetup, Greeting } from 'examples/greeting/public';

interface SetupDependencies {
greeting: GreetingSetup;
}

interface StartDependencies {
greeting: GreetingStart;
}

/**
* Expose direct access to specific greeter implementations on the start contract.
* If a plugin knows ahead of time which specific implementation they would like to access
* they should access it directly off this plugin as opposed to retrieving it off the
* generic registry, via `greeting.getGreeter('Casual')`.
*/
export interface CustomGreetingsStart {
getCasualGreeter: () => Greeting;
getExcitedGreeter: () => Greeting;
getFormalGreeter: () => Greeting;
}

export class CustomGreetingsPlugin
implements Plugin<void, CustomGreetingsStart, SetupDependencies, StartDependencies> {
private casualGreeterProvider?: () => Greeting;
private excitedGreeterProvider?: () => Greeting;
private formalGreeterProvider?: () => Greeting;

setup(core: CoreSetup<StartDependencies>, { greeting }: SetupDependencies) {
this.casualGreeterProvider = greeting.registerGreetingDefinition({
id: 'Casual',
salutation: 'Hey there',
punctuation: '.',
});
this.excitedGreeterProvider = greeting.registerGreetingDefinition({
id: 'Excited',
salutation: 'Hi',
punctuation: '!!',
});
this.formalGreeterProvider = greeting.registerGreetingDefinition({
id: 'Formal',
salutation: 'Hello ',
punctuation: '.',
});
}

start() {
const { casualGreeterProvider, excitedGreeterProvider, formalGreeterProvider } = this;
if (!casualGreeterProvider || !excitedGreeterProvider || !formalGreeterProvider) {
throw new Error('Something unexpected went wrong. Greeters should be defined by now.');
}
return {
getCasualGreeter: () => casualGreeterProvider(),
getExcitedGreeter: () => excitedGreeterProvider(),
getFormalGreeter: () => formalGreeterProvider(),
};
}
}
35 changes: 35 additions & 0 deletions examples/custom_greetings/public/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { GreetingStart, Greeting } from '../../greeting/public';

export interface Services {
greetWithGreeter: (greeting: Greeting, name: string) => void;
getGreeters: () => Greeting[];
}

/**
* Rather than pass down depencies directly, we add some indirection with this services file, to help decouple and
* buffer this plugin from any changes in dependency contracts.
* @param dependencies
*/
export const getServices = (dependencies: { greetingServices: GreetingStart }): Services => ({
greetWithGreeter: (greeting: Greeting, name: string) => greeting.greetMe(name),
getGreeters: () => dependencies.greetingServices.getGreeters(),
});
14 changes: 14 additions & 0 deletions examples/custom_greetings/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"../../typings/**/*",
],
"exclude": []
}
27 changes: 27 additions & 0 deletions examples/enhancements_pattern_explorer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Enhancements pattern

This plugin shows a recommended pattern for how one plugin can enhance another plugins functionality when
dealing with a registry of items.

There are three key pieces. The plugin that creates the registry should:

- Expose a `setCustomProvider` function in the Setup contract.
- Expose the ability for other plugins to register _definitions_ in the setup contract.
- Expose the ability for other plugins to retrieve _instances_ in the start contract.

There are three plugin associated with this example.

- the `greeting` plugin exposes a registry of greetings. The default provider uses the very basic `alert` function to greet the user.
- the `greetingEnhanced` plugin registers a custom greeting provider which uses an EuiModal to greet the user with improved stylign.
- this plugin, `enhancementsPatternExplorer` registers a few example greetings as well as an app to expose the `greet` functionality.

To see how this works, first run Kibana with nothing in your `kibana.yml` via `yarn start --run-examples`. Navigate to the Enhancements pattern
app and see how the greetings look.

Then, stop kibana and edit `kibana.yml` to turn the `greetingEnhanced` plugin off by adding this line:

```
greeting_enhanced.enabled: false
```

Restart kibana and go through the same motions, and you should now see just the basic `alert` window.
10 changes: 10 additions & 0 deletions examples/enhancements_pattern_explorer/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "enhancedPatternExplorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["enhanced_pattern_explorer"],
"server": false,
"ui": true,
"requiredPlugins": ["greeting", "customGreetings"],
"optionalPlugins": []
}
17 changes: 17 additions & 0 deletions examples/enhancements_pattern_explorer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "enhancements_pattern_explorer",
"version": "1.0.0",
"main": "target/examples/enhancements_pattern_explorer",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.7.2"
}
}
125 changes: 125 additions & 0 deletions examples/enhancements_pattern_explorer/public/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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 ReactDOM from 'react-dom';
import { EuiPageContent } from '@elastic/eui';
import { EuiFieldText } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';
import { AppMountParameters } from 'kibana/public';

import { EuiButton } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiFormRow } from '@elastic/eui';
import { Services } from './services';
import { Greeting } from '../../greeting/public';

function greeterToComboOption(greeter: Greeting) {
return {
value: greeter,
label: greeter.label,
};
}

function EnhancementsPatternApp(props: Services) {
const [name, setName] = useState('');
const greetersAsOptions = props.getGreeters().map(greeter => greeterToComboOption(greeter));
const defaultGreeting = props.getGreeters()[0];

const [selectedGreeter, setSelectedGreeter] = useState<Greeting | undefined>(defaultGreeting);
return (
<EuiPageContent>
<EuiText>
<h1>Enhancements pattern</h1>
This explorer shows how one plugin can add enhancements via a{' '}
<EuiCode>setCustomProvider</EuiCode> pattern. If you run kibana with{' '}
<EuiCode>yarn start --run-examples</EuiCode> and click the Greet me button, you should see a
modal. This is the enhanced functionality. If you set{' '}
<EuiCode>greetingEnhanced.enabled: false</EuiCode> in your kibana.yml and then run this
example again you should only see a simple alert window, the unenhanced version.
</EuiText>
<EuiSpacer />
<EuiFormRow>
<EuiFieldText
placeholder="What is your name?"
value={name}
onChange={e => setName(e.target.value)}
/>
</EuiFormRow>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
helpText="This functionality will use a greeter known only at runtime, so
all these greeters are accessed off the generic registry plugin."
>
<EuiFlexItem>
<EuiComboBox<Greeting>
selectedOptions={
selectedGreeter ? [greeterToComboOption(selectedGreeter)] : undefined
}
onChange={e => {
setSelectedGreeter(e[0] ? e[0].value : undefined);
}}
options={greetersAsOptions}
singleSelection={{ asPlainText: true }}
/>
<EuiButton
disabled={selectedGreeter === undefined || name === ''}
onClick={() => {
if (selectedGreeter) {
props.greetWithGreeter(selectedGreeter, name);
}
}}
>
Greet me
</EuiButton>
</EuiFlexItem>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
helpText="This button uses a greeter type known at compile time. Prefer accessing the
implementation directly off the plugin instead of accessing off the generic registry if possible."
>
<EuiButton
disabled={selectedGreeter === undefined || name === ''}
onClick={() => props.getCasualGreeter().greetMe(name)}
>
Greet me casually
</EuiButton>
</EuiFormRow>
</EuiFlexItem>

<EuiFlexItem>
<EuiSpacer size="l" />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContent>
);
}

export const renderApp = (services: Services, element: AppMountParameters['element']) => {
ReactDOM.render(<EnhancementsPatternApp {...services} />, element);

return () => ReactDOM.unmountComponentAtNode(element);
};
Loading