diff --git a/examples/custom_greetings/README.md b/examples/custom_greetings/README.md new file mode 100644 index 000000000000000..91eabd11e015f43 --- /dev/null +++ b/examples/custom_greetings/README.md @@ -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(id)`), which is error prone. \ No newline at end of file diff --git a/examples/custom_greetings/kibana.json b/examples/custom_greetings/kibana.json new file mode 100644 index 000000000000000..a43ad299ce5e24e --- /dev/null +++ b/examples/custom_greetings/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "customGreetings", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["custom_greetings"], + "server": false, + "ui": true, + "requiredPlugins": ["greeting"], + "optionalPlugins": [] +} diff --git a/examples/custom_greetings/package.json b/examples/custom_greetings/package.json new file mode 100644 index 000000000000000..03ea6d9211fd0db --- /dev/null +++ b/examples/custom_greetings/package.json @@ -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" + } +} diff --git a/examples/custom_greetings/public/index.ts b/examples/custom_greetings/public/index.ts new file mode 100644 index 000000000000000..b79acd8c7913277 --- /dev/null +++ b/examples/custom_greetings/public/index.ts @@ -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'; diff --git a/examples/custom_greetings/public/plugin.ts b/examples/custom_greetings/public/plugin.ts new file mode 100644 index 000000000000000..20010a7f38fef78 --- /dev/null +++ b/examples/custom_greetings/public/plugin.ts @@ -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 { + private casualGreeterProvider?: () => Greeting; + private excitedGreeterProvider?: () => Greeting; + private formalGreeterProvider?: () => Greeting; + + setup(core: CoreSetup, { 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: () => casualGreeterProvider(), + getFormalGreeter: () => casualGreeterProvider(), + }; + } +} diff --git a/examples/custom_greetings/public/services.ts b/examples/custom_greetings/public/services.ts new file mode 100644 index 000000000000000..c62a9372c9efba2 --- /dev/null +++ b/examples/custom_greetings/public/services.ts @@ -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(), +}); diff --git a/examples/custom_greetings/tsconfig.json b/examples/custom_greetings/tsconfig.json new file mode 100644 index 000000000000000..199fbe1fcfa2692 --- /dev/null +++ b/examples/custom_greetings/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [] +} diff --git a/examples/enhancements_pattern_explorer/kibana.json b/examples/enhancements_pattern_explorer/kibana.json index 6d539a4e0f524d3..e4415ed5bd161ff 100644 --- a/examples/enhancements_pattern_explorer/kibana.json +++ b/examples/enhancements_pattern_explorer/kibana.json @@ -5,6 +5,6 @@ "configPath": ["enhanced_pattern_explorer"], "server": false, "ui": true, - "requiredPlugins": ["greeting"], + "requiredPlugins": ["greeting", "customGreetings"], "optionalPlugins": [] } diff --git a/examples/enhancements_pattern_explorer/public/app.tsx b/examples/enhancements_pattern_explorer/public/app.tsx index 06c1eebd56eb2bf..268714c0ad7c24a 100644 --- a/examples/enhancements_pattern_explorer/public/app.tsx +++ b/examples/enhancements_pattern_explorer/public/app.tsx @@ -27,6 +27,9 @@ 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'; @@ -39,8 +42,8 @@ function greeterToComboOption(greeter: Greeting) { function EnhancementsPatternApp(props: Services) { const [name, setName] = useState(''); - const greetersAsOptions = props.getGreeterObjects().map(greeter => greeterToComboOption(greeter)); - const defaultGreeting = props.getGreeterObjects()[0]; + const greetersAsOptions = props.getGreeters().map(greeter => greeterToComboOption(greeter)); + const defaultGreeting = props.getGreeters()[0]; const [selectedGreeter, setSelectedGreeter] = useState(defaultGreeting); return ( @@ -55,29 +58,62 @@ function EnhancementsPatternApp(props: Services) { example again you should only see a simple alert window, the unenhanced version. - - selectedOptions={selectedGreeter ? [greeterToComboOption(selectedGreeter)] : undefined} - onChange={e => { - setSelectedGreeter(e[0] ? e[0].value : undefined); - }} - options={greetersAsOptions} - singleSelection={{ asPlainText: true }} - /> - setName(e.target.value)} - /> - { - if (selectedGreeter) { - props.greetWithGreeter(selectedGreeter, name); - } - }} - > - Greet me - + + setName(e.target.value)} + /> + + + + + + + + selectedOptions={ + selectedGreeter ? [greeterToComboOption(selectedGreeter)] : undefined + } + onChange={e => { + setSelectedGreeter(e[0] ? e[0].value : undefined); + }} + options={greetersAsOptions} + singleSelection={{ asPlainText: true }} + /> + { + if (selectedGreeter) { + props.greetWithGreeter(selectedGreeter, name); + } + }} + > + Greet me + + + + + + + props.getCasualGreeter().greetMe(name)} + > + Greet me casually + + + + + + + + ); } diff --git a/examples/enhancements_pattern_explorer/public/plugin.ts b/examples/enhancements_pattern_explorer/public/plugin.ts index 7855c74ddbc2804..7343e485137048d 100644 --- a/examples/enhancements_pattern_explorer/public/plugin.ts +++ b/examples/enhancements_pattern_explorer/public/plugin.ts @@ -19,56 +19,33 @@ import { CoreSetup, AppMountParameters, Plugin } from 'kibana/public'; -import { GreetingStart, GreetingSetup, Greeting } from 'examples/greeting/public'; +import { GreetingStart } from 'examples/greeting/public'; +import { CustomGreetingsStart } from 'examples/custom_greetings/public'; import { getServices } from './services'; -interface SetupDependencies { - greeting: GreetingSetup; -} - interface StartDependencies { greeting: GreetingStart; + customGreetings: CustomGreetingsStart; } -interface EnhancedPatternExplorerPluginStart { - enhancedFirstGreeting: (name: string) => void; -} - -export class EnhancedPatternExplorerPlugin - implements - Plugin { - firstGreeting?: () => Greeting; - - setup(core: CoreSetup, { greeting }: SetupDependencies) { - this.firstGreeting = greeting.registerGreetingDefinition({ - id: 'Casual', - salutation: 'Hey there', - punctuation: '.', - }); - greeting.registerGreetingDefinition({ id: 'Excited', salutation: 'Hi', punctuation: '!!' }); - greeting.registerGreetingDefinition({ id: 'Formal', salutation: 'Hello ', punctuation: '.' }); - +export class EnhancedPatternExplorerPlugin implements Plugin { + setup(core: CoreSetup) { core.application.register({ id: 'enhancingmentsPattern', title: 'Ennhancements pattern', async mount(params: AppMountParameters) { const { renderApp } = await import('./app'); const [, depsStart] = await core.getStartServices(); - return renderApp(getServices({ greetingServices: depsStart.greeting }), params.element); + return renderApp( + getServices({ + greeting: depsStart.greeting, + customGreetings: depsStart.customGreetings, + }), + params.element + ); }, }); } - start() { - // an example of registering a greeting and returning a reference to the - // plain or enhanced result - setTimeout(() => this.firstGreeting && this.firstGreeting().greetMe('code ninja'), 2000); - return { - enhancedFirstGreeting: (name: string) => { - if (this.firstGreeting) { - this.firstGreeting().greetMe(name); - } - }, - }; - } + start() {} } diff --git a/examples/enhancements_pattern_explorer/public/services.ts b/examples/enhancements_pattern_explorer/public/services.ts index 7f2f73384e210fb..a39a876a50c787a 100644 --- a/examples/enhancements_pattern_explorer/public/services.ts +++ b/examples/enhancements_pattern_explorer/public/services.ts @@ -17,12 +17,13 @@ * under the License. */ +import { CustomGreetingsStart } from 'examples/custom_greetings/public'; import { GreetingStart, Greeting } from '../../greeting/public'; export interface Services { greetWithGreeter: (greeting: Greeting, name: string) => void; - getGreeterIds: () => string[]; - getGreeterObjects: () => Greeting[]; + getGreeters: () => Greeting[]; + getCasualGreeter: () => Greeting; } /** @@ -30,8 +31,11 @@ export interface Services { * buffer this plugin from any changes in dependency contracts. * @param dependencies */ -export const getServices = (dependencies: { greetingServices: GreetingStart }): Services => ({ +export const getServices = (dependencies: { + greeting: GreetingStart; + customGreetings: CustomGreetingsStart; +}): Services => ({ greetWithGreeter: (greeting: Greeting, name: string) => greeting.greetMe(name), - getGreeterIds: () => dependencies.greetingServices.getRegisteredGreetings(), - getGreeterObjects: () => dependencies.greetingServices.getRegisteredGreetingsAsObjects(), + getGreeters: () => dependencies.greeting.getGreeters(), + getCasualGreeter: () => dependencies.customGreetings.getCasualGreeter(), }); diff --git a/examples/greeting/public/index.ts b/examples/greeting/public/index.ts index 85962271789cf22..9ae4d1f4e06e9b0 100644 --- a/examples/greeting/public/index.ts +++ b/examples/greeting/public/index.ts @@ -20,4 +20,4 @@ import { GreetingPlugin } from './plugin'; export const plugin = () => new GreetingPlugin(); -export { GreetingStart, GreetingSetup, Greeting, GreetingDefinition } from './plugin'; +export { GreetingStart, GreetingSetup, Greeter as Greeting, GreetingDefinition } from './plugin'; diff --git a/examples/greeting/public/plugin.ts b/examples/greeting/public/plugin.ts index 476904a93b5a2ec..b189782fad316a4 100644 --- a/examples/greeting/public/plugin.ts +++ b/examples/greeting/public/plugin.ts @@ -25,12 +25,12 @@ export interface GreetingDefinition { punctuation: string; } -export interface Greeting { +export interface Greeter { greetMe: (name: string) => void; label: string; } -type GreetingProvider = (def: GreetingDefinition) => Greeting; +type GreetingProvider = (def: GreetingDefinition) => Greeter; const defaultGreetingProvider: GreetingProvider = (def: GreetingDefinition) => ({ greetMe: (name: string) => alert(`${def.salutation} ${name}${def.punctuation}`), @@ -38,14 +38,26 @@ const defaultGreetingProvider: GreetingProvider = (def: GreetingDefinition) => ( }); export interface GreetingStart { - getGreeting: (id: string) => Greeting; - getRegisteredGreetings: () => string[]; - getRegisteredGreetingsAsObjects: () => Greeting[]; + /** + * This function should be used if the value of `id` is not known at compile time. Usually + * this is because `id` has been persisted somewhere. If the value of `id` is known at + * compile time (e.g. `greeting.getGreeter(CASUAL_GREETER))`) developers should prefer accessing + * off the plugin that registered `CasualGreeter`, like `customGreetings.getCasualGreeter()`, because: + * - makes it more explicit you should add `customGreetings` to your plugin dependency list. + * - don't need to handle the possibility of `undefined` if it's a required dependency. + * - can get more specialized types in certain situations (e.g. `interface CustomGreeter` vs `Greeter` - + * does not apply to this example but many real world examples are like this.) + */ + getGreeter: (id: string) => Greeter; + /** + * Returns an array of all registered greeters. + */ + getGreeters: () => Greeter[]; } export interface GreetingSetup { setCustomProvider: (customProvider: GreetingProvider) => void; - registerGreetingDefinition: (greetingDefinition: GreetingDefinition) => () => Greeting; + registerGreetingDefinition: (greetingDefinition: GreetingDefinition) => () => Greeter; } export class GreetingPlugin implements Plugin { @@ -63,10 +75,9 @@ export class GreetingPlugin implements Plugin { start() { return { - getGreeting: (id: string) => this.greetingProvider(this.greetingDefinitions[id]), - getRegisteredGreetings: () => Object.keys(this.greetingDefinitions), - getRegisteredGreetingsAsObjects: () => - Object.values(this.greetingDefinitions).map(this.greetingProvider), + getGreeter: (id: string) => this.greetingProvider(this.greetingDefinitions[id]), + + getGreeters: () => Object.values(this.greetingDefinitions).map(this.greetingProvider), }; } }