[Canvas][i18n] Elements (#27904)
* [Canvas][i18n] Elements * Addressing feedback; using global i18n * Fixing unit test to reflect globals * Making i18n more flexible * Switching to a Provider strategy for i18n
This commit is contained in:
parent
6cefed969b
commit
fa475e2887
|
@ -5,6 +5,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
|
|
||||||
|
import { applyElementStrings, i18nProvider } from '../strings';
|
||||||
import { elementSpecs } from './index';
|
import { elementSpecs } from './index';
|
||||||
|
|
||||||
elementSpecs.forEach(canvas.register);
|
const { i18n, register } = canvas;
|
||||||
|
|
||||||
|
// i18n is only available from Kibana when specs are registered. Init the Canvas i18n Provider with that instance.
|
||||||
|
i18nProvider.init(i18n);
|
||||||
|
|
||||||
|
// Apply localized strings to the Element specs, then register them.
|
||||||
|
applyElementStrings(elementSpecs).forEach(register);
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ElementFactory } from '../elements/types';
|
||||||
|
import { getElementStrings } from './index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function takes a set of Canvas Element specification factories, runs them,
|
||||||
|
* replaces relevant strings (if available) and returns a new factory. We do this
|
||||||
|
* so the specifications themselves have no dependency on i18n, for clarity for both
|
||||||
|
* our and external plugin developers.
|
||||||
|
*/
|
||||||
|
export const applyElementStrings = (elements: ElementFactory[]) => {
|
||||||
|
const elementStrings = getElementStrings();
|
||||||
|
|
||||||
|
return elements.map(spec => {
|
||||||
|
const result = spec();
|
||||||
|
const { name } = result;
|
||||||
|
const strings = elementStrings[name];
|
||||||
|
|
||||||
|
// If we have registered strings for this spec, we should replace any that are available.
|
||||||
|
if (strings) {
|
||||||
|
const { displayName, help } = strings;
|
||||||
|
// If the function has a registered help string, replace it on the spec.
|
||||||
|
if (help) {
|
||||||
|
result.help = help;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName) {
|
||||||
|
result.displayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => result;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18nProvider } from './i18n_provider';
|
||||||
|
i18nProvider.init();
|
||||||
|
|
||||||
|
import { getElementStrings } from '.';
|
||||||
|
import { elementSpecs } from '../elements';
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
i18nProvider.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ElementStrings', () => {
|
||||||
|
const elementStrings = getElementStrings();
|
||||||
|
const elementNames = elementSpecs.map(spec => spec().name);
|
||||||
|
const stringKeys = Object.keys(elementStrings);
|
||||||
|
|
||||||
|
test('All element names should exist in the strings definition', () => {
|
||||||
|
elementNames.forEach(name => expect(stringKeys).toContain(name));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('All string definitions should correspond to an existing element', () => {
|
||||||
|
stringKeys.forEach(key => expect(elementNames).toContain(key));
|
||||||
|
});
|
||||||
|
|
||||||
|
const strings = Object.values(elementStrings);
|
||||||
|
|
||||||
|
test('All elements should have a displayName string defined', () => {
|
||||||
|
strings.forEach(value => {
|
||||||
|
expect(value).toHaveProperty('displayName');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('All elements should have a help string defined', () => {
|
||||||
|
strings.forEach(value => {
|
||||||
|
expect(value).toHaveProperty('help');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18nProvider } from './i18n_provider';
|
||||||
|
|
||||||
|
interface ElementStrings {
|
||||||
|
displayName: string;
|
||||||
|
help: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ElementStringDict {
|
||||||
|
[elementName: string]: ElementStrings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will return a dictionary of strings, organized by Canvas
|
||||||
|
* Element specification. This function requires that `i18nProvider` be
|
||||||
|
* properly initialized.
|
||||||
|
*/
|
||||||
|
export const getElementStrings = (): ElementStringDict => {
|
||||||
|
const i18n = i18nProvider.getInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
areaChart: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.areaChartDisplayName', {
|
||||||
|
defaultMessage: 'Area chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.areaChartHelpText', {
|
||||||
|
defaultMessage: 'A line chart with a filled body',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
bubbleChart: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.bubbleChartDisplayName', {
|
||||||
|
defaultMessage: 'Bubble chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.bubbleChartHelpText', {
|
||||||
|
defaultMessage: 'A customizable bubble chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.debugDisplayName', {
|
||||||
|
defaultMessage: 'Debug',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.debugHelpText', {
|
||||||
|
defaultMessage: 'Just dumps the configuration of the element',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
donut: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.donutChartDisplayName', {
|
||||||
|
defaultMessage: 'Donut chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.donutChartHelpText', {
|
||||||
|
defaultMessage: 'A customizable donut chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
dropdown_filter: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.dropdownFilterDisplayName', {
|
||||||
|
defaultMessage: 'Dropdown Filter',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.dropdownFilterHelpText', {
|
||||||
|
defaultMessage: 'A dropdown from which you can select values for an "exactly" filter',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
horizontalBarChart: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.horizontalBarChartDisplayName', {
|
||||||
|
defaultMessage: 'Horizontal Bar chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.horizontalBarChartHelpText', {
|
||||||
|
defaultMessage: 'A customizable horizontal bar chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
horizontalProgressBar: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.horizontalProgressBarDisplayName', {
|
||||||
|
defaultMessage: 'Horizontal Progress Bar',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.horizontalProgressBarHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a horizontal bar',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
horizontalProgressPill: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.horizontalProgressPillDisplayName', {
|
||||||
|
defaultMessage: 'Horizontal Progress Pill',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.horizontalProgressPillHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a horizontal pill',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.imageDisplayName', {
|
||||||
|
defaultMessage: 'Image',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.imageHelpText', {
|
||||||
|
defaultMessage: 'A static image',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
lineChart: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.lineChartDisplayName', {
|
||||||
|
defaultMessage: 'Line chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.lineChartHelpText', {
|
||||||
|
defaultMessage: 'A customizable line chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
markdown: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.markdownDisplayName', {
|
||||||
|
defaultMessage: 'Markdown',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.markdownHelpText', {
|
||||||
|
defaultMessage: 'Markup from Markdown',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
metric: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.metricDisplayName', {
|
||||||
|
defaultMessage: 'Metric',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.metricHelpText', {
|
||||||
|
defaultMessage: 'A number with a label',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
pie: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.pieDisplayName', {
|
||||||
|
defaultMessage: 'Pie chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.pieHelpText', {
|
||||||
|
defaultMessage: 'Pie chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
plot: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.plotDisplayName', {
|
||||||
|
defaultMessage: 'Coordinate plot',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.plotHelpText', {
|
||||||
|
defaultMessage: 'Mixed line, bar or dot charts',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
progressGauge: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.progressGaugeDisplayName', {
|
||||||
|
defaultMessage: 'Progress Gauge',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.progressGaugeHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a gauge',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
progressSemicircle: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.progressSemicircleDisplayName', {
|
||||||
|
defaultMessage: 'Progress Semicircle',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.progressSemicircleHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a semicircle',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
progressWheel: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.progressWheelDisplayName', {
|
||||||
|
defaultMessage: 'Progress Wheel',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.progressWheelHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a wheel',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
repeatImage: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.repeatImageDisplayName', {
|
||||||
|
defaultMessage: 'Image repeat',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.repeatImageHelpText', {
|
||||||
|
defaultMessage: 'Repeats an image N times',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
revealImage: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.revealImageDisplayName', {
|
||||||
|
defaultMessage: 'Image reveal',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.revealImageHelpText', {
|
||||||
|
defaultMessage: 'Reveals a percentage of an image',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.shapeDisplayName', {
|
||||||
|
defaultMessage: 'Shape',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.shapeHelpText', {
|
||||||
|
defaultMessage: 'A customizable shape',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.tableDisplayName', {
|
||||||
|
defaultMessage: 'Data table',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.tableHelpText', {
|
||||||
|
defaultMessage: 'A scrollable grid for displaying data in a tabular format',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
tiltedPie: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.tiltedPieDisplayName', {
|
||||||
|
defaultMessage: 'Tilted pie chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.tiltedPieHelpText', {
|
||||||
|
defaultMessage: 'A customizable tilted pie chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
time_filter: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.timeFilterDisplayName', {
|
||||||
|
defaultMessage: 'Time filter',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.timeFilterHelpText', {
|
||||||
|
defaultMessage: 'Set a time window',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
verticalBarChart: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.verticalBarChartDisplayName', {
|
||||||
|
defaultMessage: 'Vertical bar chart',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.verticalBarChartHelpText', {
|
||||||
|
defaultMessage: 'A customizable vertical bar chart',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
verticalProgressBar: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.verticalProgressBarDisplayName', {
|
||||||
|
defaultMessage: 'Vertical Progress Bar',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.verticalProgressBarHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a vertical bar',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
verticalProgressPill: {
|
||||||
|
displayName: i18n.translate('xpack.canvas.elements.verticalProgressPillDisplayName', {
|
||||||
|
defaultMessage: 'Vertical Progress Pill',
|
||||||
|
}),
|
||||||
|
help: i18n.translate('xpack.canvas.elements.verticalProgressPillHelpText', {
|
||||||
|
defaultMessage: 'Displays progress as a portion of a vertical pill',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18n as i18nCore } from '@kbn/i18n';
|
||||||
|
|
||||||
|
let i18nCoreInstance: typeof i18nCore | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @kbn/i18n is provided as a global module, but there's a difference between the version provided by Kibana, which is properly
|
||||||
|
* initialized, and the one imported directly from the global module. We need the former, as the latter, for example, won't
|
||||||
|
* be set to the proper locale, (set in kibana.yml or the command line).
|
||||||
|
*
|
||||||
|
* As a result, we need to initialize our own provider before using i18n in Canvas code. This simple singleton is here for that
|
||||||
|
* purpose.
|
||||||
|
*/
|
||||||
|
export const i18nProvider = {
|
||||||
|
// For simplicity in cases like testing, you can just init this Provider without parameters... but you won't have the
|
||||||
|
// Kibana-initialized i18n runtime.
|
||||||
|
init: (i18n: typeof i18nCore = i18nCore): typeof i18nCore => {
|
||||||
|
if (i18nCoreInstance === null) {
|
||||||
|
i18nCoreInstance = i18n;
|
||||||
|
}
|
||||||
|
return i18nCoreInstance;
|
||||||
|
},
|
||||||
|
getInstance: (): typeof i18nCore => {
|
||||||
|
if (i18nCoreInstance === null) {
|
||||||
|
throw new Error(
|
||||||
|
'i18nProvider not initialized; you must first call `init` with an instance of `@kbn/i18n`'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return i18nCoreInstance;
|
||||||
|
},
|
||||||
|
};
|
9
x-pack/plugins/canvas/canvas_plugin_src/strings/index.ts
Normal file
9
x-pack/plugins/canvas/canvas_plugin_src/strings/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './apply_strings';
|
||||||
|
export * from './element_strings';
|
||||||
|
export * from './i18n_provider';
|
|
@ -25,11 +25,11 @@ export class Element {
|
||||||
public height?: number;
|
public height?: number;
|
||||||
|
|
||||||
constructor(config: ElementSpec) {
|
constructor(config: ElementSpec) {
|
||||||
const { name, image, displayName, expression, filter, width, height } = config;
|
const { name, image, displayName, expression, filter, help, width, height } = config;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.displayName = displayName || name;
|
this.displayName = displayName || name;
|
||||||
this.image = image || defaultHeader;
|
this.image = image || defaultHeader;
|
||||||
this.help = config.help || '';
|
this.help = help || '';
|
||||||
|
|
||||||
if (!config.expression) {
|
if (!config.expression) {
|
||||||
throw new Error('Element types must have a default expression');
|
throw new Error('Element types must have a default expression');
|
||||||
|
|
|
@ -17,3 +17,9 @@ exports.runKibanaScript = function(name, args = []) {
|
||||||
process.argv.splice(2, 0, ...args);
|
process.argv.splice(2, 0, ...args);
|
||||||
require('../../../../scripts/' + name); // eslint-disable-line import/no-dynamic-require
|
require('../../../../scripts/' + name); // eslint-disable-line import/no-dynamic-require
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.runXPackScript = function(name, args = []) {
|
||||||
|
process.chdir(resolve(__dirname, '../../..'));
|
||||||
|
process.argv.splice(2, 0, ...args);
|
||||||
|
require('../../../scripts/' + name); // eslint-disable-line import/no-dynamic-require
|
||||||
|
};
|
||||||
|
|
7
x-pack/plugins/canvas/scripts/jest.js
Normal file
7
x-pack/plugins/canvas/scripts/jest.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('./_helpers').runXPackScript('jest', ['plugins/canvas']);
|
13
x-pack/plugins/canvas/types/global.d.ts
vendored
Normal file
13
x-pack/plugins/canvas/types/global.d.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18n as I18N } from '@kbn/i18n';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const canvas: {
|
||||||
|
i18n: typeof I18N;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue