[FieldFormats] Example plugin (#108070) (#108663)

# Conflicts:
#	.github/CODEOWNERS
This commit is contained in:
Anton Dosov 2021-08-16 14:38:26 +02:00 committed by GitHub
parent e737f79d55
commit 65e73c08ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 564 additions and 1 deletions

View file

@ -0,0 +1,10 @@
## Field formats example
Field formats is a service used by index patterns for applying custom formatting to values in a document.
Field formats service can also be used separately from index patterns.
This example plugin shows:
1. How field formats can be used for formatting values
2. How to create a custom field format and make it available in index pattern field editor
3. How to create a custom editor for a custom field formatter

View file

@ -0,0 +1,12 @@
{
"id": "fieldFormatsExample",
"version": "1.0.0",
"kibanaVersion": "kibana",
"ui": true,
"owner": {
"name": "App Services",
"githubTeam": "kibana-app-services"
},
"description": "A plugin that demonstrates field formats usage",
"requiredPlugins": ["developerExamples", "fieldFormats", "indexPatternFieldEditor", "data"]
}

View file

@ -0,0 +1,208 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import {
EuiBasicTable,
EuiCallOut,
EuiCode,
EuiCodeBlock,
EuiLink,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FieldFormatsStart } from '../../../src/plugins/field_formats/public';
import * as example1 from './examples/1_using_existing_format';
import * as example2 from './examples/2_creating_custom_formatter';
// @ts-ignore
import example1SampleCode from '!!raw-loader!./examples/1_using_existing_format';
// @ts-ignore
import example2SampleCode from '!!raw-loader!./examples/2_creating_custom_formatter';
// @ts-ignore
import example3SampleCode from '!!raw-loader!./examples/3_creating_custom_format_editor';
export interface Deps {
fieldFormats: FieldFormatsStart;
/**
* Just for demo purposes
*/
openIndexPatternNumberFieldEditor: () => void;
}
const UsingAnExistingFieldFormatExample: React.FC<{ deps: Deps }> = (props) => {
const sample = example1.getSample(props.deps.fieldFormats);
return (
<>
<EuiText>
<p>
This example shows how to use existing field formatter to format values. As an example, we
have a following sample configuration{' '}
<EuiCode>{JSON.stringify(example1.sampleSerializedFieldFormat)}</EuiCode> representing a{' '}
<EuiCode>bytes</EuiCode>
field formatter with a <EuiCode>0.00b</EuiCode> pattern.
</p>
</EuiText>
<EuiSpacer size={'s'} />
<EuiCodeBlock>{example1SampleCode}</EuiCodeBlock>
<EuiSpacer size={'s'} />
<EuiBasicTable
data-test-subj={'example1 sample table'}
items={sample}
textOnly={true}
columns={[
{
field: 'raw',
name: 'Raw value',
'data-test-subj': 'example1 sample raw',
},
{
field: 'formatted',
name: 'Formatted value',
'data-test-subj': 'example1 sample formatted',
},
]}
/>
</>
);
};
const CreatingCustomFieldFormat: React.FC<{ deps: Deps }> = (props) => {
const sample = example2.getSample(props.deps.fieldFormats);
return (
<>
<EuiText>
<p>
This example shows how to create a custom field formatter. As an example, we create a
currency formatter and then display some values as <EuiCode>USD</EuiCode>.
</p>
</EuiText>
<EuiSpacer size={'s'} />
<EuiCodeBlock>{example2SampleCode}</EuiCodeBlock>
<EuiSpacer size={'s'} />
<EuiBasicTable
items={sample}
textOnly={true}
data-test-subj={'example2 sample table'}
columns={[
{
field: 'raw',
name: 'Raw value',
'data-test-subj': 'example2 sample raw',
},
{
field: 'formatted',
name: 'Formatted value',
'data-test-subj': 'example2 sample formatted',
},
]}
/>
<EuiSpacer size={'s'} />
<EuiCallOut
title="Seamless integration with index patterns!"
color="success"
iconType="indexManagementApp"
>
<p>
Currency formatter that we&apos;ve just created is already integrated with index patterns.
It can be applied to any <EuiCode>numeric</EuiCode> field of any index pattern.{' '}
<EuiLink onClick={() => props.deps.openIndexPatternNumberFieldEditor()}>
Open index pattern field editor to give it a try.
</EuiLink>
</p>
</EuiCallOut>
</>
);
};
const CreatingCustomFieldFormatEditor: React.FC<{ deps: Deps }> = (props) => {
return (
<>
<EuiText>
<p>
This example shows how to create a custom field formatter editor. As an example, we will
create a format editor for the currency formatter created in the previous section. This
custom editor will allow to select either <EuiCode>USD</EuiCode> or <EuiCode>EUR</EuiCode>{' '}
currency.
</p>
</EuiText>
<EuiSpacer size={'s'} />
<EuiCodeBlock>{example3SampleCode}</EuiCodeBlock>
<EuiSpacer size={'s'} />
<EuiCallOut
title="Check the result in the index pattern field editor!"
color="primary"
iconType="indexManagementApp"
>
<p>
Currency formatter and its custom editor are integrated with index patterns. It can be
applied to any <EuiCode>numeric</EuiCode> field of any index pattern.{' '}
<EuiLink onClick={() => props.deps.openIndexPatternNumberFieldEditor()}>
Open index pattern field editor to give it a try.
</EuiLink>
</p>
</EuiCallOut>
</>
);
};
export const App: React.FC<{ deps: Deps }> = (props) => {
return (
<EuiPage>
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Field formats examples</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody style={{ maxWidth: 800, margin: '0 auto' }}>
<section>
<EuiTitle size="m">
<h2>Using an existing field format</h2>
</EuiTitle>
<EuiSpacer />
<UsingAnExistingFieldFormatExample deps={props.deps} />
</section>
<EuiSpacer />
<EuiSpacer />
<section>
<EuiTitle size="m">
<h2>Creating a custom field format</h2>
</EuiTitle>
<EuiSpacer />
<CreatingCustomFieldFormat deps={props.deps} />
</section>
<EuiSpacer />
<EuiSpacer />
<section>
<EuiTitle size="m">
<h2>Creating a custom field format editor</h2>
</EuiTitle>
<EuiSpacer />
<CreatingCustomFieldFormatEditor deps={props.deps} />
</section>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SerializedFieldFormat } from '../../../../src/plugins/field_formats/common';
import { FieldFormatsStart } from '../../../../src/plugins/field_formats/public';
// 1. Assume we have an existing field format configuration serialized and saved somewhere
// In this case it is `bytes` field formatter with a configured `'0.00b'` pattern
// NOTE: the `params` field is not type checked and a consumer has to know the `param` format that a particular `formatId` expects,
// https://github.com/elastic/kibana/issues/108158
export const sampleSerializedFieldFormat: SerializedFieldFormat<{ pattern: string }> = {
id: 'bytes',
params: {
pattern: '0.00b',
},
};
export function getSample(fieldFormats: FieldFormatsStart) {
// 2. we create a field format instance from an existing configuration
const fieldFormat = fieldFormats.deserialize(sampleSerializedFieldFormat);
// 3. now we can use it to convert values
const pairs = [1000, 100000, 100000000].map((value) => ({
raw: value,
formatted: fieldFormat.convert(value),
}));
return pairs;
}

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import { FieldFormat, SerializedFieldFormat } from '../../../../src/plugins/field_formats/common';
import { FieldFormatsSetup, FieldFormatsStart } from '../../../../src/plugins/field_formats/public';
// 1. Create a custom formatter by extending {@link FieldFormat}
export class ExampleCurrencyFormat extends FieldFormat {
static id = 'example-currency';
static title = 'Currency (example)';
// 2. Specify field types that this formatter supports
static fieldType = KBN_FIELD_TYPES.NUMBER;
// Or pass an array in case supports multiple types
// static fieldType = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE];
// 3. This formats support a `currency` param. Use `EUR` as a default.
getParamDefaults() {
return {
currency: 'EUR',
};
}
// 4. Implement a conversion function
textConvert = (val: unknown) => {
if (typeof val !== 'number') return `${val}`;
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: this.param('currency'),
}).format(val);
};
}
export function registerExampleFormat(fieldFormats: FieldFormatsSetup) {
// 5. Register a field format. This should happen in setup plugin lifecycle phase.
fieldFormats.register([ExampleCurrencyFormat]);
}
// 6. Now let's apply the formatter to some sample values
export function getSample(fieldFormats: FieldFormatsStart) {
const exampleSerializedFieldFormat: SerializedFieldFormat<{ currency: string }> = {
id: 'example-currency',
params: {
currency: 'USD',
},
};
const fieldFormat = fieldFormats.deserialize(exampleSerializedFieldFormat);
const pairs = [1000, 100000, 100000000].map((value) => ({
raw: value,
formatted: fieldFormat.convert(value),
}));
return pairs;
}

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import {
FieldFormatEditor,
FieldFormatEditorFactory,
IndexPatternFieldEditorSetup,
} from '../../../../src/plugins/index_pattern_field_editor/public';
import { ExampleCurrencyFormat } from './2_creating_custom_formatter';
// 1. Create an editor component
// NOTE: the `params` field is not type checked and a consumer has to know the `param` format that a particular `formatId` expects,
// https://github.com/elastic/kibana/issues/108158
const ExampleCurrencyFormatEditor: FieldFormatEditor<{ currency: string }> = (props) => {
return (
<EuiFormRow label={'Currency'}>
<EuiSelect
defaultValue={props.formatParams.currency}
options={[
{ text: 'EUR', value: 'EUR' },
{ text: 'USD', value: 'USD' },
]}
onChange={(e) => {
props.onChange({
currency: e.target.value,
});
}}
/>
</EuiFormRow>
);
};
// 2. Make sure it has a `formatId` that corresponds to format's id
ExampleCurrencyFormatEditor.formatId = ExampleCurrencyFormat.id;
// 3. Wrap editor component in a factory. This is needed to support and encourage code-splitting.
const ExampleCurrencyFormatEditorFactory: FieldFormatEditorFactory<{
currency: string;
}> = async () => ExampleCurrencyFormatEditor;
ExampleCurrencyFormatEditorFactory.formatId = ExampleCurrencyFormatEditor.formatId;
export function registerExampleFormatEditor(indexPatternFieldEditor: IndexPatternFieldEditorSetup) {
// 4. Register a field editor. This should happen in setup plugin lifecycle phase.
indexPatternFieldEditor.fieldFormatEditors.register(ExampleCurrencyFormatEditorFactory);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FieldFormatsExamplePlugin } from './plugin';
export function plugin() {
return new FieldFormatsExamplePlugin();
}

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import {
AppMountParameters,
AppNavLinkStatus,
CoreSetup,
CoreStart,
Plugin,
} from '../../../src/core/public';
import { DeveloperExamplesSetup } from '../../developer_examples/public';
import { App } from './app';
import { FieldFormatsSetup, FieldFormatsStart } from '../../../src/plugins/field_formats/public';
import { registerExampleFormat } from './examples/2_creating_custom_formatter';
import {
IndexPatternFieldEditorStart,
IndexPatternFieldEditorSetup,
} from '../../../src/plugins/index_pattern_field_editor/public';
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
import { registerExampleFormatEditor } from './examples/3_creating_custom_format_editor';
import img from './formats.png';
interface SetupDeps {
developerExamples: DeveloperExamplesSetup;
fieldFormats: FieldFormatsSetup;
indexPatternFieldEditor: IndexPatternFieldEditorSetup;
}
interface StartDeps {
fieldFormats: FieldFormatsStart;
indexPatternFieldEditor: IndexPatternFieldEditorStart;
data: DataPublicPluginStart;
}
export class FieldFormatsExamplePlugin implements Plugin<void, void, SetupDeps, StartDeps> {
public setup(core: CoreSetup<StartDeps>, deps: SetupDeps) {
registerExampleFormat(deps.fieldFormats);
registerExampleFormatEditor(deps.indexPatternFieldEditor);
// just for demonstration purposes:
// opens a field editor using default index pattern and first number field
const openIndexPatternNumberFieldEditor = async () => {
const [, plugins] = await core.getStartServices();
const indexPattern = await plugins.data.indexPatterns.getDefault();
if (!indexPattern) {
alert('Creating at least one index pattern to continue with this example');
return;
}
const numberField = indexPattern
.getNonScriptedFields()
.find((f) => !f.name.startsWith('_') && f.type === KBN_FIELD_TYPES.NUMBER);
if (!numberField) {
alert(
'Default index pattern needs at least a single field of type `number` to continue with this example'
);
return;
}
plugins.indexPatternFieldEditor.openEditor({
ctx: {
indexPattern,
},
fieldName: numberField.name,
});
};
// Register an application into the side navigation menu
core.application.register({
id: 'fieldFormatsExample',
title: 'Field formats example',
navLinkStatus: AppNavLinkStatus.hidden,
async mount({ element }: AppMountParameters) {
const [, plugins] = await core.getStartServices();
ReactDOM.render(
<App deps={{ fieldFormats: plugins.fieldFormats, openIndexPatternNumberFieldEditor }} />,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
},
});
// This section is only needed to get this example plugin to show up in our Developer Examples.
deps.developerExamples.register({
appId: 'fieldFormatsExample',
title: 'Field formats example',
description: `Learn how to use an existing field formats or how to create a custom one`,
image: img,
});
}
public start(core: CoreStart) {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"common/**/*.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../src/core/tsconfig.json" },
{ "path": "../developer_examples/tsconfig.json" },
{ "path": "../../src/plugins/field_formats/tsconfig.json" },
{ "path": "../../src/plugins/data/tsconfig.json" },
{ "path": "../../src/plugins/index_pattern_field_editor/tsconfig.json" }
]
}

