Merge remote-tracking branch 'origin/master' into feature/merge-code

This commit is contained in:
Fuyao Zhao 2019-01-03 11:21:41 -05:00
commit c68c2d74c2
439 changed files with 7744 additions and 3751 deletions

86
.sass-lint.yml Normal file
View file

@ -0,0 +1,86 @@
files:
include:
- '{src,x-pack}/**/*.s+(a|c)ss'
ignore:
# _only include_ rollup and security files
- '**/x-pack/plugins/!(rollup|security)/**'
# ignore all of src
- '**/src/**/*'
# ignore all node_modules
- '**/node_modules/**'
rules:
quotes:
- 2
-
style: 'single'
# } else { style on one line, like our JS
brace-style:
- 2
-
style: '1tbs'
variable-name-format:
- 2
-
convention: 'camelcase'
# Needs regex, right now we ignore
class-name-format: 0
# Order how you please
property-sort-order: 0
hex-notation:
- 2
-
style: 'uppercase'
mixin-name-format:
- 2
-
allow-leading-underscore: false
convention: 'camelcase'
# Use none instead of 0 for no border
border-zero:
- 2
- convention: 'none'
force-element-nesting: 0
# something { not something{
space-before-brace:
- 2
force-pseudo-nesting: 0
# 2 spaces for indentation
indentation: 2
function-name-format:
- 2
-
allow-leading-underscore: false
convention: 'camelcase'
# This removes the need for ::hover
pseudo-element: 0
# ($var / 2) rather than ($var/2)
space-around-operator: 2
# We minify css, so this doesn't apply
no-css-comments: 0
# We use _ (underscores) for import path that don't directly compile
clean-import-paths: 0
# Allows input[type=search]
force-attribute-nesting: 0
no-qualifying-elements:
- 2
-
# Allows input[type=search]
allow-element-with-attribute: 1
# Files can end without a newline
final-newline: 0
# We use some rare duplicate property values for browser variance
no-duplicate-properties:
- 2
-
exclude:
- 'font-size'
- 'word-break'
# Put a line-break between sections of CSS, but allow quicky one-liners for legibility
empty-line-between-blocks:
- 2
-
allow-single-line-rulesets: 1
# Warns are nice for deprecations and development
no-warn: 0
# Transition all is useful in certain situations and there's no recent info to suggest slowdown
no-transition-all: 0

View file

@ -1,5 +1,5 @@
Kibana source code with Kibana X-Pack source code
Copyright 2012-2018 Elasticsearch B.V.
Copyright 2012-2019 Elasticsearch B.V.
---
This product has relied on ASTExplorer that is licensed under MIT.

View file

@ -52,19 +52,11 @@ triangle that appears next to the URL line of the request. Notice that as you mo
.The Action Menu
image::dev-tools/console/images/introduction_action_menu.png["The Action Menu",width=400,align="center"]
When the response come back, you should see it in the left hand panel:
When the response come back, you should see it in the right hand panel:
.The Output Pane
image::dev-tools/console/images/introduction_output.png[Screenshot]
[float]
[[console-ui]]
=== The Console UI
In this section you will find a more detailed description of UI of Console. The basic aspects of the UI are explained
in the <<console-kibana>> section.
include::multi-requests.asciidoc[]
include::auto-formatting.asciidoc[]

View file

