From cd83db703783aa80b474fbc8caf56c7d9ab46fa0 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Tue, 4 Sep 2018 21:46:59 -0400 Subject: [PATCH 01/11] Fix #22510, dashboard-only mode doesn't display saved searches (#22685) --- x-pack/plugins/dashboard_mode/public/dashboard_viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js index 272174029ded928..ef2929f2ab2ff3f 100644 --- a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js @@ -33,6 +33,7 @@ import 'ui/vislib'; import 'ui/agg_response'; import 'ui/agg_types'; import 'ui/timepicker'; +import 'ui/pager'; import 'leaflet'; import { showAppRedirectNotification } from 'ui/notify'; From 5f4a1c58e88826789a2b558e2b2f8ea56af82a7b Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 08:55:39 +0200 Subject: [PATCH 02/11] Move timezone settings into autoload file (#22623) * Move timezone settings into autoload file * Remove applying setting from timelion * Remove manual set from ML * Remove manual set from monitoring * Remove now obsolete code from embedding test plugin --- src/core_plugins/kibana/public/kibana.js | 3 -- .../kibana/public/kibana_root_controller.js | 34 ------------- src/core_plugins/timelion/public/app.js | 4 -- src/ui/public/autoload/all.js | 1 + src/ui/public/autoload/settings.js | 50 +++++++++++++++++++ .../plugins/visualize_embedding/public/app.js | 28 ----------- .../dashboard_mode/public/dashboard_viewer.js | 4 +- x-pack/plugins/ml/public/app.js | 8 --- .../plugins/monitoring/public/monitoring.js | 4 -- 9 files changed, 52 insertions(+), 84 deletions(-) delete mode 100644 src/core_plugins/kibana/public/kibana_root_controller.js create mode 100644 src/ui/public/autoload/settings.js diff --git a/src/core_plugins/kibana/public/kibana.js b/src/core_plugins/kibana/public/kibana.js index 522b6b708702578..984706556800d67 100644 --- a/src/core_plugins/kibana/public/kibana.js +++ b/src/core_plugins/kibana/public/kibana.js @@ -59,7 +59,6 @@ import 'ui/agg_types'; import 'ui/timepicker'; import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; -import { KibanaRootController } from './kibana_root_controller'; routes.enable(); @@ -68,6 +67,4 @@ routes redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` }); -chrome.setRootController('kibana', KibanaRootController); - uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/core_plugins/kibana/public/kibana_root_controller.js b/src/core_plugins/kibana/public/kibana_root_controller.js deleted file mode 100644 index 830cd0bd16c7b9a..000000000000000 --- a/src/core_plugins/kibana/public/kibana_root_controller.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 moment from 'moment-timezone'; - -export function KibanaRootController($scope, courier, config) { - config.watch('dateFormat:tz', setDefaultTimezone, $scope); - config.watch('dateFormat:dow', setStartDayOfWeek, $scope); - - function setDefaultTimezone(tz) { - moment.tz.setDefault(tz); - } - - function setStartDayOfWeek(day) { - const dow = moment.weekdays().indexOf(day); - moment.updateLocale(moment.locale(), { week: { dow } }); - } -} diff --git a/src/core_plugins/timelion/public/app.js b/src/core_plugins/timelion/public/app.js index ffadf7c9cdb1bc3..316a9704b5c3d3e 100644 --- a/src/core_plugins/timelion/public/app.js +++ b/src/core_plugins/timelion/public/app.js @@ -18,7 +18,6 @@ */ import _ from 'lodash'; -import moment from 'moment-timezone'; import { DocTitleProvider } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; @@ -101,9 +100,6 @@ app.controller('timelion', function ( $scope.page = config.get('timelion:showTutorial', true) ? 1 : 0; $scope.setPage = (page) => $scope.page = page; - // TODO: For some reason the Kibana core doesn't correctly do this for all apps. - moment.tz.setDefault(config.get('dateFormat:tz')); - timefilter.enableAutoRefreshSelector(); timefilter.enableTimeRangeSelector(); diff --git a/src/ui/public/autoload/all.js b/src/ui/public/autoload/all.js index fba21aacd198486..b1aa11cb7249467 100644 --- a/src/ui/public/autoload/all.js +++ b/src/ui/public/autoload/all.js @@ -21,4 +21,5 @@ import './accessibility'; import './modules'; import './directives'; import './filters'; +import './settings'; import './styles'; diff --git a/src/ui/public/autoload/settings.js b/src/ui/public/autoload/settings.js new file mode 100644 index 000000000000000..48505037dffe4c1 --- /dev/null +++ b/src/ui/public/autoload/settings.js @@ -0,0 +1,50 @@ +/* + * 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. + */ + +/** + * Autoload this file if we want some of the top level settings applied to a plugin. + * Currently this file makes sure the following settings are applied globally: + * - dateFormat:tz (meaning the Kibana time zone will be used in your plugin) + * - dateFormat:dow (meaning the Kibana configured start of the week will be used in your plugin) + */ + +import moment from 'moment-timezone'; +import chrome from '../chrome'; + +function setDefaultTimezone(tz) { + moment.tz.setDefault(tz); +} + +function setStartDayOfWeek(day) { + const dow = moment.weekdays().indexOf(day); + moment.updateLocale(moment.locale(), { week: { dow } }); +} + +const uiSettings = chrome.getUiSettingsClient(); + +setDefaultTimezone(uiSettings.get('dateFormat:tz')); +setStartDayOfWeek(uiSettings.get('dateFormat:dow')); + +uiSettings.subscribe(({ key, newValue }) => { + if (key === 'dateFormat:tz') { + setDefaultTimezone(newValue); + } else if (key === 'dateFormat:dow') { + setStartDayOfWeek(newValue); + } +}); diff --git a/test/plugin_functional/plugins/visualize_embedding/public/app.js b/test/plugin_functional/plugins/visualize_embedding/public/app.js index 3f622c946a11592..4463feac27513db 100644 --- a/test/plugin_functional/plugins/visualize_embedding/public/app.js +++ b/test/plugin_functional/plugins/visualize_embedding/public/app.js @@ -37,34 +37,6 @@ import 'uiExports/savedObjectTypes'; import 'uiExports/fieldFormats'; import 'uiExports/search'; -// ----------- TODO Remove once https://github.com/elastic/kibana/pull/22623 is merged - -import moment from 'moment-timezone'; - -function setDefaultTimezone(tz) { - moment.tz.setDefault(tz); -} - -function setStartDayOfWeek(day) { - const dow = moment.weekdays().indexOf(day); - moment.updateLocale(moment.locale(), { week: { dow } }); -} - -const uiSettings = chrome.getUiSettingsClient(); - -setDefaultTimezone(uiSettings.get('dateFormat:tz')); -setStartDayOfWeek(uiSettings.get('dateFormat:dow')); - -uiSettings.subscribe(({ key, newValue }) => { - if (key === 'dateFormat:tz') { - setDefaultTimezone(newValue); - } else if (key === 'dateFormat:dow') { - setStartDayOfWeek(newValue); - } -}); - -// ----------------- END OF REMOVAL ---------- - import { Main } from './components/main'; const app = uiModules.get('apps/firewallDemoPlugin', ['kibana']); diff --git a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js index ef2929f2ab2ff3f..2b6997742280cc5 100644 --- a/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/plugins/dashboard_mode/public/dashboard_viewer.js @@ -38,7 +38,6 @@ import 'leaflet'; import { showAppRedirectNotification } from 'ui/notify'; import { DashboardConstants, createDashboardEditUrl } from 'plugins/kibana/dashboard/dashboard_constants'; -import { KibanaRootController } from 'plugins/kibana/kibana_root_controller'; uiModules.get('kibana') .config(dashboardConfigProvider => dashboardConfigProvider.turnHideWriteControlsOn()); @@ -47,9 +46,8 @@ routes.enable(); routes.otherwise({ redirectTo: defaultUrl() }); chrome - .setRootController('kibana', function ($controller, $scope, courier, config) { + .setRootController('kibana', function () { chrome.showOnlyById('kibana:dashboard'); - $controller(KibanaRootController, { $scope, courier, config }); }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/x-pack/plugins/ml/public/app.js b/x-pack/plugins/ml/public/app.js index ba1d17bdd9b8d83..ad76c56cfdf198e 100644 --- a/x-pack/plugins/ml/public/app.js +++ b/x-pack/plugins/ml/public/app.js @@ -34,14 +34,6 @@ import 'plugins/ml/components/loading_indicator'; import 'plugins/ml/settings'; import uiRoutes from 'ui/routes'; -import moment from 'moment-timezone'; -import { uiModules } from 'ui/modules'; - -const uiModule = uiModules.get('kibana'); -uiModule.run((config) => { - // Set the timezone for moment formatting to that configured in Kibana. - moment.tz.setDefault(config.get('dateFormat:tz')); -}); if (typeof uiRoutes.enable === 'function') { uiRoutes.enable(); diff --git a/x-pack/plugins/monitoring/public/monitoring.js b/x-pack/plugins/monitoring/public/monitoring.js index f564848d02c902e..0e4528ba679c3b5 100644 --- a/x-pack/plugins/monitoring/public/monitoring.js +++ b/x-pack/plugins/monitoring/public/monitoring.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment-timezone'; import uiRoutes from 'ui/routes'; import chrome from 'ui/chrome'; import 'ui/autoload/all'; @@ -20,9 +19,6 @@ import 'plugins/monitoring/views/all'; const uiSettings = chrome.getUiSettingsClient(); -// Allow UTC times to be entered for Absolute Time range in timepicker -moment.tz.setDefault(uiSettings.get('dateFormat:tz')); - // default timepicker default to the last hour uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify({ from: 'now-1h', From 9c01863c0a454ec0eceafa157dc479d4ee1f179f Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 09:35:18 +0200 Subject: [PATCH 03/11] Fix react vis type documentation (#22573) --- .../visualize/development-create-visualization.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/visualize/development-create-visualization.asciidoc b/docs/development/visualize/development-create-visualization.asciidoc index 437b4ff9e5f3d13..f843904fcdc4163 100644 --- a/docs/development/visualize/development-create-visualization.asciidoc +++ b/docs/development/visualize/development-create-visualization.asciidoc @@ -179,7 +179,7 @@ VisTypesRegistryProvider.register(MyNewVisType); [[development-react-visualization-type]] ==== React Visualization Type React visualization type assumes you are using React as your rendering technology. -Just pass in a React component to `visConfig.template`. +Just pass in a React component to `visConfig.component`. The visualization will receive `vis`, `appState`, `updateStatus` and `visData` as props. It also has a `renderComplete` property, which needs to be called once the rendering has completed. @@ -197,7 +197,7 @@ const MyNewVisType = (Private) => { icon: 'my_icon', description: 'Cool new chart', visConfig: { - template: ReactComponent + component: ReactComponent } }); } From 4cf727aa980c7b5596c330c8e7082c097f42caea Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 5 Sep 2018 14:02:15 +0300 Subject: [PATCH 04/11] Add logging to messages validation (#22296) * Add logging and parallelization to messages validation * Refactor dev/i18n * Resolve comments * Remove parallelism and fix tests * Resolve comments --- .../extract_code_messages.test.js.snap | 31 --- .../extract_default_translations.test.js.snap | 179 +++++------------- .../extract_handlebars_messages.test.js.snap | 21 -- .../extract_html_messages.test.js.snap | 31 --- .../extract_i18n_call_messages.test.js.snap | 29 --- .../extract_pug_messages.test.js.snap | 15 -- .../extract_react_messages.test.js.snap | 27 --- src/dev/i18n/extract_default_translations.js | 101 ++-------- .../i18n/extract_default_translations.test.js | 35 +--- .../__snapshots__/code.test.js.snap | 31 +++ .../__snapshots__/handlebars.test.js.snap | 21 ++ .../__snapshots__/html.test.js.snap | 31 +++ .../__snapshots__/i18n_call.test.js.snap | 29 +++ .../extractors/__snapshots__/pug.test.js.snap | 15 ++ .../__snapshots__/react.test.js.snap | 27 +++ .../code.js} | 6 +- .../code.test.js} | 18 +- .../handlebars.js} | 30 +-- .../handlebars.test.js} | 4 +- .../html.js} | 36 +--- .../html.test.js} | 4 +- .../i18n_call.js} | 29 +-- .../i18n_call.test.js} | 6 +- src/dev/i18n/extractors/index.js | 25 +++ .../pug.js} | 4 +- .../pug.test.js} | 4 +- .../react.js} | 38 ++-- .../react.test.js} | 6 +- src/dev/i18n/index.js | 22 +++ .../__snapshots__/json.test.js.snap | 67 +++++++ .../__snapshots__/json5.test.js.snap | 65 +++++++ src/dev/i18n/serializers/index.js | 21 ++ src/dev/i18n/serializers/json.js | 34 ++++ src/dev/i18n/serializers/json.test.js | 37 ++++ src/dev/i18n/serializers/json5.js | 48 +++++ src/dev/i18n/serializers/json5.test.js | 42 ++++ src/dev/run/index.js | 2 +- src/dev/run_i18n_check.js | 47 ++++- 38 files changed, 701 insertions(+), 517 deletions(-) delete mode 100644 src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/code.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/html.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/pug.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/react.test.js.snap rename src/dev/i18n/{extract_code_messages.js => extractors/code.js} (94%) rename src/dev/i18n/{extract_code_messages.test.js => extractors/code.test.js} (89%) rename src/dev/i18n/{extract_handlebars_messages.js => extractors/handlebars.js} (68%) rename src/dev/i18n/{extract_handlebars_messages.test.js => extractors/handlebars.test.js} (95%) rename src/dev/i18n/{extract_html_messages.js => extractors/html.js} (78%) rename src/dev/i18n/{extract_html_messages.test.js => extractors/html.test.js} (94%) rename src/dev/i18n/{extract_i18n_call_messages.js => extractors/i18n_call.js} (64%) rename src/dev/i18n/{extract_i18n_call_messages.test.js => extractors/i18n_call.test.js} (95%) create mode 100644 src/dev/i18n/extractors/index.js rename src/dev/i18n/{extract_pug_messages.js => extractors/pug.js} (91%) rename src/dev/i18n/{extract_pug_messages.test.js => extractors/pug.test.js} (94%) rename src/dev/i18n/{extract_react_messages.js => extractors/react.js} (73%) rename src/dev/i18n/{extract_react_messages.test.js => extractors/react.test.js} (97%) create mode 100644 src/dev/i18n/index.js create mode 100644 src/dev/i18n/serializers/__snapshots__/json.test.js.snap create mode 100644 src/dev/i18n/serializers/__snapshots__/json5.test.js.snap create mode 100644 src/dev/i18n/serializers/index.js create mode 100644 src/dev/i18n/serializers/json.js create mode 100644 src/dev/i18n/serializers/json.test.js create mode 100644 src/dev/i18n/serializers/json5.js create mode 100644 src/dev/i18n/serializers/json5.test.js diff --git a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap deleted file mode 100644 index 507efdfd615955e..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractCodeMessages extracts React, server-side and angular service default messages 1`] = ` -Array [ - Array [ - "kbn.mgmt.id-1", - Object { - "context": undefined, - "message": "Message text 1", - }, - ], - Array [ - "kbn.mgmt.id-2", - Object { - "context": "Message context", - "message": "Message text 2", - }, - ], - Array [ - "kbn.mgmt.id-3", - Object { - "context": undefined, - "message": "Message text 3", - }, - ], -] -`; - -exports[`extractCodeMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractCodeMessages throws on missing defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index 750280b7f8d954a..1a9997dc07dd921 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -1,142 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dev/i18n/extract_default_translations extracts messages to en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } +exports[`dev/i18n/extract_default_translations extracts messages from path to map 1`] = ` +Array [ + Array [ + "plugin_1.id_1", + Object { + "context": undefined, + "message": "Message 1", }, - \\"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\\" - } + ], + Array [ + "plugin_1.id_2", + Object { + "context": "Message context", + "message": "Message 2", }, - \\"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\\" - } - } - }, - \\"plugin_1.id_1\\": \\"Message 1\\", - \\"plugin_1.id_2\\": { - \\"text\\": \\"Message 2\\", - \\"comment\\": \\"Message context\\" - }, - \\"plugin_1.id_3\\": \\"Message 3\\", - \\"plugin_1.id_4\\": \\"Message 4\\", - \\"plugin_1.id_5\\": \\"Message 5\\", - \\"plugin_1.id_6\\": \\"Message 6\\", - \\"plugin_1.id_7\\": \\"Message 7\\" -}" -`; - -exports[`dev/i18n/extract_default_translations injects default formats into en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } + ], + Array [ + "plugin_1.id_3", + Object { + "context": undefined, + "message": "Message 3", + }, + ], + Array [ + "plugin_1.id_4", + Object { + "context": undefined, + "message": "Message 4", + }, + ], + Array [ + "plugin_1.id_5", + Object { + "context": undefined, + "message": "Message 5", + }, + ], + Array [ + "plugin_1.id_6", + Object { + "context": "", + "message": "Message 6", }, - \\"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\\" - } + ], + Array [ + "plugin_1.id_7", + Object { + "context": undefined, + "message": "Message 7", }, - \\"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\\" - } - } - }, - \\"plugin_2.message-id\\": \\"Message text\\" -}" + ], +] `; 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 -Error:  I18N ERROR  There is more than one default message for the same id \\"plugin_3.duplicate_id\\": +Error: There is more than one default message for the same id \\"plugin_3.duplicate_id\\": \\"Message 1\\" and \\"Message 2\\"" `; -exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `" I18N ERROR  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`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See .i18nrc.json for the list of supported namespaces."`; diff --git a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap deleted file mode 100644 index 7c9f72a6921ba66..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_handlebars_messages extracts handlebars default messages 1`] = ` -Array [ - Array [ - "ui.id-1", - Object { - "context": "Message context", - "message": "Message text", - }, - ], -] -`; - -exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `" I18N ERROR  Empty id argument in Handlebars i18n is not allowed."`; - -exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `" I18N ERROR  Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`; - -exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `" I18N ERROR  Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap deleted file mode 100644 index aa6048b92b84ce9..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_html_messages extracts default messages from HTML 1`] = ` -Array [ - Array [ - "kbn.dashboard.id-1", - Object { - "context": "Message context 1", - "message": "Message text 1", - }, - ], - Array [ - "kbn.dashboard.id-2", - Object { - "context": undefined, - "message": "Message text 2", - }, - ], - Array [ - "kbn.dashboard.id-3", - Object { - "context": "Message context 3", - "message": "Message text 3", - }, - ], -] -`; - -exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `" I18N ERROR  Empty \\"i18n-id\\" value in angular directive is not allowed."`; - -exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `" I18N ERROR  Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap deleted file mode 100644 index 13a79e578861ca9..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 2`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `" I18N ERROR  defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`; - -exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; - -exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap deleted file mode 100644 index 16767f882063afc..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractPugMessages extracts messages from pug template 1`] = ` -Array [ - "message-id", - Object { - "context": "Message context", - "message": "Default message", - }, -] -`; - -exports[`extractPugMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; - -exports[`extractPugMessages throws on missing default message 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap deleted file mode 100644 index 2bf17cab30c2827..000000000000000 --- a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_react_messages extractFormattedMessages extracts messages from "" element 1`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `" I18N ERROR  context value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `" I18N ERROR  defaultMessage value should be a string literal (\\"message-id\\")."`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `" I18N ERROR  Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 5bbfaa221b433ad..06d397961e23322 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -18,28 +18,27 @@ */ import path from 'path'; -import { i18n } from '@kbn/i18n'; -import JSON5 from 'json5'; import normalize from 'normalize-path'; import chalk from 'chalk'; -import { extractHtmlMessages } from './extract_html_messages'; -import { extractCodeMessages } from './extract_code_messages'; -import { extractPugMessages } from './extract_pug_messages'; -import { extractHandlebarsMessages } from './extract_handlebars_messages'; -import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { + extractHtmlMessages, + extractCodeMessages, + extractPugMessages, + extractHandlebarsMessages, +} from './extractors'; +import { globAsync, readFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; -import { createFailError } from '../run'; - -const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; +import { createFailError, isFailError } from '../run'; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); + if (targetMap.has(key) && existingValue.message !== value.message) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -There is more than one default message for the same id "${key}": + throw createFailError(`There is more than one default message for the same id "${key}": "${existingValue.message}" and "${value.message}"`); } + targetMap.set(key, value); } @@ -47,7 +46,7 @@ function normalizePath(inputPath) { return normalize(path.relative('.', inputPath)); } -function filterPaths(inputPaths) { +export function filterPaths(inputPaths) { const availablePaths = Object.values(paths); const pathsForExtraction = new Set(); @@ -79,9 +78,8 @@ export function validateMessageNamespace(id, filePath) { ); if (!id.startsWith(`${expectedNamespace}.`)) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -Expected "${id}" id to have "${expectedNamespace}" namespace. \ -See i18nrc.json for the list of supported namespaces.`); + throw createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ +See .i18nrc.json for the list of supported namespaces.`); } } @@ -133,72 +131,15 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { addMessageToMap(targetMap, id, value); } } catch (error) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` - ); + if (isFailError(error)) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` + ); + } + + throw error; } } }) ); } - -function serializeToJson5(defaultMessages) { - // .slice(0, -1): remove closing curly brace from json to append messages - let jsonBuffer = Buffer.from( - JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) - ); - - for (const [mapKey, mapValue] of defaultMessages) { - const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); - const formattedContext = mapValue.context - ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') - : ''; - - jsonBuffer = Buffer.concat([ - jsonBuffer, - Buffer.from(` '${mapKey}': '${formattedMessage}',`), - Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), - ]); - } - - // append previously removed closing curly brace - jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); - - return jsonBuffer; -} - -function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; - - for (const [mapKey, mapValue] of defaultMessages) { - if (mapValue.context) { - resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; - } else { - resultJsonObject[mapKey] = mapValue.message; - } - } - - return JSON.stringify(resultJsonObject, undefined, 2); -} - -export async function extractDefaultTranslations({ paths, output, outputFormat }) { - const defaultMessagesMap = new Map(); - - for (const inputPath of filterPaths(paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); - } - - // messages shouldn't be extracted to a file if output is not supplied - if (!output || !defaultMessagesMap.size) { - return; - } - - const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => - key1.localeCompare(key2) - ); - - await writeFileAsync( - path.resolve(output, 'en.json'), - outputFormat === 'json5' ? serializeToJson5(defaultMessages) : serializeToJson(defaultMessages) - ); -} diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index 57e89f731fc6afc..b89361e87fcf7ea 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -20,7 +20,7 @@ import path from 'path'; import { - extractDefaultTranslations, + extractMessagesFromPathToMap, validateMessageNamespace, } from './extract_default_translations'; @@ -40,42 +40,21 @@ jest.mock('../../../.i18nrc.json', () => ({ exclude: [], })); -const utils = require('./utils'); -utils.writeFileAsync = jest.fn(); - describe('dev/i18n/extract_default_translations', () => { - test('extracts messages to en.json', async () => { + test('extracts messages from path to map', async () => { const [pluginPath] = pluginsPaths; + const resultMap = new Map(); - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); - - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); - }); - - test('injects default formats into en.json', async () => { - const [, pluginPath] = pluginsPaths; - - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); + await extractMessagesFromPathToMap(pluginPath, resultMap); - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); + expect([...resultMap].sort()).toMatchSnapshot(); }); test('throws on id collision', async () => { const [, , pluginPath] = pluginsPaths; + await expect( - extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) + extractMessagesFromPathToMap(pluginPath, new Map()) ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/src/dev/i18n/extractors/__snapshots__/code.test.js.snap b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap new file mode 100644 index 000000000000000..26c621e32964dc8 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/code extracts React, server-side and angular service default messages 1`] = ` +Array [ + Array [ + "kbn.mgmt.id-1", + Object { + "context": undefined, + "message": "Message text 1", + }, + ], + Array [ + "kbn.mgmt.id-2", + Object { + "context": "Message context", + "message": "Message text 2", + }, + ], + Array [ + "kbn.mgmt.id-3", + Object { + "context": undefined, + "message": "Message text 3", + }, + ], +] +`; + +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 missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap new file mode 100644 index 000000000000000..7ca5178c7538ff4 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/handlebars extracts handlebars default messages 1`] = ` +Array [ + Array [ + "ui.id-1", + Object { + "context": "Message context", + "message": "Message text", + }, + ], +] +`; + +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 missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"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 properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap new file mode 100644 index 000000000000000..982341c8800740a --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/html extracts default messages from HTML 1`] = ` +Array [ + Array [ + "kbn.dashboard.id-1", + Object { + "context": "Message context 1", + "message": "Message text 1", + }, + ], + Array [ + "kbn.dashboard.id-2", + Object { + "context": undefined, + "message": "Message text 2", + }, + ], + Array [ + "kbn.dashboard.id-3", + Object { + "context": "Message context 3", + "message": "Message text 3", + }, + ], +] +`; + +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 missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap new file mode 100644 index 000000000000000..c9bf2f07716d4c9 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 2`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`; + +exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap new file mode 100644 index 000000000000000..c95fb0d149cd036 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/pug extracts messages from pug template 1`] = ` +Array [ + "message-id", + Object { + "context": "Message context", + "message": "Default message", + }, +] +`; + +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 missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/react.test.js.snap b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap new file mode 100644 index 000000000000000..6a51a5e2160043e --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/react extractFormattedMessages extracts messages from "" element 1`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_code_messages.js b/src/dev/i18n/extractors/code.js similarity index 94% rename from src/dev/i18n/extract_code_messages.js rename to src/dev/i18n/extractors/code.js index e7b72e6efa162e3..e7477b17e2759ec 100644 --- a/src/dev/i18n/extract_code_messages.js +++ b/src/dev/i18n/extractors/code.js @@ -26,9 +26,9 @@ import { isMemberExpression, } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; /** * Detect Intl.formatMessage() function call (React). diff --git a/src/dev/i18n/extract_code_messages.test.js b/src/dev/i18n/extractors/code.test.js similarity index 89% rename from src/dev/i18n/extract_code_messages.test.js rename to src/dev/i18n/extractors/code.test.js index 5b3e64ebb4f0788..3cc7d39f78d4003 100644 --- a/src/dev/i18n/extract_code_messages.test.js +++ b/src/dev/i18n/extractors/code.test.js @@ -24,8 +24,8 @@ import { extractCodeMessages, isFormattedMessageElement, isIntlFormatMessageFunction, -} from './extract_code_messages'; -import { traverseNodes } from './utils'; +} from './code'; +import { traverseNodes } from '../utils'; const extractCodeMessagesSource = Buffer.from(` i18n('kbn.mgmt.id-1', { defaultMessage: 'Message text 1' }); @@ -65,7 +65,7 @@ function f() { } `; -describe('extractCodeMessages', () => { +describe('dev/i18n/extractors/code', () => { test('extracts React, server-side and angular service default messages', () => { const actual = Array.from(extractCodeMessages(extractCodeMessagesSource)); expect(actual.sort()).toMatchSnapshot(); @@ -84,12 +84,16 @@ describe('extractCodeMessages', () => { describe('isIntlFormatMessageFunction', () => { test('detects intl.formatMessage call expression', () => { - const callExpressionNodes = [...traverseNodes(parse(intlFormatMessageSource).program.body)].filter( - node => isCallExpression(node) - ); + const callExpressionNodes = [ + ...traverseNodes(parse(intlFormatMessageSource).program.body), + ].filter(node => isCallExpression(node)); expect(callExpressionNodes).toHaveLength(4); - expect(callExpressionNodes.every(callExpressionNode => isIntlFormatMessageFunction(callExpressionNode))).toBe(true); + expect( + callExpressionNodes.every(callExpressionNode => + isIntlFormatMessageFunction(callExpressionNode) + ) + ).toBe(true); }); }); diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extractors/handlebars.js similarity index 68% rename from src/dev/i18n/extract_handlebars_messages.js rename to src/dev/i18n/extractors/handlebars.js index 1aabdb61e2be139..7c57c8d0da731fb 100644 --- a/src/dev/i18n/extract_handlebars_messages.js +++ b/src/dev/i18n/extractors/handlebars.js @@ -17,10 +17,8 @@ * under the License. */ -import chalk from 'chalk'; - -import { formatJSString } from './utils'; -import { createFailError } from '../run'; +import { formatJSString } from '../utils'; +import { createFailError } from '../../run'; const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; @@ -39,29 +37,22 @@ export function* extractHandlebarsMessages(buffer) { } if (tokens.length !== 3) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.` - ); + throw createFailError(`Wrong number of arguments for handlebars i18n call.`); } if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } const messageId = formatJSString(idString.slice(1, -1)); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.` - ); + throw createFailError(`Empty id argument in Handlebars i18n is not allowed.`); } if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Properties string in Handlebars i18n should be a string literal ("${messageId}").` + `Properties string in Handlebars i18n should be a string literal ("${messageId}").` ); } @@ -70,15 +61,13 @@ Properties string in Handlebars i18n should be a string literal ("${messageId}") if (typeof message !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in Handlebars i18n should be a string ("${messageId}").` + `defaultMessage value in Handlebars i18n should be a string ("${messageId}").` ); } if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` + `Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` ); } @@ -86,8 +75,7 @@ Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` if (context != null && typeof context !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Context value in Handlebars i18n should be a string ("${messageId}").` + `Context value in Handlebars i18n should be a string ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_handlebars_messages.test.js b/src/dev/i18n/extractors/handlebars.test.js similarity index 95% rename from src/dev/i18n/extract_handlebars_messages.test.js rename to src/dev/i18n/extractors/handlebars.test.js index e4f53852b6cc3a4..52365989bd7fdc8 100644 --- a/src/dev/i18n/extract_handlebars_messages.test.js +++ b/src/dev/i18n/extractors/handlebars.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractHandlebarsMessages } from './extract_handlebars_messages'; +import { extractHandlebarsMessages } from './handlebars'; -describe('dev/i18n/extract_handlebars_messages', () => { +describe('dev/i18n/extractors/handlebars', () => { test('extracts handlebars default messages', () => { const source = Buffer.from(`\ window.onload = function () { diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extractors/html.js similarity index 78% rename from src/dev/i18n/extract_html_messages.js rename to src/dev/i18n/extractors/html.js index 4c8cad3ce10088f..b576acb31c6d2e5 100644 --- a/src/dev/i18n/extract_html_messages.js +++ b/src/dev/i18n/extractors/html.js @@ -17,14 +17,13 @@ * under the License. */ -import chalk from 'chalk'; import { jsdom } from 'jsdom'; import { parse } from '@babel/parser'; import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Find all substrings of "{{ any text }}" pattern @@ -53,17 +52,13 @@ function parseFilterObjectExpression(expression) { for (const property of node.properties) { if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.` - ); + throw createFailError(`defaultMessage value should be a string literal.`); } message = formatJSString(property.value.value); } else if (isPropertyWithKey(property, CONTEXT_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.` - ); + throw createFailError(`context value should be a string literal.`); } context = formatJSString(property.value.value); @@ -101,27 +96,20 @@ function* getFilterMessages(htmlContent) { const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); if (!filterObjectExpression || !idExpression) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Cannot parse i18n filter expression: {{ ${expression} }}` - ); + throw createFailError(`Cannot parse i18n filter expression: {{ ${expression} }}`); } const messageId = parseIdExpression(idExpression); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in angular filter expression is not allowed.` - ); + throw createFailError(`Empty "id" value in angular filter expression is not allowed.`); } const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` + `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` ); } @@ -137,17 +125,13 @@ function* getDirectiveMessages(htmlContent) { for (const element of document.querySelectorAll('[i18n-id]')) { const messageId = formatHTMLString(element.getAttribute('i18n-id')); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "i18n-id" value in angular directive is not allowed.` - ); + throw createFailError(`Empty "i18n-id" value in angular directive is not allowed.`); } const message = formatHTMLString(element.getAttribute('i18n-default-message')); if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular directive is not allowed ("${messageId}").` + `Empty defaultMessage in angular directive is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_html_messages.test.js b/src/dev/i18n/extractors/html.test.js similarity index 94% rename from src/dev/i18n/extract_html_messages.test.js rename to src/dev/i18n/extractors/html.test.js index d5cf7d6fd5ee2b3..40664edd81e4aa2 100644 --- a/src/dev/i18n/extract_html_messages.test.js +++ b/src/dev/i18n/extractors/html.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { extractHtmlMessages } from './extract_html_messages'; +import { extractHtmlMessages } from './html'; const htmlSourceBuffer = Buffer.from(`
@@ -37,7 +37,7 @@ const htmlSourceBuffer = Buffer.from(`
`); -describe('dev/i18n/extract_html_messages', () => { +describe('dev/i18n/extractors/html', () => { test('extracts default messages from HTML', () => { const actual = Array.from(extractHtmlMessages(htmlSourceBuffer)); expect(actual.sort()).toMatchSnapshot(); diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extractors/i18n_call.js similarity index 64% rename from src/dev/i18n/extract_i18n_call_messages.js rename to src/dev/i18n/extractors/i18n_call.js index ba146c06621fe31..1adcf42598e16c0 100644 --- a/src/dev/i18n/extract_i18n_call_messages.js +++ b/src/dev/i18n/extractors/i18n_call.js @@ -17,12 +17,11 @@ * under the License. */ -import chalk from 'chalk'; import { isObjectExpression, isStringLiteral } from '@babel/types'; -import { isPropertyWithKey, formatJSString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; /** * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST @@ -31,19 +30,13 @@ export function extractI18nCallMessages(node) { const [idSubTree, optionsSubTree] = node.arguments; if (!isStringLiteral(idSubTree)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Message id in i18n() or i18n.translate() should be a string literal.` - ); + throw createFailError(`Message id in i18n() or i18n.translate() should be a string literal.`); } const messageId = idSubTree.value; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in i18n() or i18n.translate() is not allowed.` - ); + throw createFailError(`Empty "id" value in i18n() or i18n.translate() is not allowed.`); } let message; @@ -51,8 +44,7 @@ Empty "id" value in i18n() or i18n.translate() is not allowed.` if (!isObjectExpression(optionsSubTree)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } @@ -60,8 +52,7 @@ Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId} if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -69,8 +60,7 @@ defaultMessage value in i18n() or i18n.translate() should be a string literal (" } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -80,8 +70,7 @@ context value in i18n() or i18n.translate() should be a string literal ("${messa if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_i18n_call_messages.test.js b/src/dev/i18n/extractors/i18n_call.test.js similarity index 95% rename from src/dev/i18n/extract_i18n_call_messages.test.js rename to src/dev/i18n/extractors/i18n_call.test.js index 0985233e4b3dd4b..f3ab92f4f1d6e2e 100644 --- a/src/dev/i18n/extract_i18n_call_messages.test.js +++ b/src/dev/i18n/extractors/i18n_call.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { traverseNodes } from '../utils'; const i18nCallMessageSource = ` i18n('message-id-1', { defaultMessage: 'Default message 1', context: 'Message context 1' }); @@ -31,7 +31,7 @@ const translateCallMessageSource = ` i18n.translate('message-id-2', { defaultMessage: 'Default message 2', context: 'Message context 2' }); `; -describe('extractI18nCallMessages', () => { +describe('dev/i18n/extractors/i18n_call', () => { test('extracts "i18n" and "i18n.translate" functions call message', () => { let callExpressionNode = [...traverseNodes(parse(i18nCallMessageSource).program.body)].find( node => isCallExpression(node) diff --git a/src/dev/i18n/extractors/index.js b/src/dev/i18n/extractors/index.js new file mode 100644 index 000000000000000..7362eeb4e700394 --- /dev/null +++ b/src/dev/i18n/extractors/index.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export { extractCodeMessages } from './code'; +export { extractHandlebarsMessages } from './handlebars'; +export { extractHtmlMessages } from './html'; +export { extractI18nCallMessages } from './i18n_call'; +export { extractPugMessages } from './pug'; +export { extractFormattedMessages, extractIntlMessages } from './react'; diff --git a/src/dev/i18n/extract_pug_messages.js b/src/dev/i18n/extractors/pug.js similarity index 91% rename from src/dev/i18n/extract_pug_messages.js rename to src/dev/i18n/extractors/pug.js index 8451c0b11db2405..59851d19e88ab60 100644 --- a/src/dev/i18n/extract_pug_messages.js +++ b/src/dev/i18n/extractors/pug.js @@ -19,8 +19,8 @@ import { parse } from '@babel/parser'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; /** * Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}` diff --git a/src/dev/i18n/extract_pug_messages.test.js b/src/dev/i18n/extractors/pug.test.js similarity index 94% rename from src/dev/i18n/extract_pug_messages.test.js rename to src/dev/i18n/extractors/pug.test.js index 0f72c13a6a339fc..7f901d1d992dbc4 100644 --- a/src/dev/i18n/extract_pug_messages.test.js +++ b/src/dev/i18n/extractors/pug.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractPugMessages } from './extract_pug_messages'; +import { extractPugMessages } from './pug'; -describe('extractPugMessages', () => { +describe('dev/i18n/extractors/pug', () => { test('extracts messages from pug template', () => { const source = Buffer.from(`\ #{i18n('message-id', { defaultMessage: 'Default message', context: 'Message context' })} diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extractors/react.js similarity index 73% rename from src/dev/i18n/extract_react_messages.js rename to src/dev/i18n/extractors/react.js index 014f1214d0a18aa..074af4a76d5b475 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extractors/react.js @@ -18,17 +18,14 @@ */ import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types'; -import chalk from 'chalk'; -import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils'; -import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; -import { createFailError } from '../run'; +import { isPropertyWithKey, formatJSString, formatHTMLString } from '../utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from '../constants'; +import { createFailError } from '../../run'; function extractMessageId(value) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } return value.value; @@ -36,10 +33,7 @@ function extractMessageId(value) { function extractMessageValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value should be a string literal ("${id}").` - ); + throw createFailError(`defaultMessage value should be a string literal ("${id}").`); } return value.value; @@ -47,9 +41,7 @@ defaultMessage value should be a string literal ("${id}").` function extractContextValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").` - ); + throw createFailError(`context value should be a string literal ("${id}").`); } return value.value; @@ -65,8 +57,7 @@ export function extractIntlMessages(node) { if (!isObjectExpression(options)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Object with defaultMessage property is not passed to intl.formatMessage().` + `Object with defaultMessage property is not passed to intl.formatMessage().` ); } @@ -81,10 +72,7 @@ Object with defaultMessage property is not passed to intl.formatMessage().` : undefined; if (!messageId) { - createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in intl.formatMessage() is not allowed.` - ); + createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`); } const message = messageProperty @@ -93,8 +81,7 @@ Empty "id" value in intl.formatMessage() is not allowed.` if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` + `Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` ); } @@ -122,9 +109,7 @@ export function extractFormattedMessages(node) { : undefined; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in is not allowed.` - ); + throw createFailError(`Empty "id" value in is not allowed.`); } const message = messageProperty @@ -133,8 +118,7 @@ export function extractFormattedMessages(node) { if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty default message in is not allowed ("${messageId}").` + `Empty default message in is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_react_messages.test.js b/src/dev/i18n/extractors/react.test.js similarity index 97% rename from src/dev/i18n/extract_react_messages.test.js rename to src/dev/i18n/extractors/react.test.js index 00233ac1abed22b..91e65a0ecc20fdf 100644 --- a/src/dev/i18n/extract_react_messages.test.js +++ b/src/dev/i18n/extractors/react.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression, isJSXOpeningElement, isJSXIdentifier } from '@babel/types'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; -import { traverseNodes } from './utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; +import { traverseNodes } from '../utils'; const intlFormatMessageCallSource = ` const MyComponentContent = ({ intl }) => ( @@ -79,7 +79,7 @@ intl.formatMessage({ `, ]; -describe('dev/i18n/extract_react_messages', () => { +describe('dev/i18n/extractors/react', () => { describe('extractIntlMessages', () => { test('extracts messages from "intl.formatMessage" function call', () => { const ast = parse(intlFormatMessageCallSource, { plugins: ['jsx'] }); diff --git a/src/dev/i18n/index.js b/src/dev/i18n/index.js new file mode 100644 index 000000000000000..703e6ac68285553 --- /dev/null +++ b/src/dev/i18n/index.js @@ -0,0 +1,22 @@ +/* + * 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. + */ + +export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations'; +export { writeFileAsync } from './utils'; +export { serializeToJson, serializeToJson5 } from './serializers'; diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap new file mode 100644 index 000000000000000..c35e91e25cbb681 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json should serialize default messages to JSON 1`] = ` +"{ + \\"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\\" + } + } + }, + \\"plugin1.message.id-1\\": \\"Message text 1 \\", + \\"plugin2.message.id-2\\": { + \\"text\\": \\"Message text 2\\", + \\"comment\\": \\"Message context\\" + } +}" +`; diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap new file mode 100644 index 000000000000000..2166b32f28fd169 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 1`] = ` +"{ + 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', + }, + }, + }, + 'plugin1.message.id-1': 'Message text 1', + 'plugin2.message.id-2': 'Message text 2', // Message context +} +" +`; diff --git a/src/dev/i18n/serializers/index.js b/src/dev/i18n/serializers/index.js new file mode 100644 index 000000000000000..3c10d7754563d35 --- /dev/null +++ b/src/dev/i18n/serializers/index.js @@ -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. + */ + +export { serializeToJson } from './json'; +export { serializeToJson5 } from './json5'; diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js new file mode 100644 index 000000000000000..8e615af1e81d32a --- /dev/null +++ b/src/dev/i18n/serializers/json.js @@ -0,0 +1,34 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export function serializeToJson(defaultMessages) { + const resultJsonObject = { formats: i18n.formats }; + + for (const [mapKey, mapValue] of defaultMessages) { + if (mapValue.context) { + resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; + } else { + resultJsonObject[mapKey] = mapValue.message; + } + } + + return JSON.stringify(resultJsonObject, undefined, 2); +} diff --git a/src/dev/i18n/serializers/json.test.js b/src/dev/i18n/serializers/json.test.js new file mode 100644 index 000000000000000..9486a999fe7db25 --- /dev/null +++ b/src/dev/i18n/serializers/json.test.js @@ -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 { serializeToJson } from './json'; + +describe('dev/i18n/serializers/json', () => { + test('should serialize default messages to JSON', () => { + const messages = new Map([ + ['plugin1.message.id-1', { message: 'Message text 1 ' }], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson(messages)).toMatchSnapshot(); + }); +}); diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js new file mode 100644 index 000000000000000..0156053d5f43b51 --- /dev/null +++ b/src/dev/i18n/serializers/json5.js @@ -0,0 +1,48 @@ +/* + * 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 JSON5 from 'json5'; +import { i18n } from '@kbn/i18n'; + +const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; + +export function serializeToJson5(defaultMessages) { + // .slice(0, -1): remove closing curly brace from json to append messages + let jsonBuffer = Buffer.from( + JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + ); + + for (const [mapKey, mapValue] of defaultMessages) { + const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); + const formattedContext = mapValue.context + ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') + : ''; + + jsonBuffer = Buffer.concat([ + jsonBuffer, + Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), + ]); + } + + // append previously removed closing curly brace + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + + return jsonBuffer; +} diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js new file mode 100644 index 000000000000000..90be880bd32a384 --- /dev/null +++ b/src/dev/i18n/serializers/json5.test.js @@ -0,0 +1,42 @@ +/* + * 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 { serializeToJson5 } from './json5'; + +describe('dev/i18n/serializers/json5', () => { + test('should serialize default messages to JSON5', () => { + const messages = new Map([ + [ + 'plugin1.message.id-1', + { + message: 'Message text 1', + }, + ], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson5(messages).toString()).toMatchSnapshot(); + }); +}); diff --git a/src/dev/run/index.js b/src/dev/run/index.js index 1eef88d60b0a52f..b176ac365fcf448 100644 --- a/src/dev/run/index.js +++ b/src/dev/run/index.js @@ -18,4 +18,4 @@ */ export { run } from './run'; -export { createFailError, combineErrors } from './fail'; +export { createFailError, combineErrors, isFailError } from './fail'; diff --git a/src/dev/run_i18n_check.js b/src/dev/run_i18n_check.js index 9d5f0011d6f387d..02d70622b54cd00 100644 --- a/src/dev/run_i18n_check.js +++ b/src/dev/run_i18n_check.js @@ -17,13 +17,46 @@ * under the License. */ -import { run } from './run'; -import { extractDefaultTranslations } from './i18n/extract_default_translations'; +import chalk from 'chalk'; +import Listr from 'listr'; +import { resolve } from 'path'; + +import { run, createFailError } from './run'; +import { + filterPaths, + extractMessagesFromPathToMap, + writeFileAsync, + serializeToJson, + serializeToJson5, +} from './i18n/'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); + const paths = Array.isArray(path) ? path : [path || './']; + const filteredPaths = filterPaths(paths); + + if (filteredPaths.length === 0) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +None of input paths is available for extraction or validation. See .i18nrc.json.` + ); + } + + const list = new Listr( + filteredPaths.map(filteredPath => ({ + task: messages => extractMessagesFromPathToMap(filteredPath, messages), + title: filteredPath, + })) + ); + + // 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) + ); }); From d8f907b18a161d487c9af8af1189935e4e7ba092 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 5 Sep 2018 15:27:24 +0200 Subject: [PATCH 05/11] Fix broken visualize CSS (#22707) --- .../kibana/public/visualize/editor/styles/_editor.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less index 6991ae34d6cb193..2cfc573345885ab 100644 --- a/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less +++ b/src/core_plugins/kibana/public/visualize/editor/styles/_editor.less @@ -124,7 +124,7 @@ /* Without setting this to 0 you will run into a bug where the filter bar modal is hidden under a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ - > visualize { + > .visualize { height: 100%; flex: 1 1 auto; display: flex; @@ -419,7 +419,7 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */ flex-basis: 100%; } - visualize { + .visualize { .flex-parent(); flex: 1 1 100%; } From ccf455e9fdf3fe092abcb988e03e42afd8a5be2d Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 5 Sep 2018 11:10:38 -0400 Subject: [PATCH 06/11] Fix #22581 by introducing an artificial delay (#22601) Introduce a delay into reports to allow visualizations time to appear in the DOM. This is intended as a temporary (and hacky) workaround until we come up with a more robust way to determine that all of the visualizations on the page are ready for capture. --- .../export_types/printable_pdf/server/lib/screenshots.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js index 0845ecad6566439..57e17f924bb119c 100644 --- a/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js +++ b/x-pack/plugins/reporting/export_types/printable_pdf/server/lib/screenshots.js @@ -133,7 +133,13 @@ export function screenshotsObservableFactory(server) { } } - return Promise.all(renderedTasks); + // The renderComplete fires before the visualizations are in the DOM, so + // we wait for the event loop to flush before telling reporting to continue. This + // seems to correct a timing issue that was causing reporting to occasionally + // capture the first visualization before it was actually in the DOM. + const hackyWaitForVisualizations = () => new Promise(r => setTimeout(r, 100)); + + return Promise.all(renderedTasks).then(hackyWaitForVisualizations); }, args: [layout.selectors.renderComplete, captureConfig.loadDelay], awaitPromise: true, From b7918690f47a28c123e6f0cd3f331f9be89db944 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 5 Sep 2018 12:14:05 -0400 Subject: [PATCH 07/11] align staging urls with new endpoints (#22691) --- .../public/__tests__/region_map_visualization.js | 8 ++++---- src/core_plugins/tests_bundle/tests_entry_template.js | 2 +- .../public/__tests__/coordinate_maps_visualization.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js index 7f3064b99380f0b..32b9c22839ff7d8 100644 --- a/src/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -36,9 +36,9 @@ import afterdatachangeandresizePng from './afterdatachangeandresize.png'; import aftercolorchangePng from './aftercolorchange.png'; import changestartupPng from './changestartup.png'; -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; -const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; -const vectorManifestUrl = `"https://staging-dot-elastic-layer.appspot.com/v1/manifest`; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; +const tmsManifestUrl = `https://tiles-maps-stage.elastic.co/v2/manifest`; +const vectorManifestUrl = `https://vector-staging.maps.elastic.co/v2/manifest`; const manifest = { 'services': [{ 'id': 'tiles_v2', @@ -189,7 +189,7 @@ describe('RegionMapsVisualizationTests', function () { 'attribution': '

Made with NaturalEarth | Elastic Maps Service

', 'name': 'World Countries', 'format': 'geojson', - 'url': 'https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', + 'url': 'https://vector-staging.maps.elastic.co/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', 'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, { 'name': 'iso3', 'description': 'Three letter abbreviation' diff --git a/src/core_plugins/tests_bundle/tests_entry_template.js b/src/core_plugins/tests_bundle/tests_entry_template.js index eb015454aaf2726..799a25033bef200 100644 --- a/src/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/core_plugins/tests_bundle/tests_entry_template.js @@ -60,7 +60,7 @@ const legacyMetadata = { }, mapConfig: { includeElasticMapsService: true, - manifestServiceUrl: 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest' + manifestServiceUrl: 'https://catalogue-staging.maps.elastic.co/v2/manifest' }, vegaConfig: { enabled: true, diff --git a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index b1cd3bfd37c3617..426c2c37a08ed34 100644 --- a/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -50,7 +50,7 @@ function mockRawData() { mockRawData(); -const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'; +const manifestUrl = 'https://catalogue-staging.maps.elastic.co/v2/manifest'; const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`; const manifest = { 'services': [{ From 23ed2135bf2917642651248610ced7b7c724f581 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 5 Sep 2018 17:16:13 +0100 Subject: [PATCH 08/11] [ML] Fixing issue with incorrect timezones in jobs list (#22714) * [ML] Fixing issue with incorrect timezones in jobs list * refactoring min and max calculation * changes based on review * changing TimeStamp to Timestamp --- .../components/job_actions/results.js | 20 +++++++--------- .../components/jobs_list/jobs_list.js | 8 +++++-- .../jobs_list_view/jobs_list_view.js | 2 +- .../start_datafeed_modal.js | 2 +- .../ml/server/models/job_service/jobs.js | 24 +++++++------------ 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js index 3565f510645dad1..05e1db79890770f 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js @@ -14,6 +14,8 @@ import { } from '@elastic/eui'; import chrome from 'ui/chrome'; +import moment from 'moment'; +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; import { mlJobService } from 'plugins/ml/services/job_service'; @@ -21,22 +23,18 @@ function getLink(location, jobs) { let from = 0; let to = 0; if (jobs.length === 1) { - from = jobs[0].earliestTimeStamp.string; - to = jobs[0].latestTimeStamp.string; + from = jobs[0].earliestTimestampMs; + to = jobs[0].latestTimestampMs; } else { - const froms = jobs.map(j => j.earliestTimeStamp).sort((a, b) => a.unix > b.unix); - const tos = jobs.map(j => j.latestTimeStamp).sort((a, b) => a.unix < b.unix); - from = froms[0].string; - to = tos[0].string; + from = Math.min(...jobs.map(j => j.earliestTimestampMs)); + to = Math.max(...jobs.map(j => j.latestTimestampMs)); } - // if either of the dates are empty, set them to undefined - // moment will convert undefined to now. - from = (from === '') ? undefined : from; - to = (to === '') ? undefined : to; + const fromString = moment(from).format(TIME_FORMAT); + const toString = moment(to).format(TIME_FORMAT); const jobIds = jobs.map(j => j.id); - const url = mlJobService.createResultsUrl(jobIds, from, to, location); + const url = mlJobService.createResultsUrl(jobIds, fromString, toString, location); return `${chrome.getBasePath()}/app/${url}`; } diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js index 9898cfb0c7518ce..7687ac372308cf9 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -11,6 +11,7 @@ import React, { } from 'react'; import { sortBy } from 'lodash'; +import moment from 'moment'; import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; @@ -25,6 +26,7 @@ import { const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export class JobsList extends Component { constructor(props) { @@ -157,11 +159,13 @@ export class JobsList extends Component { }, { name: 'Latest timestamp', truncateText: false, - field: 'latestTimeStampUnix', + field: 'latestTimeStampSortValue', sortable: true, render: (time, item) => ( - { item.latestTimeStamp.string } + { + (item.latestTimestampMs === undefined) ? '' : moment(item.latestTimestampMs).format(TIME_FORMAT) + } ) }, { diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 8205c75da9e33b1..d5594933cf86a44 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -240,7 +240,7 @@ export class JobsListView extends Component { fullJobsList[job.id] = job.fullJob; delete job.fullJob; } - job.latestTimeStampUnix = job.latestTimeStamp.unix; + job.latestTimeStampSortValue = (job.latestTimeStampMs || 0); return job; }); const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses); diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index c22a7aeec1926af..cd5a939cbba169f 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -196,6 +196,6 @@ StartDatafeedModal.propTypes = { }; function getLowestLatestTime(jobs) { - const times = jobs.map(j => j.latestTimeStamp.unix.valueOf()); + const times = jobs.map(j => j.latestTimeStampSortValue); return moment(Math.min(...times)); } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.js b/x-pack/plugins/ml/server/models/job_service/jobs.js index 6a9da9ff5b75d30..2cb43bdabc99a99 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/plugins/ml/server/models/job_service/jobs.js @@ -13,8 +13,6 @@ import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import moment from 'moment'; import { uniq } from 'lodash'; -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); @@ -99,8 +97,8 @@ export function jobsProvider(callWithRequest) { const jobs = fullJobsList.map((job) => { const hasDatafeed = (typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length); const { - earliest: earliestTimeStamp, - latest: latestTimeStamp } = earliestAndLatestTimeStamps(job.data_counts); + earliest: earliestTimestampMs, + latest: latestTimestampMs } = earliestAndLatestTimeStamps(job.data_counts); const tempJob = { id: job.job_id, @@ -112,8 +110,8 @@ export function jobsProvider(callWithRequest) { hasDatafeed, datafeedId: (hasDatafeed && job.datafeed_config.datafeed_id) ? job.datafeed_config.datafeed_id : '', datafeedState: (hasDatafeed && job.datafeed_config.state) ? job.datafeed_config.state : '', - latestTimeStamp, - earliestTimeStamp, + latestTimestampMs, + earliestTimestampMs, nodeName: (job.node) ? job.node.name : undefined, }; if (jobIds.find(j => (j === tempJob.id))) { @@ -243,22 +241,16 @@ export function jobsProvider(callWithRequest) { function earliestAndLatestTimeStamps(dataCounts) { const obj = { - earliest: { string: '', unix: 0 }, - latest: { string: '', unix: 0 }, + earliest: undefined, + latest: undefined, }; if (dataCounts.earliest_record_timestamp) { - const ts = moment(dataCounts.earliest_record_timestamp); - obj.earliest.string = ts.format(TIME_FORMAT); - obj.earliest.unix = ts.valueOf(); - obj.earliest.moment = ts; + obj.earliest = moment(dataCounts.earliest_record_timestamp).valueOf(); } if (dataCounts.latest_record_timestamp) { - const ts = moment(dataCounts.latest_record_timestamp); - obj.latest.string = ts.format(TIME_FORMAT); - obj.latest.unix = ts.valueOf(); - obj.latest.moment = ts; + obj.latest = moment(dataCounts.latest_record_timestamp).valueOf(); } return obj; From 865a51de0aa2f0a23fe25de9f48182f7010864f9 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 5 Sep 2018 13:02:28 -0600 Subject: [PATCH 09/11] Add instructions for running reporting functional tests to x-pack README (#22683) * add instructions * rest of instructions * pdf-image requirements * typo * add Ubutnu commands --- x-pack/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/x-pack/README.md b/x-pack/README.md index 63722ac16ab1073..177a88f6e9f4366 100644 --- a/x-pack/README.md +++ b/x-pack/README.md @@ -100,6 +100,36 @@ We also have SAML API integration tests which set up Elasticsearch and Kibana wi node scripts/functional_tests --config test/saml_api_integration/config ``` +#### Running Reporting functional tests + +prerequisites: +The reporting functional tests use [pdf-image](https://www.npmjs.com/package/pdf-image) to convert PDF's pages to png files for image comparisions between generated reports and baseline reports. +pdf-image requires the system commands `convert`, `gs`, and `pdfinfo` to function. Those can be set up by running the following. + +```sh +//OSX +brew install imagemagick ghostscript poppler + +//Ubutnu +sudo apt-get install imagemagick ghostscript poppler-utils +``` + +To run the reporting functional tests: + +Start the test server +```sh +// Run from the directory KIBANA_HOME/x-pack +node scripts/functional_tests_server +``` + +Start the test runner +```sh +// Run from the directory KIBANA_HOME/x-pack +node ../scripts/functional_test_runner --config test/reporting/configs/chromium_functional.js +``` + +**Note** Configurations from `kibana.dev.yml` are picked up when running the tests. Ensure that `kibana.dev.yml` does not contain any `xpack.reporting` configurations. + #### Developing functional tests If you are **developing functional tests** then you probably don't want to rebuild Elasticsearch and wait for all that setup on every test run, so instead use this command to build and start just the Elasticsearch and Kibana servers: From f647d6cd5c0c99a46ddc67b39a0f48b5ed23675c Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 5 Sep 2018 20:19:00 +0100 Subject: [PATCH 10/11] [ML] Makefield type icon component keyboard accessible (#22708) --- .../__snapshots__/field_type_icon.test.js.snap | 8 ++++---- .../field_type_icon/field_type_icon.js | 18 +++++++++--------- .../field_type_icon/field_type_icon.test.js | 7 ++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap b/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap index f8ff8dd75232322..df9367578f0cac4 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap +++ b/x-pack/plugins/ml/public/components/field_type_icon/__snapshots__/field_type_icon.test.js.snap @@ -2,7 +2,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` @@ -10,8 +10,8 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = ` exports[`FieldTypeIcon update component 1`] = ` `; diff --git a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js index 33c2fda04edad03..cf23fa1468f7daf 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js +++ b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.js @@ -20,31 +20,31 @@ export function FieldTypeIcon({ tooltipEnabled = false, type }) { switch (type) { case ML_JOB_FIELD_TYPES.BOOLEAN: - ariaLabel = 'Boolean field'; + ariaLabel = 'boolean type'; iconClass = 'fa-adjust'; break; case ML_JOB_FIELD_TYPES.DATE: - ariaLabel = 'Date field'; + ariaLabel = 'date type'; iconClass = 'fa-clock-o'; break; case ML_JOB_FIELD_TYPES.NUMBER: - ariaLabel = 'Metric field'; + ariaLabel = 'number type'; iconChar = '#'; break; case ML_JOB_FIELD_TYPES.GEO_POINT: - ariaLabel = 'Geo-point field'; + ariaLabel = 'geo_point type'; iconClass = 'fa-globe'; break; case ML_JOB_FIELD_TYPES.KEYWORD: - ariaLabel = 'Aggregatable string field'; + ariaLabel = 'keyword type'; iconChar = 't'; break; case ML_JOB_FIELD_TYPES.TEXT: - ariaLabel = 'String field'; + ariaLabel = 'text type'; iconClass = 'fa-file-text-o'; break; case ML_JOB_FIELD_TYPES.IP: - ariaLabel = 'IP address field'; + ariaLabel = 'IP type'; iconClass = 'fa-laptop'; break; default: @@ -69,7 +69,7 @@ export function FieldTypeIcon({ tooltipEnabled = false, type }) { // to support having another component directly inside the tooltip anchor // see https://github.com/elastic/eui/issues/839 return ( - + ); @@ -86,7 +86,7 @@ FieldTypeIcon.propTypes = { // To pass on its properties we apply `rest` to the outer `span` element. function FieldTypeIconContainer({ ariaLabel, className, iconChar, ...rest }) { return ( - + {(iconChar === '') ? ( ) : ( diff --git a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js index 74432ee96433008..975319f94273901 100644 --- a/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/plugins/ml/public/components/field_type_icon/field_type_icon.test.js @@ -8,6 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { FieldTypeIcon } from './field_type_icon'; +import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; describe('FieldTypeIcon', () => { @@ -22,12 +23,12 @@ describe('FieldTypeIcon', () => { }); test(`render component when type matches a field type`, () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); test(`render with tooltip and test hovering`, () => { - const wrapper = mount(); + const wrapper = mount(); const container = wrapper.find({ className: 'field-type-icon-container' }); expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); @@ -42,7 +43,7 @@ describe('FieldTypeIcon', () => { test(`update component`, () => { const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBeTruthy(); - wrapper.setProps({ type: 'keyword' }); + wrapper.setProps({ type: ML_JOB_FIELD_TYPES.IP }); expect(wrapper).toMatchSnapshot(); }); From f7fbed34eb777a1f41e2d718b38b10eb4aace021 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 5 Sep 2018 21:31:03 +0200 Subject: [PATCH 11/11] [APM] Update Node.js onboarding instructions (#22562) --- .../kibana/server/tutorials/apm/apm_client_instructions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js index 49c4b999af18eba..183cf26a6679ee0 100644 --- a/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js +++ b/src/core_plugins/kibana/server/tutorials/apm/apm_client_instructions.js @@ -30,10 +30,11 @@ export const createNodeClientInstructions = () => [ textPre: 'Agents are libraries that run inside of your application process.' + ' APM services are created programmatically based on the `serviceName`.' + - ' This agent supports Express, Koa, hapi, and custom Node.js.', + ' This agent supports a vararity of frameworks but can also be used with your custom stack.', commands: `// Add this to the VERY top of the first file loaded in your app var apm = require('elastic-apm-node').start({curlyOpen} - // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + // Override service name from package.json + // Allowed characters: a-z, A-Z, 0-9, -, _, and space serviceName: '', // Use if APM Server requires a token