View file

@ -20,7 +20,10 @@
import { IndexPatternFieldEditorPlugin } from './plugin';
export { PluginStart as IndexPatternFieldEditorStart } from './types';
export type {
PluginSetup as IndexPatternFieldEditorSetup,
PluginStart as IndexPatternFieldEditorStart,
} from './types';
export { DefaultFormatEditor } from './components/field_format_editor/editors/default/default';
export { FieldFormatEditorFactory, FieldFormatEditor, FormatEditorProps } from './components';

View file

@ -30,6 +30,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./routing'),
require.resolve('./expressions_explorer'),
require.resolve('./index_pattern_field_editor_example'),
require.resolve('./field_formats'),
],
services: {
...functionalConfig.get('services'),

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from 'test/functional/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
describe('Field formats example', function () {
before(async () => {
this.tags('ciGroup2');
await PageObjects.common.navigateToApp('fieldFormatsExample');
});
it('renders field formats example 1', async () => {
const formattedValues = await Promise.all(
(await testSubjects.findAll('example1 sample formatted')).map((wrapper) =>
wrapper.getVisibleText()
)
);
expect(formattedValues).to.eql(['1000.00B', '97.66KB', '95.37MB']);
});
it('renders field formats example 2', async () => {
const formattedValues = await Promise.all(
(await testSubjects.findAll('example2 sample formatted')).map((wrapper) =>
wrapper.getVisibleText()
)
);
expect(formattedValues).to.eql(['$1,000.00', '$100,000.00', '$100,000,000.00']);
});
});
}