@ -42,6 +42,8 @@
"test:dev": "grunt test:dev",
"test:quick": "grunt test:quick",
"test:browser": "grunt test:browser",
"test:jest": "node scripts/jest",
"test:mocha": "grunt test:mocha",
"test:ui": "echo 'use `node scripts/functional_tests`' && false",
"test:ui:server": "echo 'use `node scripts/functional_tests_server`' && false",
"test:ui:runner": "echo 'use `node scripts/functional_test_runner`' && false",
@ -93,7 +95,7 @@
},
"dependencies": {
"@elastic/datemath": "5.0.2",
"@elastic/eui": "5.8.1",
"@elastic/eui": "6.0.1",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "2.3.2",
@ -107,6 +109,7 @@
"@kbn/pm": "1.0.0",
"@kbn/test-subj-selector": "0.2.1",
"@kbn/ui-framework": "1.0.0",
"JSONStream": "1.1.1",
"abortcontroller-polyfill": "^1.1.9",
"angular": "1.6.9",
"angular-aria": "1.6.6",
@ -238,7 +241,7 @@
"vision": "^5.3.3",
"webpack": "4.23.1",
"webpack-merge": "4.1.4",
"whatwg-fetch": "^2.0.3",
"whatwg-fetch": "^3.0.0",
"wreck": "^14.0.2",
"x-pack": "7.0.0",
"yauzl": "2.7.0"
@ -270,7 +273,7 @@
"@types/enzyme": "^3.1.12",
"@types/eslint": "^4.16.2",
"@types/execa": "^0.9.0",
"@types/fetch-mock": "^5.12.2",
"@types/fetch-mock": "7.2.1",
"@types/getopts": "^2.0.0",
"@types/glob": "^5.0.35",
"@types/globby": "^8.0.0",
@ -337,7 +340,7 @@
"eslint-plugin-react": "^7.11.1",
"expect.js": "0.3.1",
"faker": "1.1.0",
"fetch-mock": "^5.13.1",
"fetch-mock": "7.3.0",
"geckodriver": "1.12.2",
"getopts": "2.0.0",
"grunt": "1.0.1",
@ -382,6 +385,7 @@
"prettier": "^1.14.3",
"proxyquire": "1.7.11",
"regenerate": "^1.4.0",
"sass-lint": "^1.12.1",
"simple-git": "1.37.0",
"sinon": "^7.2.2",
"strip-ansi": "^3.0.1",

View file

@ -172,6 +172,7 @@ module.exports = {
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-duplicates': 'error',
'import/no-dynamic-require': 'error',
'prefer-object-spread/prefer-object-spread': 'error',
}

View file

@ -55,7 +55,7 @@ exports.getPlugins = function(config, kibanaPath, projectRoot) {
return pluginsFromMap.concat(
glob.sync(globPatterns).map(pkgJsonPath => {
const path = dirname(pkgJsonPath);
const pkg = require(pkgJsonPath);
const pkg = require(pkgJsonPath); // eslint-disable-line import/no-dynamic-require
return {
name: pkg.name,
directory: path,

View file

@ -393,29 +393,29 @@ For example, there is a component that is wrapped by `injectI18n`, like in the `
```js
// ...
class AddFilterUi extends Component {
export const AddFilter = injectI18n(
class AddFilterUi extends Component {
// ...
render() {
const { filter } = this.state;
return (
<EuiFlexGroup>
<EuiFlexItem grow={10}>
<EuiFieldText
fullWidth
value={filter}
onChange={e => this.setState({ filter: e.target.value.trim() })}
placeholder={this.props.intl.formatMessage({
id: 'kbn.management.indexPattern.edit.source.placeholder',
defaultMessage: 'source filter, accepts wildcards (e.g., `user*` to filter fields starting with \'user\')'
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
render() {
const { filter } = this.state;
return (
<EuiFlexGroup>
<EuiFlexItem grow={10}>
<EuiFieldText
fullWidth
value={filter}
onChange={e => this.setState({ filter: e.target.value.trim() })}
placeholder={this.props.intl.formatMessage({
id: 'kbn.management.indexPattern.edit.source.placeholder',
defaultMessage: 'source filter, accepts wildcards (e.g., `user*` to filter fields starting with \'user\')'
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
}
export const AddFilter = injectI18n(AddFilterUi);
);
```
To test the `AddFilter` component it is needed to render its `WrappedComponent` property using `shallowWithIntl` function to pass `intl` object into the `props`.

View file

@ -298,7 +298,7 @@ React component as a pure function:
import React from 'react';
import { injectI18n, intlShape } from '@kbn/i18n/react';
const MyComponentContent = ({ intl }) => (
export const MyComponent = injectI18n({ intl }) => (
<input
type="text"
placeholder={intl.formatMessage(
@ -311,13 +311,11 @@ const MyComponentContent = ({ intl }) => (
{ name, unreadCount }
)}
/>
);
));
MyComponentContent.propTypes = {
MyComponent.WrappedComponent.propTypes = {
intl: intlShape.isRequired,
};
export const MyComponent = injectI18n(MyComponentContent);
```
React component as a class:
@ -326,27 +324,27 @@ React component as a class:
import React from 'react';
import { injectI18n, intlShape } from '@kbn/i18n/react';
class MyComponentContent extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
};
export const MyComponent = injectI18n(
class MyComponent extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
};
render() {
const { intl } = this.props;
render() {
const { intl } = this.props;
return (
<input
type="text"
placeholder={intl.formatMessage({
id: 'kbn.management.objects.searchPlaceholder',
defaultMessage: 'Search',
})}
/>
);
return (
<input
type="text"
placeholder={intl.formatMessage({
id: 'kbn.management.objects.searchPlaceholder',
defaultMessage: 'Search',
})}
/>
);
}
}
}
export const MyComponent = injectI18n(MyComponentContent);
);
```
## AngularJS

View file

@ -9,6 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
"@kbn/i18n": "1.0.0",
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
"lodash.clone": "^4.5.0",
"scriptjs": "^2.5.8",
@ -24,6 +25,7 @@
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "6.20.0",
"css-loader": "1.0.0",
"copy-webpack-plugin": "^4.6.0",
"del": "^3.0.0",
"getopts": "^2.2.3",
"pegjs": "0.9.0",
@ -34,4 +36,4 @@
"webpack": "4.23.1",
"webpack-cli": "^3.1.2"
}
}
}

View file

@ -0,0 +1,20 @@
/*
* 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 '../common/register';

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import $script from 'scriptjs';
let resolvePromise = null;
@ -43,6 +44,7 @@ const loadBrowserRegistries = (registries, basePath) => {
const type = remainingTypes.pop();
window.canvas = window.canvas || {};
window.canvas.register = d => registries[type].register(d);
window.canvas.i18n = i18n;
// Load plugins one at a time because each needs a different loader function
// $script will only load each of these once, we so can call this as many times as we need?

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { typesRegistry } from '../common/lib/types_registry';
import { functionsRegistry as serverFunctions } from '../common/lib/functions_registry';
import { getPluginPaths } from './get_plugin_paths';
@ -48,23 +49,18 @@ export const populateServerRegistries = types => {
const remainingTypes = types;
const populatedTypes = {};
const globalKeys = Object.keys(global);
const loadType = () => {
const type = remainingTypes.pop();
getPluginPaths(type).then(paths => {
global.canvas = global.canvas || {};
global.canvas.register = d => registries[type].register(d);
global.canvas.i18n = i18n;
paths.forEach(path => {
require(path);
require(path); // eslint-disable-line import/no-dynamic-require
});
Object.keys(global).forEach(key => {
if (!globalKeys.includes(key)) {
delete global[key];
}
});
delete global.canvas;
populatedTypes[type] = registries[type];
if (remainingTypes.length) loadType();

View file

@ -0,0 +1,3 @@
/* eslint-disable */
import util from 'util';
console.log(util.format('hello world'));

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
const { extname } = require('path');
const { transform } = require('babel-core');
exports.createServerCodeTransformer = (sourceMaps) => {
return (content, path) => {
switch (extname(path)) {
case '.js':
const { code = '' } = transform(content.toString('utf8'), {
filename: path,
ast: false,
code: true,
sourceMaps: sourceMaps ? 'inline' : false,
babelrc: false,
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
});
return code;
default:
return content.toString('utf8');
}
};
};

View file

@ -0,0 +1,49 @@
/*
* 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 { readFileSync } from 'fs';
import { resolve } from 'path';
import { createServerCodeTransformer } from './server_code_transformer';
const JS_FIXTURE_PATH = resolve(__dirname, '__fixtures__/sample.js');
const JS_FIXTURE = readFileSync(JS_FIXTURE_PATH);
describe('js support', () => {
it('transpiles js file', () => {
const transformer = createServerCodeTransformer();
expect(transformer(JS_FIXTURE, JS_FIXTURE_PATH)).toMatchInlineSnapshot(`
"'use strict';
var _util = require('util');
var _util2 = _interopRequireDefault(_util);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_util2.default.format('hello world')); /* eslint-disable */"
`);
});
it('throws errors for js syntax errors', () => {
const transformer = createServerCodeTransformer();
expect(() => transformer(Buffer.from(`export default 'foo`), JS_FIXTURE_PATH)).toThrowError(
/Unterminated string constant/
);
});
});

View file

@ -18,6 +18,10 @@
*/
const { resolve } = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { createServerCodeTransformer } = require('./server_code_transformer');
const {
PLUGIN_SOURCE_DIR,
PLUGIN_BUILD_DIR,
@ -31,7 +35,7 @@ module.exports = function ({ sourceMaps }, { watch }) {
mode: 'none',
entry: {
'types/all': resolve(PLUGIN_SOURCE_DIR, 'types/register.js'),
'functions/common/all': resolve(PLUGIN_SOURCE_DIR, 'functions/common/register.js'),
'functions/browser/all': resolve(PLUGIN_SOURCE_DIR, 'functions/browser/register.js'),
},
// there were problems with the node and web targets since this code is actually
@ -95,6 +99,15 @@ module.exports = function ({ sourceMaps }, { watch }) {
stats: 'errors-only',
plugins: [
new CopyWebpackPlugin([
{
from: resolve(PLUGIN_SOURCE_DIR, 'functions/common'),
to: resolve(PLUGIN_BUILD_DIR, 'functions/common'),
ignore: '**/__tests__/**',
transform: createServerCodeTransformer(sourceMaps)
},
]),
function LoaderFailHandlerPlugin() {
if (!watch) {
return;

View file

@ -27,7 +27,7 @@ const chalk = require('chalk');
const pkg = require('../package.json');
const kibanaPkgPath = require.resolve('../../../package.json');
const kibanaPkg = require(kibanaPkgPath);
const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require
const KBN_DIR = dirname(kibanaPkgPath);
@ -72,6 +72,7 @@ module.exports = function({ name }) {
filters: {
'public/**/*': 'generateApp',
'translations/**/*': 'generateTranslations',
'.i18nrc.json': 'generateTranslations',
'public/hack.js': 'generateHack',
'server/**/*': 'generateApi',
'public/app.scss': 'generateScss',
@ -80,6 +81,7 @@ module.exports = function({ name }) {
move: {
gitignore: '.gitignore',
eslintrc: '.eslintrc',
'package_template.json': 'package.json',
},
data: answers =>
Object.assign(

View file

@ -0,0 +1,5 @@
{
"paths": {
"<%= camelCase(name) %>": "./"
}
}

View file

@ -17,6 +17,11 @@
"test:browser": "plugin-helpers test:browser",
"build": "plugin-helpers build"
},
<%_ if (generateTranslations) { _%>
"dependencies": {
"@kbn/i18n": "link:../../kibana/packages/kbn-i18n"
},
<%_ } _%>
"devDependencies": {
"@elastic/eslint-config-kibana": "link:../../kibana/packages/eslint-config-kibana",
"@elastic/eslint-import-resolver-kibana": "link:../../kibana/packages/kbn-eslint-import-resolver-kibana",

View file

@ -2,6 +2,9 @@ import React from 'react';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { render, unmountComponentAtNode } from 'react-dom';
<%_ if (generateTranslations) { _%>
import { I18nProvider } from '@kbn/i18n/react';
<%_ } _%>
import 'ui/autoload/styles';
import './less/main.less';
@ -24,7 +27,16 @@ function RootController($scope, $element, $http) {
const domNode = $element[0];
// render react to DOM
<%_ if (generateTranslations) { _%>
render(
<I18nProvider>
<Main title="<%= name %>" httpClient={$http} />
</I18nProvider>,
domNode
);
<%_ } else { _%>
render(<Main title="<%= name %>" httpClient={$http} />, domNode);
<%_ } _%>
// unmount react on controller destroy
$scope.$on('$destroy', () => {

View file

@ -9,6 +9,9 @@ import {
EuiPageContentBody,
EuiText
} from '@elastic/eui';
<%_ if (generateTranslations) { _%>
import { FormattedMessage } from '@kbn/i18n/react';
<%_ } _%>
export class Main extends React.Component {
constructor(props) {
@ -33,19 +36,57 @@ export class Main extends React.Component {
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>{title} Hello World!</h1>
<h1>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.helloWorldText"
defaultMessage="{title} Hello World!"
values={{ title }}
/>
<%_ } else { _%>
{title} Hello World!
<%_ } _%>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiTitle>
<h2>Congratulations</h2>
<h2>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.congratulationsTitle"
defaultMessage="Congratulations"
/>
<%_ } else { _%>
Congratulations
<%_ } _%>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<h3>You have successfully created your first Kibana Plugin!</h3>
<p>The server time (via API call) is {this.state.time || 'NO API CALL YET'}</p>
<h3>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.congratulationsText"
defaultMessage="You have successfully created your first Kibana Plugin!"
/>
<%_ } else { _%>
You have successfully created your first Kibana Plugin!
<%_ } _%>
</h3>
<p>
<%_ if (generateTranslations) { _%>
<FormattedMessage
id="<%= camelCase(name) %>.serverTimeText"
defaultMessage="The server time (via API call) is {time}"
values={{ time: this.state.time || 'NO API CALL YET' }}
/>
<%_ } else { _%>
The server time (via API call) is {this.state.time || 'NO API CALL YET'}
<%_ } _%>
</p>
</EuiText>
</EuiPageContentBody>
</EuiPageContent>

View file

@ -0,0 +1,84 @@
{
"formats": {
"number": {
"currency": {
"style": "currency"
},
"percent": {
"style": "percent"
}
},
"date": {
"short": {
"month": "numeric",
"day": "numeric",
"year": "2-digit"
},
"medium": {
"month": "short",
"day": "numeric",
"year": "numeric"
},
"long": {
"month": "long",
"day": "numeric",
"year": "numeric"
},
"full": {
"weekday": "long",
"month": "long",
"day": "numeric",
"year": "numeric"
}
},
"time": {
"short": {
"hour": "numeric",
"minute": "numeric"
},
"medium": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric"
},
"long": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
},
"full": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
}
},
"relative": {
"years": {
"units": "year"
},
"months": {
"units": "month"
},
"days": {
"units": "day"
},
"hours": {
"units": "hour"
},
"minutes": {
"units": "minute"
},
"seconds": {
"units": "second"
}
}
},
"messages": {
"<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。",
"<%= camelCase(name) %>.congratulationsTitle": "恭喜!",
"<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!",
"<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}"
}
}

View file

@ -26,10 +26,10 @@ function babelRegister() {
try {
// add support for moved babel-register source: https://github.com/elastic/kibana/pull/13973
require(resolve(plugin.kibanaRoot, 'src/setup_node_env/babel_register'));
require(resolve(plugin.kibanaRoot, 'src/setup_node_env/babel_register')); // eslint-disable-line import/no-dynamic-require
} catch (error) {
if (error.code === 'MODULE_NOT_FOUND') {
require(resolve(plugin.kibanaRoot, 'src/optimize/babel/register'));
require(resolve(plugin.kibanaRoot, 'src/optimize/babel/register')); // eslint-disable-line import/no-dynamic-require
} else {
throw error;
}
@ -42,11 +42,8 @@ function resolveKibanaPath(path) {
}
function readFtrConfigFile(log, path, settingOverrides) {
return require(resolveKibanaPath('src/functional_test_runner')).readConfigFile(
log,
path,
settingOverrides
);
return require(resolveKibanaPath('src/functional_test_runner')) // eslint-disable-line import/no-dynamic-require
.readConfigFile(log, path, settingOverrides);
}
module.exports = {

View file

@ -51,7 +51,7 @@ function removeSymlinkDependencies(root) {
// parse a ts config file
function parseTsconfig(pluginSourcePath, configPath) {
const ts = require(path.join(pluginSourcePath, 'node_modules', 'typescript'));
const ts = require(path.join(pluginSourcePath, 'node_modules', 'typescript')); // eslint-disable-line import/no-dynamic-require
const { error, config } = ts.parseConfigFileTextToJson(
configPath,

View file

@ -42,7 +42,7 @@ describe('creating the build', () => {
await createBuild(PLUGIN, buildTarget, buildVersion, kibanaVersion, buildFiles);
const pkg = require(resolve(PLUGIN_BUILD_TARGET, 'package.json'));
const pkg = require(resolve(PLUGIN_BUILD_TARGET, 'package.json')); // eslint-disable-line import/no-dynamic-require
expect(pkg).not.toHaveProperty('scripts');
expect(pkg).not.toHaveProperty('devDependencies');
});
@ -52,7 +52,7 @@ describe('creating the build', () => {
await createBuild(PLUGIN, buildTarget, buildVersion, kibanaVersion, buildFiles);
const pkg = require(resolve(PLUGIN_BUILD_TARGET, 'package.json'));
const pkg = require(resolve(PLUGIN_BUILD_TARGET, 'package.json')); // eslint-disable-line import/no-dynamic-require
expect(pkg).toHaveProperty('build');
expect(pkg.build.git).not.toBeUndefined();
expect(pkg.build.date).not.toBeUndefined();

21
scripts/i18n_integrate.js Normal file
View file

@ -0,0 +1,21 @@
/*
* 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.
*/
require('../src/setup_node_env');
require('../src/dev/run_i18n_integrate');

21
scripts/sasslint.js Normal file
View file

@ -0,0 +1,21 @@
/*
* 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.
*/
require('../src/setup_node_env');
require('../src/dev/run_sasslint');

View file

@ -1,126 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#batchSet Buffers are always clear of previously buffered changes: two requests, second only sends bar, not foo 1`] = `
Object {
"matched": Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
],
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"bar\\":\\"box\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
"method": "POST",
},
],
"unmatched": Array [],
}
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"bar\\":\\"box\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
]
`;
exports[`#batchSet Overwrites previously buffered values with new values for the same key: two requests, foo=d in final 1`] = `
Object {
"matched": Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"a\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"a\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
],
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"d\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
"method": "POST",
},
],
"unmatched": Array [],
}
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"d\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
]
`;
exports[`#batchSet buffers changes while first request is in progress, sends buffered changes after first request completes: final, includes both requests 1`] = `
Object {
"matched": Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
],
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"box\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
"method": "POST",
},
],
"unmatched": Array [],
}
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"box\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
},
],
]
`;
exports[`#batchSet buffers changes while first request is in progress, sends buffered changes after first request completes: initial, only one request 1`] = `
Object {
"matched": Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
],
"method": "POST",
},
],
"unmatched": Array [],
}
]
`;
exports[`#batchSet rejects all promises for batched requests that fail: promise rejections 1`] = `
@ -147,22 +135,19 @@ exports[`#batchSet rejects on 404 response 1`] = `"Request failed with status co
exports[`#batchSet rejects on 500 1`] = `"Request failed with status code: 500"`;
exports[`#batchSet sends a single change immediately: synchronous fetch 1`] = `
Object {
"matched": Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
"method": "POST",
Array [
Array [
"/foo/bar/api/kibana/settings",
Object {
"body": "{\\"changes\\":{\\"foo\\":\\"bar\\"}}",
"credentials": "same-origin",
"headers": Object {
"accept": "application/json",
"content-type": "application/json",
"kbn-version": "v9.9.9",
},
],
"method": "POST",
},
],
"unmatched": Array [],
}
]
`;

View file

@ -17,7 +17,8 @@
* under the License.
*/
import fetchMock from 'fetch-mock';
// @ts-ignore
import fetchMock from 'fetch-mock/es5/client';
import * as Rx from 'rxjs';
import { takeUntil, toArray } from 'rxjs/operators';
@ -142,10 +143,16 @@ describe('#batchSet', () => {
fetchMock.once('*', {
body: { settings: {} },
});
fetchMock.once('*', {
status: 400,
body: 'invalid',
});
fetchMock.once(
'*',
{
status: 400,
body: 'invalid',
},
{
overwriteRoutes: false,
}
);
const { uiSettingsApi } = setup();
// trigger the initial sync request, which enabled buffering
@ -161,7 +168,7 @@ describe('#batchSet', () => {
).resolves.toMatchSnapshot('promise rejections');
// ensure only two requests were sent
expect(fetchMock.calls().matched).toHaveLength(2);
expect(fetchMock.calls()).toHaveLength(2);
});
});
@ -191,10 +198,16 @@ describe('#getLoadingCount$()', () => {
fetchMock.once('*', {
body: { settings: {} },
});
fetchMock.once('*', {
status: 400,
body: 'invalid',
});
fetchMock.once(
'*',
{
status: 400,
body: 'invalid',
},
{
overwriteRoutes: false,
}
);
const { uiSettingsApi } = setup();
const done$ = new Rx.Subject();

View file

@ -25,7 +25,7 @@ import { createPlatform } from './platform';
export async function getConfig({ isRelease, targetAllPlatforms, versionQualifier }) {
const pkgPath = resolve(__dirname, '../../../../package.json');
const pkg = require(pkgPath);
const pkg = require(pkgPath); // eslint-disable-line import/no-dynamic-require
const repoRoot = dirname(pkgPath);
const nodeVersion = pkg.engines.node;

View file

@ -34,7 +34,7 @@ export const TranspileScssTask = {
const uiExports = collectUiExports(enabledPlugins);
try {
const bundles = await buildAll(uiExports.styleSheetPaths);
const bundles = await buildAll(uiExports.styleSheetPaths, log);
bundles.forEach(bundle => log.info(`Compiled SCSS: ${bundle.source}`));
} catch (error) {
const { message, line, file } = error;

View file

@ -0,0 +1,63 @@
{
"formats": {
"number": {
"currency": {
"style": "currency"
},
"percent": {
"style": "percent"
}
},
"date": {
"short": {
"month": "numeric",
"day": "numeric",
"year": "2-digit"
},
"medium": {
"month": "short",
"day": "numeric",
"year": "numeric"
},
"long": {
"month": "long",
"day": "numeric",
"year": "numeric"
},
"full": {
"weekday": "long",
"month": "long",
"day": "numeric",
"year": "numeric"
}
},
"time": {
"short": {
"hour": "numeric",
"minute": "numeric"
},
"medium": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric"
},
"long": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
},
"full": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
}
}
},
"messages": {
"plugin-1.message-id-1": "Translated text 1",
"plugin-1.message-id-2": "Translated text 2",
"plugin-2.message-id": "Translated text"
}
}

View file

@ -55,9 +55,17 @@ Array [
`;
exports[`dev/i18n/extract_default_translations throws on id collision 1`] = `
" I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx
Array [
" I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx
Error: There is more than one default message for the same id \\"plugin_3.duplicate_id\\":
\\"Message 1\\" and \\"Message 2\\""
\\"Message 1\\" and \\"Message 2\\"",
]
`;
exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See .i18nrc.json for the list of supported namespaces."`;
exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `
Array [
Array [
[Error: Expected "wrong_plugin_namespace.message-id" id to have "plugin_2" namespace. See .i18nrc.json for the list of supported namespaces.],
],
]
`;

View file

@ -0,0 +1,163 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 1`] = `
Array [
"src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations/fr.json",
"{
\\"formats\\": {
\\"number\\": {
\\"currency\\": {
\\"style\\": \\"currency\\"
},
\\"percent\\": {
\\"style\\": \\"percent\\"
}
},
\\"date\\": {
\\"short\\": {
\\"month\\": \\"numeric\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"2-digit\\"
},
\\"medium\\": {
\\"month\\": \\"short\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
},
\\"long\\": {
\\"month\\": \\"long\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
},
\\"full\\": {
\\"weekday\\": \\"long\\",
\\"month\\": \\"long\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
}
},
\\"time\\": {
\\"short\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\"
},
\\"medium\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\"
},
\\"long\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\",
\\"timeZoneName\\": \\"short\\"
},
\\"full\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\",
\\"timeZoneName\\": \\"short\\"
}
}
},
\\"messages\\": {
\\"plugin-1.message-id-1\\": \\"Translated text 1\\",
\\"plugin-1.message-id-2\\": \\"Translated text 2\\"
}
}",
]
`;
exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 2`] = `
Array [
"src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations/fr.json",
"{
\\"formats\\": {
\\"number\\": {
\\"currency\\": {
\\"style\\": \\"currency\\"
},
\\"percent\\": {
\\"style\\": \\"percent\\"
}
},
\\"date\\": {
\\"short\\": {
\\"month\\": \\"numeric\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"2-digit\\"
},
\\"medium\\": {
\\"month\\": \\"short\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
},
\\"long\\": {
\\"month\\": \\"long\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
},
\\"full\\": {
\\"weekday\\": \\"long\\",
\\"month\\": \\"long\\",
\\"day\\": \\"numeric\\",
\\"year\\": \\"numeric\\"
}
},
\\"time\\": {
\\"short\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\"
},
\\"medium\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\"
},
\\"long\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\",
\\"timeZoneName\\": \\"short\\"
},
\\"full\\": {
\\"hour\\": \\"numeric\\",
\\"minute\\": \\"numeric\\",
\\"second\\": \\"numeric\\",
\\"timeZoneName\\": \\"short\\"
}
}
},
\\"messages\\": {
\\"plugin-2.message-id\\": \\"Translated text\\"
}
}",
]
`;
exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 3`] = `
Array [
"src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations",
"src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations",
]
`;
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 1`] = `
"
Missing translations:
plugin-1.message-id-2"
`;
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 2`] = `
"
Unused translations:
plugin-1.message-id-3"
`;
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 3`] = `
"
Unused translations:
plugin-2.message
Missing translations:
plugin-2.message-id"
`;

View file

@ -9,6 +9,8 @@ exports[`i18n utils should create verbose parser error message 1`] = `
"
`;
exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n"`;
exports[`i18n utils should not escape linebreaks 1`] = `
"Text
with

View file

@ -18,8 +18,6 @@
*/
import path from 'path';
import normalize from 'normalize-path';
import chalk from 'chalk';
import {
extractHtmlMessages,
@ -27,26 +25,24 @@ import {
extractPugMessages,
extractHandlebarsMessages,
} from './extractors';
import { globAsync, readFileAsync } from './utils';
import { paths, exclude } from '../../../.i18nrc.json';
import { globAsync, readFileAsync, normalizePath } from './utils';
import { createFailError, isFailError } from '../run';
function addMessageToMap(targetMap, key, value) {
function addMessageToMap(targetMap, key, value, reporter) {
const existingValue = targetMap.get(key);
if (targetMap.has(key) && existingValue.message !== value.message) {
throw createFailError(`There is more than one default message for the same id "${key}":
"${existingValue.message}" and "${value.message}"`);
reporter.report(
createFailError(`There is more than one default message for the same id "${key}":
"${existingValue.message}" and "${value.message}"`)
);
} else {
targetMap.set(key, value);
}
targetMap.set(key, value);
}
function normalizePath(inputPath) {
return normalize(path.relative('.', inputPath));
}
export function filterPaths(inputPaths) {
export function filterPaths(inputPaths, paths) {
const availablePaths = Object.values(paths);
const pathsForExtraction = new Set();
@ -70,26 +66,28 @@ export function filterPaths(inputPaths) {
return [...pathsForExtraction];
}
function filterEntries(entries) {
function filterEntries(entries, exclude) {
return entries.filter(entry =>
exclude.every(excludedPath => !normalizePath(entry).startsWith(excludedPath))
);
}
export function validateMessageNamespace(id, filePath) {
export function validateMessageNamespace(id, filePath, allowedPaths, reporter) {
const normalizedPath = normalizePath(filePath);
const [expectedNamespace] = Object.entries(paths).find(([, pluginPath]) =>
const [expectedNamespace] = Object.entries(allowedPaths).find(([, pluginPath]) =>
normalizedPath.startsWith(`${pluginPath}/`)
);
if (!id.startsWith(`${expectedNamespace}.`)) {
throw createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \
See .i18nrc.json for the list of supported namespaces.`);
reporter.report(
createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \
See .i18nrc.json for the list of supported namespaces.`)
);
}
}
export async function extractMessagesFromPathToMap(inputPath, targetMap) {
export async function extractMessagesFromPathToMap(inputPath, targetMap, config, reporter) {
const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html,hbs,handlebars}', {
cwd: inputPath,
matchBase: true,
@ -123,7 +121,7 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) {
[hbsEntries, extractHandlebarsMessages],
].map(async ([entries, extractFunction]) => {
const files = await Promise.all(
filterEntries(entries).map(async entry => {
filterEntries(entries, config.exclude).map(async entry => {
return {
name: entry,
content: await readFileAsync(entry),
@ -132,21 +130,31 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) {
);
for (const { name, content } of files) {
const reporterWithContext = reporter.withContext({ name });
try {
for (const [id, value] of extractFunction(content)) {
validateMessageNamespace(id, name);
addMessageToMap(targetMap, id, value);
for (const [id, value] of extractFunction(content, reporterWithContext)) {
validateMessageNamespace(id, name, config.paths, reporterWithContext);
addMessageToMap(targetMap, id, value, reporterWithContext);
}
} catch (error) {
if (isFailError(error)) {
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}`
);
if (!isFailError(error)) {
throw error;
}
throw error;
reporterWithContext.report(error);
}
}
})
);
}
export async function getDefaultMessagesMap(inputPaths, config, reporter) {
const defaultMessagesMap = new Map();
for (const inputPath of filterPaths(inputPaths, config.paths)) {
await extractMessagesFromPathToMap(inputPath, defaultMessagesMap, config, reporter);
}
return defaultMessagesMap;
}

View file

@ -23,6 +23,7 @@ import {
extractMessagesFromPathToMap,
validateMessageNamespace,
} from './extract_default_translations';
import { ErrorReporter } from './utils';
const fixturesPath = path.resolve(__dirname, '__fixtures__', 'extract_default_translations');
const pluginsPaths = [
@ -31,31 +32,32 @@ const pluginsPaths = [
path.join(fixturesPath, 'test_plugin_3'),
];
jest.mock('../../../.i18nrc.json', () => ({
const config = {
paths: {
plugin_1: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1',
plugin_2: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2',
plugin_3: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3',
},
exclude: [],
}));
};
describe('dev/i18n/extract_default_translations', () => {
test('extracts messages from path to map', async () => {
const [pluginPath] = pluginsPaths;
const resultMap = new Map();
await extractMessagesFromPathToMap(pluginPath, resultMap);
await extractMessagesFromPathToMap(pluginPath, resultMap, config, new ErrorReporter());
expect([...resultMap].sort()).toMatchSnapshot();
});
test('throws on id collision', async () => {
const [, , pluginPath] = pluginsPaths;
const reporter = new ErrorReporter();
await expect(
extractMessagesFromPathToMap(pluginPath, new Map())
).rejects.toThrowErrorMatchingSnapshot();
extractMessagesFromPathToMap(pluginPath, new Map(), config, reporter)
).resolves.not.toThrow();
expect(reporter.errors).toMatchSnapshot();
});
test('validates message namespace', () => {
@ -64,15 +66,18 @@ describe('dev/i18n/extract_default_translations', () => {
__dirname,
'__fixtures__/extract_default_translations/test_plugin_2/test_file.html'
);
expect(() => validateMessageNamespace(id, filePath)).not.toThrow();
expect(() => validateMessageNamespace(id, filePath, config.paths)).not.toThrow();
});
test('throws on wrong message namespace', () => {
const report = jest.fn();
const id = 'wrong_plugin_namespace.message-id';
const filePath = path.resolve(
__dirname,
'__fixtures__/extract_default_translations/test_plugin_2/test_file.html'
);
expect(() => validateMessageNamespace(id, filePath)).toThrowErrorMatchingSnapshot();
expect(() => validateMessageNamespace(id, filePath, config.paths, { report })).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
});

View file

@ -26,6 +26,18 @@ Array [
]
`;
exports[`dev/i18n/extractors/code throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`dev/i18n/extractors/code throws on empty id 1`] = `
Array [
Array [
[Error: Empty "id" value in i18n() or i18n.translate() is not allowed.],
],
]
`;
exports[`dev/i18n/extractors/code throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extractors/code throws on missing defaultMessage 1`] = `
Array [
Array [
[Error: Empty defaultMessage in intl.formatMessage() is not allowed ("message-id").],
],
]
`;

View file

@ -12,10 +12,34 @@ Array [
]
`;
exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`;
exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `
Array [
Array [
[Error: Empty id argument in Handlebars i18n is not allowed.],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"defaultMessage value in Handlebars i18n should be a string (\\"message-id\\")."`;
exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `
Array [
Array [
[Error: defaultMessage value in Handlebars i18n should be a string ("message-id").],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`;
exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `
Array [
Array [
[Error: Wrong number of arguments for handlebars i18n call.],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`;
exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `
Array [
Array [
[Error: Properties string in Handlebars i18n should be a string literal ("ui.id-1").],
],
]
`;

View file

@ -50,12 +50,28 @@ Array [
]
`;
exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`;
exports[`dev/i18n/extractors/html throws on i18n filter usage in complex angular expression 1`] = `
"Couldn't parse angular i18n expression:
Unexpected token, expected \\";\\" (1:6):
mode as ('metricVis.colorModes.' + mode"
exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `
Array [
Array [
[Error: Empty "i18n-id" value in angular directive is not allowed.],
],
]
`;
exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extractors/html throws on i18n filter usage in complex angular expression 1`] = `
Array [
Array [
[Error: Couldn't parse angular i18n expression:
Unexpected token, expected ";" (1:6):
mode as ('metricVis.colorModes.' + mode],
],
]
`;
exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `
Array [
Array [
[Error: Empty defaultMessage in angular directive is not allowed ("message-id").],
],
]
`;

View file

@ -20,6 +20,18 @@ Array [
]
`;
exports[`dev/i18n/extractors/pug throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`dev/i18n/extractors/pug throws on empty id 1`] = `
Array [
Array [
[Error: Empty "id" value in i18n() or i18n.translate() is not allowed.],
],
]
`;
exports[`dev/i18n/extractors/pug throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extractors/pug throws on missing default message 1`] = `
Array [
Array [
[Error: Empty defaultMessage in i18n() or i18n.translate() is not allowed ("message-id").],
],
]
`;

View file

@ -29,7 +29,7 @@ import {
import { extractI18nCallMessages } from './i18n_call';
import { createParserErrorMessage, isI18nTranslateFunction, traverseNodes } from '../utils';
import { extractIntlMessages, extractFormattedMessages } from './react';
import { createFailError } from '../../run';
import { createFailError, isFailError } from '../../run';
/**
* Detect Intl.formatMessage() function call (React).
@ -61,7 +61,7 @@ export function isFormattedMessageElement(node) {
return isJSXOpeningElement(node) && isJSXIdentifier(node.name, { name: 'FormattedMessage' });
}
export function* extractCodeMessages(buffer) {
export function* extractCodeMessages(buffer, reporter) {
let ast;
try {
@ -72,19 +72,26 @@ export function* extractCodeMessages(buffer) {
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(buffer.toString(), error);
throw createFailError(errorWithContext);
reporter.report(createFailError(errorWithContext));
return;
}
throw error;
}
for (const node of traverseNodes(ast.program.body)) {
if (isI18nTranslateFunction(node)) {
yield extractI18nCallMessages(node);
} else if (isIntlFormatMessageFunction(node)) {
yield extractIntlMessages(node);
} else if (isFormattedMessageElement(node)) {
yield extractFormattedMessages(node);
try {
if (isI18nTranslateFunction(node)) {
yield extractI18nCallMessages(node);
} else if (isIntlFormatMessageFunction(node)) {
yield extractIntlMessages(node);
} else if (isFormattedMessageElement(node)) {
yield extractFormattedMessages(node);
}
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
}
}

View file

@ -65,7 +65,13 @@ function f() {
}
`;
const report = jest.fn();
describe('dev/i18n/extractors/code', () => {
beforeEach(() => {
report.mockClear();
});
test('extracts React, server-side and angular service default messages', () => {
const actual = Array.from(extractCodeMessages(extractCodeMessagesSource));
expect(actual.sort()).toMatchSnapshot();
@ -73,12 +79,14 @@ describe('dev/i18n/extractors/code', () => {
test('throws on empty id', () => {
const source = Buffer.from(`i18n.translate('', { defaultMessage: 'Default message' });`);
expect(() => extractCodeMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractCodeMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on missing defaultMessage', () => {
const source = Buffer.from(`intl.formatMessage({ id: 'message-id' });`);
expect(() => extractCodeMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractCodeMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
});

View file

@ -18,7 +18,7 @@
*/
import { formatJSString, checkValuesProperty } from '../utils';
import { createFailError } from '../../run';
import { createFailError, isFailError } from '../../run';
import { DEFAULT_MESSAGE_KEY, DESCRIPTION_KEY } from '../constants';
const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g;
@ -27,7 +27,7 @@ const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g;
/**
* Example: `'{{i18n 'message-id' '{"defaultMessage": "Message text"}'}}'`
*/
export function* extractHandlebarsMessages(buffer) {
export function* extractHandlebarsMessages(buffer, reporter) {
for (const expression of buffer.toString().match(HBS_REGEX) || []) {
const tokens = expression.match(TOKENS_REGEX);
@ -38,58 +38,78 @@ export function* extractHandlebarsMessages(buffer) {
}
if (tokens.length !== 3) {
throw createFailError(`Wrong number of arguments for handlebars i18n call.`);
reporter.report(createFailError(`Wrong number of arguments for handlebars i18n call.`));
continue;
}
if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) {
throw createFailError(`Message id should be a string literal.`);
reporter.report(createFailError(`Message id should be a string literal.`));
continue;
}
const messageId = formatJSString(idString.slice(1, -1));
if (!messageId) {
throw createFailError(`Empty id argument in Handlebars i18n is not allowed.`);
reporter.report(createFailError(`Empty id argument in Handlebars i18n is not allowed.`));
continue;
}
if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) {
throw createFailError(
`Properties string in Handlebars i18n should be a string literal ("${messageId}").`
reporter.report(
createFailError(
`Properties string in Handlebars i18n should be a string literal ("${messageId}").`
)
);
continue;
}
const properties = JSON.parse(propertiesString.slice(1, -1));
if (typeof properties.defaultMessage !== 'string') {
throw createFailError(
`defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
reporter.report(
createFailError(
`defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
)
);
continue;
}
if (properties[DESCRIPTION_KEY] != null && typeof properties[DESCRIPTION_KEY] !== 'string') {
throw createFailError(
`Description value in Handlebars i18n should be a string ("${messageId}").`
reporter.report(
createFailError(`Description value in Handlebars i18n should be a string ("${messageId}").`)
);
continue;
}
const message = formatJSString(properties[DEFAULT_MESSAGE_KEY]);
const description = formatJSString(properties[DESCRIPTION_KEY]);
if (!message) {
throw createFailError(
`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`
reporter.report(
createFailError(`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`)
);
continue;
}
const valuesObject = properties.values;
if (valuesObject != null && typeof valuesObject !== 'object') {
throw createFailError(
`"values" value should be an object in Handlebars i18n ("${messageId}").`
reporter.report(
createFailError(`"values" value should be an object in Handlebars i18n ("${messageId}").`)
);
continue;
}
checkValuesProperty(Object.keys(valuesObject || {}), message, messageId);
try {
checkValuesProperty(Object.keys(valuesObject || {}), message, messageId);
yield [messageId, { message, description }];
yield [messageId, { message, description }];
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
}
}

View file

@ -19,7 +19,13 @@
import { extractHandlebarsMessages } from './handlebars';
const report = jest.fn();
describe('dev/i18n/extractors/handlebars', () => {
beforeEach(() => {
report.mockClear();
});
test('extracts handlebars default messages', () => {
const source = Buffer.from(`\
window.onload = function () {
@ -49,7 +55,8 @@ window.onload = function () {
};
`);
expect(() => extractHandlebarsMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on wrong properties argument type', () => {
@ -59,7 +66,8 @@ window.onload = function () {
};
`);
expect(() => extractHandlebarsMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on empty id', () => {
@ -69,7 +77,8 @@ window.onload = function () {
};
`);
expect(() => extractHandlebarsMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on missing defaultMessage property', () => {
@ -79,6 +88,7 @@ window.onload = function () {
};
`);
expect(() => extractHandlebarsMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
});

View file

@ -33,7 +33,7 @@ import {
extractDescriptionValueFromNode,
} from '../utils';
import { DEFAULT_MESSAGE_KEY, DESCRIPTION_KEY, VALUES_KEY } from '../constants';
import { createFailError } from '../../run';
import { createFailError, isFailError } from '../../run';
/**
* Find all substrings of "{{ any text }}" pattern allowing '{' and '}' chars in single quote strings
@ -152,41 +152,49 @@ function* extractExpressions(htmlContent) {
}
}
function* getFilterMessages(htmlContent) {
function* getFilterMessages(htmlContent, reporter) {
for (const expression of extractExpressions(htmlContent)) {
const filterStart = expression.indexOf(I18N_FILTER_MARKER);
const idExpression = trimOneTimeBindingOperator(expression.slice(0, filterStart).trim());
const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim();
if (!filterObjectExpression || !idExpression) {
throw createFailError(`Cannot parse i18n filter expression: ${expression}`);
}
try {
if (!filterObjectExpression || !idExpression) {
throw createFailError(`Cannot parse i18n filter expression: ${expression}`);
}
const messageId = parseIdExpression(idExpression);
const messageId = parseIdExpression(idExpression);
if (!messageId) {
throw createFailError(`Empty "id" value in angular filter expression is not allowed.`);
}
if (!messageId) {
throw createFailError('Empty "id" value in angular filter expression is not allowed.');
}
const { message, description, valuesKeys } = parseFilterObjectExpression(
filterObjectExpression,
messageId
);
if (!message) {
throw createFailError(
`Empty defaultMessage in angular filter expression is not allowed ("${messageId}").`
const { message, description, valuesKeys } = parseFilterObjectExpression(
filterObjectExpression,
messageId
);
if (!message) {
throw createFailError(
`Empty defaultMessage in angular filter expression is not allowed ("${messageId}").`
);
}
checkValuesProperty(valuesKeys, message, messageId);
yield [messageId, { message, description }];
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
checkValuesProperty(valuesKeys, message, messageId);
yield [messageId, { message, description }];
}
}
function* getDirectiveMessages(htmlContent) {
function* getDirectiveMessages(htmlContent, reporter) {
const $ = cheerio.load(htmlContent);
const elements = $('[i18n-id]')
@ -205,34 +213,51 @@ function* getDirectiveMessages(htmlContent) {
for (const element of elements) {
const messageId = formatHTMLString(element.id);
if (!messageId) {
throw createFailError(`Empty "i18n-id" value in angular directive is not allowed.`);
reporter.report(
createFailError('Empty "i18n-id" value in angular directive is not allowed.')
);
continue;
}
const message = formatHTMLString(element.defaultMessage);
if (!message) {
throw createFailError(
`Empty defaultMessage in angular directive is not allowed ("${messageId}").`
reporter.report(
createFailError(
`Empty defaultMessage in angular directive is not allowed ("${messageId}").`
)
);
continue;
}
if (element.values) {
const ast = parseExpression(element.values);
const valuesObjectNode = [...traverseNodes(ast.program.body)].find(node =>
isObjectExpression(node)
);
const valuesKeys = extractValuesKeysFromNode(valuesObjectNode);
try {
if (element.values) {
const ast = parseExpression(element.values);
const valuesObjectNode = [...traverseNodes(ast.program.body)].find(node =>
isObjectExpression(node)
);
const valuesKeys = extractValuesKeysFromNode(valuesObjectNode);
checkValuesProperty(valuesKeys, message, messageId);
} else {
checkValuesProperty([], message, messageId);
checkValuesProperty(valuesKeys, message, messageId);
} else {
checkValuesProperty([], message, messageId);
}
yield [
messageId,
{ message, description: formatHTMLString(element.description) || undefined },
];
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
yield [messageId, { message, description: formatHTMLString(element.description) || undefined }];
}
}
export function* extractHtmlMessages(buffer) {
export function* extractHtmlMessages(buffer, reporter) {
const content = buffer.toString();
yield* getDirectiveMessages(content);
yield* getFilterMessages(content);
yield* getDirectiveMessages(content, reporter);
yield* getFilterMessages(content, reporter);
}

View file

@ -41,7 +41,13 @@ const htmlSourceBuffer = Buffer.from(`
</div>
`);
const report = jest.fn();
describe('dev/i18n/extractors/html', () => {
beforeEach(() => {
report.mockClear();
});
test('extracts default messages from HTML', () => {
const actual = Array.from(extractHtmlMessages(htmlSourceBuffer));
expect(actual.sort()).toMatchSnapshot();
@ -67,7 +73,8 @@ describe('dev/i18n/extractors/html', () => {
></p>
`);
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on missing i18n-default-message attribute', () => {
@ -77,7 +84,8 @@ describe('dev/i18n/extractors/html', () => {
></p>
`);
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on i18n filter usage in complex angular expression', () => {
@ -87,7 +95,8 @@ describe('dev/i18n/extractors/html', () => {
></div>
`);
expect(() => extractHtmlMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractHtmlMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('extracts message from i18n filter in interpolating directive', () => {

View file

@ -21,40 +21,52 @@ import { parse } from '@babel/parser';
import { extractI18nCallMessages } from './i18n_call';
import { isI18nTranslateFunction, traverseNodes, createParserErrorMessage } from '../utils';
import { createFailError } from '../../run';
import { createFailError, isFailError } from '../../run';
/**
* Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}`
*/
const PUG_I18N_REGEX = /i18n\((([^)']|'([^'\\]|\\.)*')*)\)/g;
function parsePugExpression(expression) {
let ast;
try {
ast = parse(expression);
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(expression, error);
throw createFailError(
`Couldn't parse Pug expression with i18n(...) call:\n${errorWithContext}`
);
}
throw error;
}
return ast;
}
/**
* Example: `#{i18n('message-id', { defaultMessage: 'Message text' })}`
*/
export function* extractPugMessages(buffer) {
export function* extractPugMessages(buffer, reporter) {
const expressions = buffer.toString().match(PUG_I18N_REGEX) || [];
for (const expression of expressions) {
let ast;
try {
ast = parse(expression);
} catch (error) {
if (error instanceof SyntaxError) {
const errorWithContext = createParserErrorMessage(expression, error);
throw createFailError(
`Couldn't parse Pug expression with i18n(...) call:\n${errorWithContext}`
);
}
const ast = parsePugExpression(expression);
const node = [...traverseNodes(ast.program.body)].find(node => isI18nTranslateFunction(node));
throw error;
}
for (const node of traverseNodes(ast.program.body)) {
if (isI18nTranslateFunction(node)) {
if (node) {
yield extractI18nCallMessages(node);
break;
}
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
}
}

View file

@ -19,7 +19,13 @@
import { extractPugMessages } from './pug';
const report = jest.fn();
describe('dev/i18n/extractors/pug', () => {
beforeEach(() => {
report.mockClear();
});
test('extracts messages from pug template with interpolation', () => {
const source = Buffer.from(`\
#{i18n('message-id', { defaultMessage: 'Default message', description: 'Message description' })}
@ -43,7 +49,8 @@ describe('dev/i18n/extractors/pug', () => {
h1= i18n('', { defaultMessage: 'Default message', description: 'Message description' })
`);
expect(() => extractPugMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractPugMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on missing default message', () => {
@ -51,6 +58,7 @@ h1= i18n('', { defaultMessage: 'Default message', description: 'Message descript
#{i18n('message-id', { description: 'Message description' })}
`);
expect(() => extractPugMessages(source).next()).toThrowErrorMatchingSnapshot();
expect(() => extractPugMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
});

View file

@ -18,5 +18,5 @@
*/
export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations';
export { writeFileAsync } from './utils';
export { writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils';
export { serializeToJson, serializeToJson5 } from './serializers';

View file

@ -0,0 +1,112 @@
/*
* 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 path from 'path';
import {
difference,
readFileAsync,
writeFileAsync,
accessAsync,
makeDirAsync,
normalizePath,
ErrorReporter,
} from './utils';
import { paths, exclude } from '../../../.i18nrc.json';
import { getDefaultMessagesMap } from './extract_default_translations';
import { createFailError } from '../run';
import { serializeToJson } from './serializers/json';
export function verifyMessages(localizedMessagesMap, defaultMessagesMap) {
let errorMessage = '';
const defaultMessagesIds = [...defaultMessagesMap.keys()];
const localizedMessagesIds = [...localizedMessagesMap.keys()];
const unusedTranslations = difference(localizedMessagesIds, defaultMessagesIds);
if (unusedTranslations.length > 0) {
errorMessage += `\nUnused translations:\n${unusedTranslations.join(', ')}`;
}
const missingTranslations = difference(defaultMessagesIds, localizedMessagesIds);
if (missingTranslations.length > 0) {
errorMessage += `\nMissing translations:\n${missingTranslations.join(', ')}`;
}
if (errorMessage) {
throw createFailError(errorMessage);
}
}
function groupMessagesByNamespace(localizedMessagesMap) {
const localizedMessagesByNamespace = new Map();
const knownNamespaces = Object.keys(paths);
for (const [messageId, messageValue] of localizedMessagesMap) {
const namespace = knownNamespaces.find(key => messageId.startsWith(`${key}.`));
if (!namespace) {
throw createFailError(`Unknown namespace in id ${messageId}.`);
}
if (!localizedMessagesByNamespace.has(namespace)) {
localizedMessagesByNamespace.set(namespace, []);
}
localizedMessagesByNamespace
.get(namespace)
.push([messageId, { message: messageValue.text || messageValue }]);
}
return localizedMessagesByNamespace;
}
async function writeMessages(localizedMessagesByNamespace, fileName, formats, log) {
for (const [namespace, messages] of localizedMessagesByNamespace) {
const destPath = path.resolve(paths[namespace], 'translations');
try {
await accessAsync(destPath);
} catch (_) {
await makeDirAsync(destPath);
}
const writePath = path.resolve(destPath, fileName);
await writeFileAsync(writePath, serializeToJson(messages, formats));
log.success(`Translations have been integrated to ${normalizePath(writePath)}`);
}
}
export async function integrateLocaleFiles(filePath, log) {
const reporter = new ErrorReporter();
const defaultMessagesMap = await getDefaultMessagesMap(['.'], { paths, exclude }, reporter);
const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString());
if (!localizedMessages.formats) {
throw createFailError(`Locale file should contain formats object.`);
}
const localizedMessagesMap = new Map(Object.entries(localizedMessages.messages));
verifyMessages(localizedMessagesMap, defaultMessagesMap);
// use basename of filePath to write the same locale name as the source file has
const fileName = path.basename(filePath);
const localizedMessagesByNamespace = groupMessagesByNamespace(localizedMessagesMap);
await writeMessages(localizedMessagesByNamespace, fileName, localizedMessages.formats, log);
}

View file

@ -0,0 +1,105 @@
/*
* 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 path from 'path';
import { verifyMessages, integrateLocaleFiles } from './integrate_locale_files';
import { normalizePath } from './utils';
const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json');
const mockDefaultMessagesMap = new Map([
['plugin-1.message-id-1', 'Message text 1'],
['plugin-1.message-id-2', 'Message text 2'],
['plugin-2.message-id', 'Message text'],
]);
jest.mock('./extract_default_translations.js', () => ({
getDefaultMessagesMap: () => mockDefaultMessagesMap,
}));
jest.mock('../../../.i18nrc.json', () => ({
paths: {
'plugin-1': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1',
'plugin-2': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2',
},
exclude: [],
}));
const utils = require('./utils');
utils.writeFileAsync = jest.fn();
utils.makeDirAsync = jest.fn();
describe('dev/i18n/integrate_locale_files', () => {
describe('verifyMessages', () => {
test('validates localized messages', () => {
const localizedMessagesMap = new Map([
['plugin-1.message-id-1', 'Translated text 1'],
['plugin-1.message-id-2', 'Translated text 2'],
['plugin-2.message-id', 'Translated text'],
]);
expect(() => verifyMessages(localizedMessagesMap, mockDefaultMessagesMap)).not.toThrow();
});
test('throws an error for unused id and missing id', () => {
const localizedMessagesMapWithMissingMessage = new Map([
['plugin-1.message-id-1', 'Translated text 1'],
['plugin-2.message-id', 'Translated text'],
]);
const localizedMessagesMapWithUnusedMessage = new Map([
['plugin-1.message-id-1', 'Translated text 1'],
['plugin-1.message-id-2', 'Translated text 2'],
['plugin-1.message-id-3', 'Translated text 3'],
['plugin-2.message-id', 'Translated text'],
]);
const localizedMessagesMapWithIdTypo = new Map([
['plugin-1.message-id-1', 'Message text 1'],
['plugin-1.message-id-2', 'Message text 2'],
['plugin-2.message', 'Message text'],
]);
expect(() =>
verifyMessages(localizedMessagesMapWithMissingMessage, mockDefaultMessagesMap)
).toThrowErrorMatchingSnapshot();
expect(() =>
verifyMessages(localizedMessagesMapWithUnusedMessage, mockDefaultMessagesMap)
).toThrowErrorMatchingSnapshot();
expect(() =>
verifyMessages(localizedMessagesMapWithIdTypo, mockDefaultMessagesMap)
).toThrowErrorMatchingSnapshot();
});
});
describe('integrateLocaleFiles', () => {
test('splits locale file by plugins and writes them into the right folders', async () => {
const success = jest.fn();
await integrateLocaleFiles(localePath, { success });
const [[path1, json1], [path2, json2]] = utils.writeFileAsync.mock.calls;
const [[dirPath1], [dirPath2]] = utils.makeDirAsync.mock.calls;
expect([normalizePath(path1), json1]).toMatchSnapshot();
expect([normalizePath(path2), json2]).toMatchSnapshot();
expect([normalizePath(dirPath1), normalizePath(dirPath2)]).toMatchSnapshot();
});
});
});

View file

@ -19,10 +19,10 @@
import { i18n } from '@kbn/i18n';
export function serializeToJson(defaultMessages) {
const resultJsonObject = { formats: i18n.formats, messages: {} };
export function serializeToJson(messages, formats = i18n.formats) {
const resultJsonObject = { formats, messages: {} };
for (const [mapKey, mapValue] of defaultMessages) {
for (const [mapKey, mapValue] of messages) {
if (mapValue.description) {
resultJsonObject.messages[mapKey] = { text: mapValue.message, comment: mapValue.description };
} else {

View file

@ -21,7 +21,7 @@ import { serializeToJson } from './json';
describe('dev/i18n/serializers/json', () => {
test('should serialize default messages to JSON', () => {
const messages = new Map([
const messages = [
['plugin1.message.id-1', { message: 'Message text 1 ' }],
[
'plugin2.message.id-2',
@ -30,7 +30,7 @@ describe('dev/i18n/serializers/json', () => {
description: 'Message description',
},
],
]);
];
expect(serializeToJson(messages)).toMatchSnapshot();
});

View file

@ -22,15 +22,15 @@ import { i18n } from '@kbn/i18n';
const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g;
export function serializeToJson5(defaultMessages) {
export function serializeToJson5(messages, formats = i18n.formats) {
// .slice(0, -4): remove closing curly braces from json to append messages
let jsonBuffer = Buffer.from(
JSON5.stringify({ formats: i18n.formats, messages: {} }, { quote: `'`, space: 2 })
JSON5.stringify({ formats, messages: {} }, { quote: `'`, space: 2 })
.slice(0, -4)
.concat('\n')
);
for (const [mapKey, mapValue] of defaultMessages) {
for (const [mapKey, mapValue] of messages) {
const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2');
const formattedDescription = mapValue.description
? mapValue.description.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2')

View file

@ -21,7 +21,7 @@ import { serializeToJson5 } from './json5';
describe('dev/i18n/serializers/json5', () => {
test('should serialize default messages to JSON5', () => {
const messages = new Map([
const messages = [
[
'plugin1.message.id-1',
{
@ -35,7 +35,7 @@ describe('dev/i18n/serializers/json5', () => {
description: 'Message description',
},
],
]);
];
expect(serializeToJson5(messages).toString()).toMatchSnapshot();
});

View file

@ -31,6 +31,8 @@ import {
import fs from 'fs';
import glob from 'glob';
import { promisify } from 'util';
import normalize from 'normalize-path';
import path from 'path';
import chalk from 'chalk';
import parser from 'intl-messageformat-parser';
@ -44,8 +46,14 @@ const HTML_KEY_PREFIX = 'html_';
export const readFileAsync = promisify(fs.readFile);
export const writeFileAsync = promisify(fs.writeFile);
export const makeDirAsync = promisify(fs.mkdir);
export const accessAsync = promisify(fs.access);
export const globAsync = promisify(glob);
export function normalizePath(inputPath) {
return normalize(path.relative('.', inputPath));
}
export function difference(left = [], right = []) {
return left.filter(value => !right.includes(value));
}
@ -174,8 +182,8 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI
return;
}
const valuesKeys = prefixedValuesKeys.map(
key => (key.startsWith(HTML_KEY_PREFIX) ? key.slice(HTML_KEY_PREFIX.length) : key)
const valuesKeys = prefixedValuesKeys.map(key =>
key.startsWith(HTML_KEY_PREFIX) ? key.slice(HTML_KEY_PREFIX.length) : key
);
let defaultMessageAst;
@ -284,7 +292,21 @@ export function extractValuesKeysFromNode(node, messageId) {
throw createFailError(`"values" value should be an object expression ("${messageId}").`);
}
return node.properties.map(
property => (isStringLiteral(property.key) ? property.key.value : property.key.name)
return node.properties.map(property =>
isStringLiteral(property.key) ? property.key.value : property.key.name
);
}
export class ErrorReporter {
errors = [];
withContext(context) {
return { report: error => this.report(error, context) };
}
report(error, context) {
this.errors.push(
`${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(context.name)}\n${error}`
);
}
}

View file

@ -27,6 +27,7 @@ import {
formatJSString,
checkValuesProperty,
createParserErrorMessage,
normalizePath,
extractMessageValueFromNode,
} from './utils';
@ -107,6 +108,10 @@ describe('i18n utils', () => {
}
});
test('should normalizePath', () => {
expect(normalizePath(__dirname)).toMatchSnapshot();
});
test('should validate conformity of "values" and "defaultMessage"', () => {
const valuesKeys = ['url', 'username', 'password'];
const defaultMessage = 'Test message with {username}, {password} and [markdown link]({url}).';

View file

@ -24,3 +24,5 @@ bluebird.Promise.setScheduler(function (fn) { global.setImmediate.call(global, f
const MutationObserver = require('mutation-observer');
Object.defineProperty(window, 'MutationObserver', { value: MutationObserver });
require('whatwg-fetch');

View file

@ -42,6 +42,8 @@ export function runMochaCli() {
// check that we aren't leaking any globals
process.argv.push('--check-leaks');
// prevent globals injected from canvas plugins from triggering leak check
process.argv.push('--globals', 'core,regeneratorRuntime,_');
// ensure that mocha requires the setup_node_env script
process.argv.push('--require', require.resolve('../../setup_node_env'));

View file

@ -22,17 +22,36 @@ import Listr from 'listr';
import { resolve } from 'path';
import { run, createFailError } from './run';
import config from '../../.i18nrc.json';
import {
filterPaths,
extractMessagesFromPathToMap,
writeFileAsync,
readFileAsync,
serializeToJson,
serializeToJson5,
ErrorReporter,
normalizePath,
} from './i18n/';
run(async ({ flags: { path, output, 'output-format': outputFormat } }) => {
run(async ({ flags: { path, output, 'output-format': outputFormat, include = [] } }) => {
const paths = Array.isArray(path) ? path : [path || './'];
const filteredPaths = filterPaths(paths);
const additionalI18nConfigPaths = Array.isArray(include) ? include : [include];
const mergedConfig = { exclude: [], ...config };
for (const configPath of additionalI18nConfigPaths) {
const additionalConfig = JSON.parse(await readFileAsync(resolve(configPath)));
for (const [pathNamespace, pathValue] of Object.entries(additionalConfig.paths)) {
mergedConfig.paths[pathNamespace] = normalizePath(resolve(configPath, '..', pathValue));
}
for (const exclude of additionalConfig.exclude || []) {
mergedConfig.exclude.push(normalizePath(resolve(configPath, '..', exclude)));
}
}
const filteredPaths = filterPaths(paths, mergedConfig.paths);
if (filteredPaths.length === 0) {
throw createFailError(
@ -41,22 +60,51 @@ None of input paths is available for extraction or validation. See .i18nrc.json.
);
}
const reporter = new ErrorReporter();
const list = new Listr(
filteredPaths.map(filteredPath => ({
task: messages => extractMessagesFromPathToMap(filteredPath, messages),
task: async messages => {
const initialErrorsNumber = reporter.errors.length;
// Return result if no new errors were reported for this path.
const result = await extractMessagesFromPathToMap(
filteredPath,
messages,
mergedConfig,
reporter
);
if (reporter.errors.length === initialErrorsNumber) {
return result;
}
// throw an empty error to make listr mark the task as failed without any message
throw new Error('');
},
title: filteredPath,
}))
})),
{
exitOnError: false,
}
);
// messages shouldn't be extracted to a file if output is not supplied
const messages = await list.run(new Map());
if (!output || !messages.size) {
return;
try {
// messages shouldn't be extracted to a file if output is not supplied
const messages = await list.run(new Map());
if (!output || !messages.size) {
return;
}
const sortedMessages = [...messages].sort(([key1], [key2]) => key1.localeCompare(key2));
await writeFileAsync(
resolve(output, 'en.json'),
outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages)
);
} catch (error) {
if (error.name === 'ListrError' && reporter.errors.length) {
throw createFailError(reporter.errors.join('\n\n'));
}
throw error;
}
const sortedMessages = [...messages].sort(([key1], [key2]) => key1.localeCompare(key2));
await writeFileAsync(
resolve(output, 'en.json'),
outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages)
);
});

View file

@ -0,0 +1,37 @@
/*
* 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 chalk from 'chalk';
import { createFailError, run } from './run';
import { integrateLocaleFiles } from './i18n/integrate_locale_files';
run(async ({ flags: { path }, log }) => {
if (!path || typeof path === 'boolean') {
throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} --path option isn't provided.`);
}
if (Array.isArray(path)) {
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} --path should be specified only once`
);
}
await integrateLocaleFiles(path, log);
});

28
src/dev/run_sasslint.js Normal file
View file

@ -0,0 +1,28 @@
/*
* 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 { resolve } from 'path';
process.argv.push('--no-exit'); // don't exit after encountering a rule error
process.argv.push('--verbose'); // print results
process.argv.push('--max-warnings', '0'); // return nonzero exit code on any warnings
process.argv.push('--config', resolve(__dirname, '..', '..', '.sass-lint.yml')); // configuration file
// common-js is required so that logic before this executes before loading sass-lint
require('sass-lint/bin/sass-lint');

View file

@ -17,8 +17,14 @@
* under the License.
*/
import { resolve } from 'path';
import {
resolve,
dirname,
relative
} from 'path';
import {
stat,
rename,
createReadStream,
createWriteStream
@ -38,28 +44,38 @@ import {
createFormatArchiveStreams,
} from '../lib';
export async function rebuildAllAction({ dataDir, log }) {
const archiveNames = await readDirectory(dataDir);
async function isDirectory(path) {
const stats = await fromNode(cb => stat(path, cb));
return stats.isDirectory();
}
for (const name of archiveNames) {
const inputDir = resolve(dataDir, name);
const files = prioritizeMappings(await readDirectory(inputDir));
for (const filename of files) {
log.info('[%s] Rebuilding %j', name, filename);
export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) {
const childNames = prioritizeMappings(await readDirectory(dataDir));
for (const childName of childNames) {
const childPath = resolve(dataDir, childName);
const path = resolve(inputDir, filename);
const gzip = isGzip(path);
const tempFile = path + (gzip ? '.rebuilding.gz' : '.rebuilding');
await createPromiseFromStreams([
createReadStream(path),
...createParseArchiveStreams({ gzip }),
...createFormatArchiveStreams({ gzip }),
createWriteStream(tempFile),
]);
await fromNode(cb => rename(tempFile, path, cb));
log.info('[%s] Rebuilt %j', name, filename);
if (await isDirectory(childPath)) {
await rebuildAllAction({
dataDir: childPath,
log,
rootDir,
});
continue;
}
const archiveName = dirname(relative(rootDir, childPath));
log.info(`${archiveName} Rebuilding ${childName}`);
const gzip = isGzip(childPath);
const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding');
await createPromiseFromStreams([
createReadStream(childPath),
...createParseArchiveStreams({ gzip }),
...createFormatArchiveStreams({ gzip }),
createWriteStream(tempFile),
]);
await fromNode(cb => rename(tempFile, childPath, cb));
log.info(`${archiveName} Rebuilt ${childName}`);
}
}

View file

@ -25,7 +25,7 @@ import { transformDeprecations } from './transform_deprecations';
const cache = new WeakMap();
async function getSettingsFromFile(log, path, settingOverrides) {
const configModule = require(path);
const configModule = require(path); // eslint-disable-line import/no-dynamic-require
const configProvider = configModule.__esModule
? configModule.default
: configModule;

View file

@ -48,7 +48,7 @@ export const loadTestFiles = ({ mocha, log, lifecycle, providers, paths, exclude
loadTracer(path, `testFile[${path}]`, () => {
log.verbose('Loading test file %s', path);
const testModule = require(path);
const testModule = require(path); // eslint-disable-line import/no-dynamic-require
const testProvider = testModule.__esModule
? testModule.default
: testModule;

View file

@ -27,7 +27,7 @@ export function resolveApi(senseVersion, apis, h) {
{
if (KNOWN_APIS.includes(name)) {
// for now we ignore sense_version. might add it in the api name later
const api = require('./' + name);
const api = require('./' + name); // eslint-disable-line import/no-dynamic-require
result[name] = api.asJson();
}
}

View file

@ -19,7 +19,7 @@
role="option"
ng-repeat="req in history.reqs"
id="historyReq{{$index}}"
ng-class="{ conHistory__req--selected: history.viewingReq === req }"
ng-class="{ 'conHistory__req-selected': history.viewingReq === req }"
ng-click="history.selectedReq = req; history.viewingReq = req; history.selectedIndex = $index"
ng-mouseenter="history.viewingReq = req"
ng-mouseleave="history.viewingReq = history.selectedReq"

View file

@ -47,7 +47,7 @@ export const vega = () => ({
const response = await vegaRequestHandler({
timeRange: get(context, 'timeRange', null),
query: get(context, 'q', null),
query: get(context, 'query', null),
filters: get(context, 'filters', null),
visParams: { spec: args.spec },
forceFetch: true

View file

@ -25,7 +25,6 @@ import { functions } from './functions';
const basePath = chrome.getBasePath();
const types = {
commonFunctions: functionsRegistry,
browserFunctions: functionsRegistry,
types: typesRegistry
};

View file

@ -83,7 +83,7 @@ exports[`after fetch initialFilter 1`] = `
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
fill={true}
href="#/dashboard"
iconSide="left"
type="button"
@ -333,7 +333,7 @@ exports[`after fetch renders table rows 1`] = `
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
fill={true}
href="#/dashboard"
iconSide="left"
type="button"
@ -501,7 +501,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
<EuiButton
color="primary"
data-test-subj="newDashboardLink"
fill={false}
fill={true}
href="#/dashboard"
iconSide="left"
type="button"

View file

@ -522,6 +522,7 @@ class DashboardListingUi extends React.Component {
<EuiButton
href={`#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`}
data-test-subj="newDashboardLink"
fill
>
<FormattedMessage
id="kbn.dashboard.listing.createNewDashboardButtonLabel"

View file

@ -564,6 +564,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful
>
<path
d="M6.5 12a.502.502 0 0 1-.354-.146l-4-4a.502.502 0 0 1 .708-.708L6.5 10.793l6.646-6.647a.502.502 0 0 1 .708.708l-7 7A.502.502 0 0 1 6.5 12"
fillRule="evenodd"
/>
</svg>
</check>

View file

@ -287,7 +287,6 @@ exports[`NewVisModal should render as expected 1`] = `
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<svg
aria-hidden="true"
@ -298,17 +297,9 @@ exports[`NewVisModal should render as expected 1`] = `
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<path
d="M7.293 8l-4.147 4.146a.5.5 0 0 0 .708.708L8 8.707l4.146 4.147a.5.5 0 0 0 .708-.708L8.707 8l4.147-4.146a.5.5 0 0 0-.708-.708L8 7.293 3.854 3.146a.5.5 0 1 0-.708.708L7.293 8z"
id="cross-a"
/>
</defs>
<use
fillRule="nonzero"
xlinkHref="#cross-a"
<path
d="M7.293 8L3.146 3.854a.5.5 0 1 1 .708-.708L8 7.293l4.146-4.147a.5.5 0 0 1 .708.708L8.707 8l4.147 4.146a.5.5 0 0 1-.708.708L8 8.707l-4.146 4.147a.5.5 0 0 1-.708-.708L7.293 8z"
/>
</svg>
</cross>

File diff suppressed because one or more lines are too long

View file

@ -7,7 +7,7 @@
"title": "Services [APM]",
"uiStateJSON": "{\"vis\": {\"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}}}}",
"version": 1,
"visState": "{\"type\": \"table\", \"title\": \"Services [APM]\", \"aggs\": [{\"id\": \"1\", \"type\": \"avg\", \"enabled\": true, \"params\": {\"customLabel\": \"Avg. Trans. Time\", \"field\": \"transaction.duration.us\"}, \"schema\": \"metric\"}, {\"id\": \"3\", \"type\": \"percentiles\", \"enabled\": true, \"params\": {\"customLabel\": \"Trans. Time\", \"field\": \"transaction.duration.us\", \"percents\": [95]}, \"schema\": \"metric\"}, {\"id\": \"4\", \"type\": \"cardinality\", \"enabled\": true, \"params\": {\"customLabel\": \"Total Transactions\", \"field\": \"transaction.id\"}, \"schema\": \"metric\"}, {\"id\": \"6\", \"type\": \"cardinality\", \"enabled\": true, \"params\": {\"customLabel\": \"Errors\", \"field\": \"error.id\"}, \"schema\": \"metric\"}, {\"id\": \"5\", \"type\": \"top_hits\", \"enabled\": true, \"params\": {\"sortOrder\": \"desc\", \"customLabel\": \"-\", \"size\": 1, \"field\": \"view errors\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}, \"schema\": \"metric\"}, {\"id\": \"2\", \"type\": \"terms\", \"enabled\": true, \"params\": {\"field\": \"context.service.name\", \"orderBy\": \"1\", \"size\": 1000, \"order\": \"desc\"}, \"schema\": \"bucket\"}], \"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}, \"showPartialRows\": false, \"showTotal\": false, \"totalFunc\": \"sum\", \"perPage\": 10, \"showMeticsAtAllLevels\": false}}"
"visState": "{\"type\": \"table\", \"title\": \"Services [APM]\", \"aggs\": [{\"id\": \"1\", \"type\": \"avg\", \"enabled\": true, \"params\": {\"customLabel\": \"Avg. Trans. Time (us)\", \"field\": \"transaction.duration.us\"}, \"schema\": \"metric\"}, {\"id\": \"3\", \"type\": \"percentiles\", \"enabled\": true, \"params\": {\"customLabel\": \"Trans. Time (us)\", \"field\": \"transaction.duration.us\", \"percents\": [95]}, \"schema\": \"metric\"}, {\"id\": \"4\", \"type\": \"cardinality\", \"enabled\": true, \"params\": {\"customLabel\": \"Total Transactions\", \"field\": \"transaction.id\"}, \"schema\": \"metric\"}, {\"id\": \"6\", \"type\": \"cardinality\", \"enabled\": true, \"params\": {\"customLabel\": \"Errors\", \"field\": \"error.id\"}, \"schema\": \"metric\"}, {\"id\": \"5\", \"type\": \"top_hits\", \"enabled\": true, \"params\": {\"sortOrder\": \"desc\", \"customLabel\": \"-\", \"size\": 1, \"field\": \"view errors\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}, \"schema\": \"metric\"}, {\"id\": \"2\", \"type\": \"terms\", \"enabled\": true, \"params\": {\"field\": \"context.service.name\", \"orderBy\": \"1\", \"size\": 1000, \"order\": \"desc\"}, \"schema\": \"bucket\"}], \"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}, \"showPartialRows\": false, \"showTotal\": false, \"totalFunc\": \"sum\", \"perPage\": 10, \"showMeticsAtAllLevels\": false}}"
},
"id": "1ffc5e20-7827-11e7-8c47-65b845b5cfb3",
"type": "visualization",
@ -162,7 +162,7 @@
"title": "Top Transactions for Time Period [APM]",
"uiStateJSON": "{\"vis\": {\"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}}}}",
"version": 1,
"visState": "{\"type\": \"table\", \"title\": \"Top Transactions for Time Period [APM]\", \"aggs\": [{\"id\": \"2\", \"type\": \"terms\", \"enabled\": true, \"schema\": \"bucket\", \"params\": {\"customLabel\": \"Transaction\", \"field\": \"transaction.name.keyword\", \"orderBy\": \"1\", \"size\": 1000, \"order\": \"desc\"}}, {\"id\": \"5\", \"type\": \"top_hits\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Type\", \"sortOrder\": \"desc\", \"size\": 1, \"field\": \"transaction.type\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}}, {\"id\": \"1\", \"type\": \"avg\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Avg. Resp Time (ms)\", \"field\": \"transaction.duration.us\"}}, {\"id\": \"3\", \"type\": \"percentiles\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Resp Time (ms)\", \"field\": \"transaction.duration.us\", \"percents\": [95]}}, {\"id\": \"4\", \"type\": \"top_hits\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"View Spans\", \"sortOrder\": \"desc\", \"size\": 1, \"field\": \"transaction.id\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}}], \"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}, \"showPartialRows\": false, \"showTotal\": false, \"totalFunc\": \"sum\", \"perPage\": 25, \"showMeticsAtAllLevels\": false}}"
"visState": "{\"type\": \"table\", \"title\": \"Top Transactions for Time Period [APM]\", \"aggs\": [{\"id\": \"2\", \"type\": \"terms\", \"enabled\": true, \"schema\": \"bucket\", \"params\": {\"customLabel\": \"Transaction\", \"field\": \"transaction.name.keyword\", \"orderBy\": \"1\", \"size\": 1000, \"order\": \"desc\"}}, {\"id\": \"5\", \"type\": \"top_hits\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Type\", \"sortOrder\": \"desc\", \"size\": 1, \"field\": \"transaction.type\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}}, {\"id\": \"1\", \"type\": \"avg\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Avg. Trans. Time (us)\", \"field\": \"transaction.duration.us\"}}, {\"id\": \"3\", \"type\": \"percentiles\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"Trans. Time (us)\", \"field\": \"transaction.duration.us\", \"percents\": [95]}}, {\"id\": \"4\", \"type\": \"top_hits\", \"enabled\": true, \"schema\": \"metric\", \"params\": {\"customLabel\": \"View Spans\", \"sortOrder\": \"desc\", \"size\": 1, \"field\": \"transaction.id\", \"aggregate\": \"concat\", \"sortField\": \"@timestamp\"}}], \"params\": {\"sort\": {\"direction\": null, \"columnIndex\": null}, \"showPartialRows\": false, \"showTotal\": false, \"totalFunc\": \"sum\", \"perPage\": 25, \"showMeticsAtAllLevels\": false}}"
},
"id": "a2e199b0-7820-11e7-8c47-65b845b5cfb3",
"type": "visualization",

View file

@ -42,7 +42,7 @@ const MetricsRequestHandlerProvider = function (Private, Notifier, config, $http
if (panel && panel.id) {
const params = {
timerange: { timezone, ...parsedTimeRange },
filters: [buildEsQuery(undefined, [query], filters, esQueryConfigs)],
filters: [buildEsQuery(undefined, query, filters, esQueryConfigs)],
panels: [panel],
state: uiStateObj
};

View file

@ -25,7 +25,7 @@ import { truncatedColorMaps } from 'ui/vislib/components/color/truncated_colorma
import { mapToLayerWithId } from './util';
import { RegionMapsVisualizationProvider } from './region_map_visualization';
import { Status } from 'ui/vis/update_status';
import { ORIGIN } from '../../../../core_plugins/ems_util/common/origin';
import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin';
VisTypesRegistryProvider.register(function RegionMapProvider(Private, regionmapsConfig, config, i18n) {

View file

@ -22,7 +22,7 @@ import { toastNotifications } from 'ui/notify';
import regionMapVisParamsTemplate from './region_map_vis_params.html';
import { mapToLayerWithId } from './util';
import '../../tile_map/public/editors/wms_options';
import { ORIGIN } from '../../../../core_plugins/ems_util/common/origin';
import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin';
uiModules.get('kibana/region_map')
.directive('regionMapVisParams', function (serviceSettings, regionmapsConfig) {

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import { ORIGIN } from '../../../../core_plugins/ems_util/common/origin';
import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin';
export function mapToLayerWithId(prefix, layer) {
const clonedLayer = _.cloneDeep(layer);

View file

@ -19,12 +19,13 @@
import MarkdownIt from 'markdown-it';
import _ from 'lodash';
import { modifyUrl } from '../../../core/public/utils';
import { TMSService } from './tms_service';
import { FileLayer } from './file_layer';
import fetch from 'node-fetch';
import { format as formatUrl, parse as parseUrl } from 'url';
const extendUrl = (url, props) => (
modifyUrl(url, parsed => _.merge(parsed, props))
modifyUrlLocal(url, parsed => _.merge(parsed, props))
);
const markdownIt = new MarkdownIt({
@ -32,6 +33,49 @@ const markdownIt = new MarkdownIt({
linkify: true
});
/**
* plugins cannot have upstream dependencies on core/*-kibana.
* Work-around by copy-pasting modifyUrl routine here.
* @param url
* @param block
*/
function modifyUrlLocal(url, block) {
const parsed = parseUrl(url, true);
// copy over the most specific version of each
// property. By default, the parsed url includes
// several conflicting properties (like path and
// pathname + search, or search and query) and keeping
// track of which property is actually used when they
// are formatted is harder than necessary
const meaningfulParts = {
protocol: parsed.protocol,
slashes: parsed.slashes,
auth: parsed.auth,
hostname: parsed.hostname,
port: parsed.port,
pathname: parsed.pathname,
query: parsed.query || {},
hash: parsed.hash,
};
// the block modifies the meaningfulParts object, or returns a new one
const modifiedParts = block(meaningfulParts) || meaningfulParts;
// format the modified/replaced meaningfulParts back into a url
return formatUrl({
protocol: modifiedParts.protocol,
slashes: modifiedParts.slashes,
auth: modifiedParts.auth,
hostname: modifiedParts.hostname,
port: modifiedParts.port,
pathname: modifiedParts.pathname,
query: modifiedParts.query,
hash: modifiedParts.hash,
});
}
/**
* Unescape a url template that was escaped by encodeURI() so leaflet

View file

@ -18,6 +18,7 @@
*/
import { resolve } from 'path';
import * as emsClient from './common/ems_client';
export default function (kibana) {
@ -25,6 +26,11 @@ export default function (kibana) {
uiExports: {
visTypes: ['plugins/tile_map/tile_map_vis'],
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
},
init(server) {
server.expose({
ems_client: emsClient
});
}
});
}

View file

@ -43,7 +43,7 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, confi
sheet: [expression],
extended: {
es: {
filter: buildEsQuery(undefined, [query], filters, esQueryConfigs)
filter: buildEsQuery(undefined, query, filters, esQueryConfigs)
}
},
time: _.extend(timeRange, {

View file

@ -17,13 +17,12 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../average`);
import moment from 'moment';
const expect = require('chai').expect;
import _ from 'lodash';
describe(filename, function () {
describe('average.js', function () {
describe('average', function () {
it('fills holes in the data', function () {

View file

@ -17,13 +17,12 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../carry`);
import moment from 'moment';
const expect = require('chai').expect;
import _ from 'lodash';
describe(filename, function () {
describe('carry.js', function () {
it('fills holes in the data', function () {
const data = [
[moment.utc('1980', 'YYYY').valueOf(), 10],

View file

@ -17,12 +17,11 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../load_functions`);
const expect = require('chai').expect;
describe(filename, () => {
describe('load_functions.js', () => {
it('exports a function', () => {
expect(fn).to.be.a('function');
});

View file

@ -25,7 +25,7 @@ import processFunctionDefinition from './process_function_definition';
export default function (directory) {
function getTuple(directory, name) {
return [name, require('../' + directory + '/' + name)];
return [name, require('../' + directory + '/' + name)]; // eslint-disable-line import/no-dynamic-require
}
// Get a list of all files and use the filename as the object key

View file

@ -17,15 +17,14 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../abs`);
import _ from 'lodash';
const expect = require('chai').expect;
const seriesList = require('./fixtures/seriesList.js')();
import invoke from './helpers/invoke_series_fn.js';
describe(filename, function () {
describe('abs.js', function () {
it('should return the positive value of every value', function () {
return invoke(fn, [seriesList]).then(function (result) {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../bars`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('bars.js', () => {
let seriesList;
beforeEach(() => {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../color`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('color.js', () => {
let seriesList;
beforeEach(() => {

View file

@ -17,15 +17,14 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../condition`);
import moment from 'moment';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
import getSeriesList from './helpers/get_single_series_list';
import _ from 'lodash';
describe(filename, function () {
describe('condition.js', function () {
let comparable;
let seriesList;

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../cusum`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('cusum.js', () => {
let seriesList;
beforeEach(() => {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../derivative`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('derivative.js', () => {
let seriesList;
beforeEach(() => {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../divide`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('divide.js', () => {
let seriesList;
beforeEach(() => {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../first`);
const expect = require('chai').expect;
const seriesList = require('./fixtures/seriesList.js')();
import invoke from './helpers/invoke_series_fn.js';
describe(filename, function () {
describe('first.js', function () {
it('should return exactly the data input', function () {
return invoke(fn, [seriesList]).then(function (result) {
expect(result.input[0]).to.eql(result.output);

View file

@ -17,15 +17,14 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../fit`);
import moment from 'moment';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
import getSeriesList from './helpers/get_single_series_list';
import _ from 'lodash';
describe(filename, function () {
describe('fit.js', function () {
describe('should not filter out zeros', function () {
it('all zeros', function () {

View file

@ -17,14 +17,13 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const fn = require(`../hide`);
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
describe(filename, () => {
describe('hide.js', () => {
let seriesList;
beforeEach(() => {

Some files were not shown because too many files have changed in this diff Show more