[I18n] Translate Timelion (#23880)

* Add translations for timelion plugin

* Fix bugs

* Fix messages ids, resolve comments

* Update translations

* Refactor links messages

* Fix values bug

* Use template literals to avoid single quote escaping
This commit is contained in:
Leanid Shutau 2018-11-13 17:16:15 +03:00 committed by GitHub
parent 6d79657b41
commit ff8675d1c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1572 additions and 513 deletions

View file

@ -2,7 +2,7 @@
"paths": {
"common.ui": "src/ui",
"console": "src/core_plugins/console",
"inputControl":"src/core_plugins/input_control_vis",
"inputControl": "src/core_plugins/input_control_vis",
"kbn": "src/core_plugins/kibana",
"kbnVislibVisTypes": "src/core_plugins/kbn_vislib_vis_types",
"markdownVis": "src/core_plugins/markdown_vis",
@ -12,6 +12,7 @@
"regionMap": "src/core_plugins/region_map",
"statusPage": "src/core_plugins/status_page",
"tileMap": "src/core_plugins/tile_map",
"timelion": "src/core_plugins/timelion",
"tagCloud": "src/core_plugins/tagcloud",
"xpack.grokDebugger": "x-pack/plugins/grokdebugger",
"xpack.idxMgmt": "x-pack/plugins/index_management",

View file

@ -103,7 +103,8 @@ export default function (kibana) {
description: `<em>[experimental]</em> Your API key from www.quandl.com`,
category: ['timelion'],
}
}
},
translations: [],
},
init: require('./init.js'),
});

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import processFunctionDefinition from './server/lib/process_function_definition';
@ -33,7 +34,15 @@ export default function (server) {
}
function getFunction(name) {
if (!functions[name]) throw new Error ('No such function: ' + name);
if (!functions[name]) {
throw new Error(
i18n.translate('timelion.noFunctionErrorMessage', {
defaultMessage: 'No such function: {name}',
values: { name },
})
);
}
return functions[name];
}

View file

@ -91,6 +91,7 @@ app.controller('timelion', function (
kbnUrl,
Notifier,
Private,
i18n,
) {
// Keeping this at app scope allows us to keep the current page when the user
@ -114,22 +115,30 @@ app.controller('timelion', function (
$scope.topNavMenu = [{
key: 'new',
description: 'New Sheet',
description: i18n('timelion.topNavMenu.newDescription', {
defaultMessage: 'New Sheet',
}),
run: function () { kbnUrl.change('/'); },
testId: 'timelionNewButton',
}, {
key: 'add',
description: 'Add a chart',
description: i18n('timelion.topNavMenu.addDescription', {
defaultMessage: 'Add a chart',
}),
run: function () { $scope.newCell(); },
testId: 'timelionAddChartButton',
}, {
key: 'save',
description: 'Save Sheet',
description: i18n('timelion.topNavMenu.saveDescription', {
defaultMessage: 'Save Sheet',
}),
template: require('plugins/timelion/partials/save_sheet.html'),
testId: 'timelionSaveButton',
}, {
key: 'delete',
description: 'Delete current sheet',
description: i18n('timelion.topNavMenu.deleteDescription', {
defaultMessage: 'Delete current sheet',
}),
disableButton: function () {
return !savedSheet.id;
},
@ -137,32 +146,55 @@ app.controller('timelion', function (
const title = savedSheet.title;
function doDelete() {
savedSheet.delete().then(() => {
toastNotifications.addSuccess(`Deleted '${title}'`);
toastNotifications.addSuccess(i18n(
'timelion.topNavMenu.delete.modal.successNotificationText',
{
defaultMessage: `Deleted '{title}'`,
values: { title },
}
));
kbnUrl.change('/');
}).catch(error => fatalError(error, location));
}
const confirmModalOptions = {
onConfirm: doDelete,
confirmButtonText: 'Delete',
title: `Delete Timelion sheet '${title}'?`
confirmButtonText: i18n('timelion.topNavMenu.delete.modal.confirmButtonLabel', {
defaultMessage: 'Delete',
}),
title: i18n('timelion.topNavMenu.delete.modalTitle', {
defaultMessage: `Delete Timelion sheet '{title}'?`,
values: { title }
}),
};
confirmModal(`You can't recover deleted sheets.`, confirmModalOptions);
confirmModal(
i18n('timelion.topNavMenu.delete.modal.warningText', {
defaultMessage: `You can't recover deleted sheets.`,
}),
confirmModalOptions
);
},
testId: 'timelionDeleteButton',
}, {
key: 'open',
description: 'Open Sheet',
description: i18n('timelion.topNavMenu.openDescription', {
defaultMessage: 'Open Sheet',
}),
template: require('plugins/timelion/partials/load_sheet.html'),
testId: 'timelionOpenButton',
}, {
key: 'options',
description: 'Options',
description: i18n('timelion.topNavMenu.optionsDescription', {
defaultMessage: 'Options',
}),
template: require('plugins/timelion/partials/sheet_options.html'),
testId: 'timelionOptionsButton',
}, {
key: 'help',
description: 'Help',
description: i18n('timelion.topNavMenu.helpDescription', {
defaultMessage: 'Help',
}),
template: '<timelion-help></timelion-help>',
testId: 'timelionDocsButton',
}];
@ -289,7 +321,13 @@ app.controller('timelion', function (
savedSheet.timelion_rows = $scope.state.rows;
savedSheet.save().then(function (id) {
if (id) {
toastNotifications.addSuccess(`Saved sheet '${savedSheet.title}'`);
toastNotifications.addSuccess(
i18n('timelion.saveSheet.successNotificationText', {
defaultMessage: `Saved sheet '{title}'`,
values: { title: savedSheet.title },
})
);
if (savedSheet.id !== $routeParams.id) {
kbnUrl.change('/{{id}}', { id: savedSheet.id });
}
@ -307,7 +345,12 @@ app.controller('timelion', function (
savedExpression.visState.title = title;
savedExpression.save().then(function (id) {
if (id) {
toastNotifications.addSuccess(`Saved expression '${savedExpression.title}'`);
toastNotifications.addSuccess(
i18n('timelion.saveExpression.successNotificationText', {
defaultMessage: `Saved expression '{title}'`,
values: { title: savedExpression.title },
}),
);
}
});
});

View file

@ -21,18 +21,18 @@
<button
class="timCell__action"
ng-click="removeCell($index)"
tooltip="Remove"
tooltip="{{ ::'timelion.cells.actions.removeTooltip' | i18n: { defaultMessage: 'Remove' } }}"
tooltip-append-to-body="1"
aria-label="Remove chart"
aria-label="{{ ::'timelion.cells.actions.removeAriaLabel' | i18n: { defaultMessage: 'Remove chart' } }}"
>
<span class="fa fa-remove"></span>
</button>
<button
class="timCell__action"
tooltip="Drag to reorder"
tooltip="{{ ::'timelion.cells.actions.reorderTooltip' | i18n: { defaultMessage: 'Drag to reorder' } }}"
tooltip-append-to-body="1"
sv-handle
aria-label="Drag to reorder"
aria-label="{{ ::'timelion.cells.actions.reorderAriaLabel' | i18n: { defaultMessage: 'Drag to reorder' } }}"
tabindex="-1"
>
<span class="fa fa-arrows"></span>
@ -40,9 +40,9 @@
<button
class="timCell__action"
ng-click="transient.fullscreen = true"
tooltip="Full screen"
tooltip="{{ ::'timelion.cells.actions.fullscreenTooltip' | i18n: { defaultMessage: 'Full screen' } }}"
tooltip-append-to-body="1"
aria-label="Full screen chart"
aria-label="{{ ::'timelion.cells.actions.fullscreenAriaLabel' | i18n: { defaultMessage: 'Full screen chart' } }}"
>
<span class="fa fa-expand"></span>
</button>

View file

@ -16,12 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import panelRegistryProvider from '../../lib/panel_registry';
require('ui/modules')
.get('apps/timelion', [])
.directive('chart', function (Private) {
.directive('chart', function (Private, i18n) {
return {
restrict: 'A',
scope: {
@ -46,7 +45,12 @@ require('ui/modules')
const panelSchema = panelRegistry.byName[seriesList.render.type];
if (!panelSchema) {
$elem.text('No such panel type: ' + seriesList.render.type);
$elem.text(
i18n('timelion.chart.seriesList.noSchemaWarning', {
defaultMessage: 'No such panel type: {renderType}',
values: { renderType: seriesList.render.type },
})
);
return;
}

View file

@ -4,9 +4,9 @@
<button
class="timCell__action"
ng-click="transient.fullscreen = false"
tooltip="Exit full screen"
tooltip="{{ ::'timelion.fullscreen.exitTooltip' | i18n: { defaultMessage: 'Exit full screen' } }}"
tooltip-append-to-body="1"
aria-label="Exit full screen"
aria-label="{{ ::'timelion.fullscreen.exitAriaLabel' | i18n: { defaultMessage: 'Exit full screen' } }}"
>
<span class="fa fa-compress"></span>
</button>

View file

@ -13,7 +13,7 @@
role="textbox"
rows="{{ rows }}"
class="timExpressionInput kuiTextArea fullWidth"
placeholder="Try a query with .es(*)"
placeholder="{{ ::'timelion.expressionInputPlaceholder' | i18n: { defaultMessage: 'Try a query with {esQuery}', values: { esQuery: '.es(*)' } } }}"
ng-model="sheet"
ng-focus="onFocusInput()"
ng-keydown="onKeyDownInput($event)"
@ -22,7 +22,7 @@
ng-mousedown="onMouseDownInput()"
ng-mouseup="onMouseUpInput()"
ng-click="onClickExpression()"
aria-label="Timelion expression"
aria-label="{{ ::'timelion.expressionInputAriaLabel' | i18n: { defaultMessage: 'Timelion expression' } }}"
aria-multiline="false"
aria-autocomplete="list"
aria-controls="timelionSuggestionList"

View file

@ -26,14 +26,27 @@
<h4>
<strong>.{{suggestion.name}}()</strong>
<small id="timelionSuggestionDescription{{$index}}">
{{suggestion.help}}
{{suggestion.chainable ? '(Chainable)' : '(Data Source)'}}
<span
ng-if="suggestion.chainable"
i18n-id="timelion.expressionSuggestions.func.description.chainableText"
i18n-default-message="{help} (Chainable)"
i18n-values="{ help: suggestion.help }"
></span>
<span
ng-if="!suggestion.chainable"
i18n-id="timelion.expressionSuggestions.func.description.dataSourceText"
i18n-default-message="{help} (Data Source)"
i18n-values="{ help: suggestion.help }"
></span>
</small>
</h4>
<div ng-show="suggestion.args.length > (suggestion.chainable ? 1: 0)">
<div ng-show="suggestions.length > 1">
<strong>Arguments:</strong>
<strong
i18n-id="timelion.expressionSuggestions.arg.listTitle"
i18n-default-message="Arguments:"
></strong>
<span ng-repeat="arg in suggestion.args" ng-hide="$index < 1 && suggestion.chainable">
<strong>{{arg.name}}</strong>=(<em>{{arg.types.join(' | ')}}</em>)
<em ng-show="!$last">,</em>
@ -43,9 +56,21 @@
<div class="timSuggestions__details" ng-show="suggestions.length === 1">
<table class="table table-striped table-condensed table-bordered">
<thead>
<th scope="col">Argument Name</th>
<th scope="col">Accepted Types</th>
<th scope="col">Information</th>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.nameTitle"
i18n-default-message="Argument Name"
></th>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.typesTitle"
i18n-default-message="Accepted Types"
></th>
<th
scope="col"
i18n-id="timelion.expressionSuggestions.arg.infoTitle"
i18n-default-message="Information"
></th>
</thead>
<tr ng-repeat="arg in suggestion.args" ng-hide="$index < 1 && suggestion.chainable">
<td>{{arg.name}}</td>

View file

@ -1,21 +1,36 @@
<div class="euiText timHelp">
<div ng-show="page === 1">
<div>
<h1>Welcome to <strong>Timelion</strong>!</h1>
<h1
i18n-id="timelion.help.welcomeTitle"
i18n-default-message="Welcome to {strongTimelionLabel}!"
i18n-values="{ strongTimelionLabel: '<strong>Timelion</strong>' }"
></h1>
<p
i18n-id="timelion.help.welcome.content.paragraph1"
i18n-default-message="Timelion is the clawing, gnashing, zebra killing, pluggable time
series interface for {emphasizedEverything}. If your datastore can
produce a time series, then you have all of the awesome power of
Timelion at your disposal. Timelion lets you compare, combine, and
combobulate datasets across multiple datasources with one
easy-to-master expression syntax. This tutorial focuses on
Elasticsearch, but you'll quickly discover that what you learn here
applies to any datasource Timelion supports."
i18n-values="{ emphasizedEverything: '<em>' + translations.emphasizedEverythingText + '</em>' }"
></p>
<p>
Timelion is the clawing, gnashing, zebra killing, pluggable time
series interface for <em>everything</em>. If your datastore can
produce a time series, then you have all of the awesome power of
Timelion at your disposal. Timelion lets you compare, combine, and
combobulate datasets across multiple datasources with one
easy-to-master expression syntax. This tutorial focuses on
Elasticsearch, but you'll quickly discover that what you learn here
applies to any datasource Timelion supports.
</p>
<p>
Ready to get started? Click <strong>Next</strong>. Want to skip the
tutorial and view the docs? <a ng-click="setPage(0)">
Jump to the function reference</a>.
<span
i18n-id="timelion.help.welcome.content.paragraph2"
i18n-default-message="Ready to get started? Click {strongNext}. Want to skip the tutorial and view the docs?"
i18n-values="{
strongNext: '<strong>' + translations.strongNextText + '</strong>',
}"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.welcome.content.functionReferenceLinkText"
i18n-default-message="Jump to the function reference"
></a>.
</p>
</div>
<div class="timHelp__buttons">
@ -24,7 +39,7 @@
ng-click="opts.dontShowHelp()"
class="kuiButton kuiButton--hollow"
>
Don't show this again
{{translations.dontShowHelpButtonLabel}}
</button>
@ -32,7 +47,7 @@
ng-click="setPage(page+1)"
class="kuiButton kuiButton--primary"
>
Next
{{translations.nextButtonLabel}}
</button>
</div>
@ -40,20 +55,28 @@
<div ng-show="page === 2">
<div ng-show="!es.valid">
<div>
<h2>First time configuration</h2>
<p>
If you're using Logstash, you don't need to configure anything to
start exploring your log data with Timelion. To search other
indices, go to <strong>Management / Kibana / Advanced Settings
</strong> and configure the <code>timelion:es.default_index</code>
and <code>timelion:es.timefield</code> settings to match your
indices.
</p>
<p>
You'll also see some other Timelion settings. For now, you don't need
to worry about them. Later, you'll see that you can set most of
them on the fly if you need to.
</p>
<h2
i18n-id="timelion.help.configuration.notValidTitle"
i18n-default-message="First time configuration"
></h2>
<p
i18n-id="timelion.help.configuration.notValid.paragraph1"
i18n-default-message="If you're using Logstash, you don't need to configure anything to
start exploring your log data with Timelion. To search other
indices, go to {advancedSettingsPath} and configure the {esDefaultIndex}
and {esTimefield} settings to match your indices."
i18n-values="{
advancedSettingsPath: '<strong>' + translations.notValidAdvancedSettingsPath + '</strong>',
esDefaultIndex: '<code>timelion:es.default_index</code>',
esTimefield: '<code>timelion:es.timefield</code>',
}"
></p>
<p
i18n-id="timelion.help.configuration.notValid.paragraph2"
i18n-default-message="You'll also see some other Timelion settings. For now, you don't need
to worry about them. Later, you'll see that you can set most of
them on the fly if you need to."
></p>
</div>
<div class="timHelp__buttons">
@ -61,74 +84,139 @@
ng-click="setPage(page-1)"
class="kuiButton kuiButton--primary"
>
Previous
{{translations.previousButtonLabel}}
</button>
<span ng-show="es.invalidCount > 0 && !es.valid">
Could not validate Elasticsearch settings:
<strong>{{es.invalidReason}}</strong>. Check your Advanced Settings
and try again. ({{es.invalidCount}})
</span>
<span
ng-show="es.invalidCount > 0 && !es.valid"
i18n-id="timelion.help.configuration.notValid.notValidSettingsErrorMessage"
i18n-default-message="Could not validate Elasticsearch settings: {reason}.
Check your Advanced Settings and try again. ({count})"
i18n-values="{
reason: '<strong>' + es.invalidReason + '</strong>',
count: es.invalidCount,
}"
></span>
<button
ng-click="recheckElasticsearch()"
class="kuiButton kuiButton--primary"
>
Validate Config
</button>
i18n-id="timelion.help.configuration.notValid.validateButtonLabel"
i18n-default-message="Validate Config"
></button>
</div>
</div>
<div ng-show="es.valid">
<div>
<h2>Good news, Elasticsearch is configured correctly!</h2>
<h2
i18n-id="timelion.help.configuration.validTitle"
i18n-default-message="Good news, Elasticsearch is configured correctly!"
></h2>
<p>
We validated your default index and your timefield and everything
looks ok. We found data from <strong>{{es.stats.min}}</strong> to
<strong>{{es.stats.max}}</strong>. You're probably all set. If this
doesn't look right, see <a ng-click="es.valid = false">First time
configuration</a> for information about configuring the Elasticsearch
datasource.
</p>
<p>
You should already see one chart, but you might need to make a
couple adjustments before you see any interesting data:
<span
i18n-id="timelion.help.configuration.valid.paragraph1Part1"
i18n-default-message="We validated your default index and your timefield and everything
looks ok. We found data from {statsMin} to {statsMax}.
You're probably all set. If this doesn't look right, see"
i18n-values="{
statsMin: '<strong>' + es.stats.min + '</strong>',
statsMax: '<strong>' + es.stats.max + '</strong>',
}"
i18n-context="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></span>
<a
ng-click="es.valid = false"
i18n-id="timelion.help.configuration.firstTimeConfigurationLinkText"
i18n-default-message="First time configuration"
i18n-context="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></a>
<span
i18n-id="timelion.help.configuration.valid.paragraph1Part2"
i18n-default-message="for information about configuring the Elasticsearch datasource."
i18n-context="Part of composite text timelion.help.configuration.valid.paragraph1Part1 +
timelion.help.configuration.firstTimeConfigurationLinkText +
timelion.help.configuration.valid.paragraph1Part2"
></span>
</p>
<p
i18n-id="timelion.help.configuration.valid.paragraph2"
i18n-default-message="You should already see one chart, but you might need to make a
couple adjustments before you see any interesting data:"
></p>
<ul>
<li>
<strong>Intervals</strong>
<strong
i18n-id="timelion.help.configuration.valid.intervalsTitle"
i18n-default-message="Intervals"
></strong>
<p>
The interval selector at the right of the input bar lets you
control the sampling frequency. It's currently set to
<code>{{state.interval}}</code>.
<span
i18n-id="timelion.help.configuration.valid.intervalsTextPart1"
i18n-default-message="The interval selector at the right of the input bar lets you
control the sampling frequency. It's currently set to {interval}."
i18n-values="{ interval: '<code>' + state.interval + '</code>' }"
i18n-context="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
></span>
<span ng-show="state.interval == 'auto'">
<strong>You're all set!</strong>
<strong
i18n-id="timelion.help.configuration.valid.intervalIsAutoText"
i18n-default-message="You're all set!"
i18n-context="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
></strong>
</span>
<span ng-show="state.interval != 'auto'">
Set it to <code>auto </code> to let Timelion choose an
appropriate interval.
</span>
If Timelion thinks your combination of time range and interval
will produce too many data points, it throws an error. You can
adjust that limit by configuring <code>timelion:max_buckets</code>
in <strong>Management/Kibana/Advanced Settings</strong>.
<span
ng-show="state.interval != 'auto'"
i18n-id="timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText"
i18n-default-message="Set it to {auto} to let Timelion choose an appropriate interval."
i18n-context="Part of composite text
timelion.help.configuration.valid.intervalsTextPart1 +
(timelion.help.configuration.valid.intervalIsAutoText ||
timelion.help.configuration.valid.intervals.content.intervalIsNotAutoText) +
timelion.help.configuration.valid.intervalsTextPart2"
i18n-values="{ auto: '<code>auto </code>' }"
></span>
<span
i18n-id="timelion.help.configuration.valid.intervalsTextPart2"
i18n-default-message="If Timelion thinks your combination of time range and interval
will produce too many data points, it throws an error.
You can adjust that limit by configuring {maxBuckets} in {advancedSettingsPath}."
i18n-values="{
maxBuckets: '<code>timelion:max_buckets</code>',
advancedSettingsPath: '<strong>' + translations.validAdvancedSettingsPath + '</strong>',
}"
></span>
</p>
</li>
<li>
<strong>Time range</strong>
<p>
Use the timepicker in the
Kibana toolbar to select the time period that contains the
data you want to visualize. Make sure you select a time
period that includes all or part of the time range shown
above.
</p>
<strong
i18n-id="timelion.help.configuration.valid.timeRangeTitle"
i18n-default-message="Time range"
></strong>
<p
i18n-id="timelion.help.configuration.valid.timeRangeText"
i18n-default-message="Use the timepicker in the Kibana toolbar to select the time period
that contains the data you want to visualize. Make sure you select
a time period that includes all or part of the time range shown above."
></p>
</li>
</ul>
<p>
Now, you should see a line chart that displays a count of your
data points over time.
</p>
<p
i18n-id="timelion.help.configuration.valid.paragraph3"
i18n-default-message="Now, you should see a line chart that displays a count of your data points over time."
></p>
</div>
<div class="timHelp__buttons">
@ -136,7 +224,7 @@
ng-click="setPage(page-1)"
class="kuiButton kuiButton--primary"
>
Previous
{{translations.previousButtonLabel}}
</button>
@ -144,7 +232,7 @@
ng-click="setPage(page+1)"
class="kuiButton kuiButton--primary"
>
Next
{{translations.nextButtonLabel}}
</button>
</div>
@ -152,60 +240,131 @@
</div>
<div ng-show="page === 3">
<div>
<h2>Querying the Elasticsearch datasource</h2>
<h2
i18n-id="timelion.help.queryingTitle"
i18n-default-message="Querying the Elasticsearch datasource"
></h2>
<p
i18n-id="timelion.help.querying.paragraph1"
i18n-default-message="Now that we've validated that you have a working Elasticsearch
datasource, you can start submitting queries. For starters,
enter {esPattern} in the input bar and hit enter."
i18n-values="{
esPattern: '<code>.es(*)</code>',
}"
></p>
<p>
Now that we've validated that you have a working Elasticsearch
datasource, you can start submitting queries. For starters,
enter <code>.es(*)</code> in the input bar and hit enter.
</p>
<p>
This says <em>hey Elasticsearch, find everything in my default
index</em>. If you want to find a subset, you could enter something
like <code>.es(html)</code> to count events that match <em>html</em>,
or <code>.es('user:bob AND bytes:>100')</code> to find events
that contain <em>bob</em> in the <code>user</code> field and have a
<code>bytes</code> field that is greater than 100. Note that this query
is enclosed in single quotes&mdash;that's because it contains
spaces. You can enter any
<span
i18n-id="timelion.help.querying.paragraph2Part1"
i18n-default-message="This says {esAsteriskQueryDescription}. If you want to find a subset, you could enter something
like {htmlQuery} to count events that match {html}, or {bobQuery}
to find events that contain {bob} in the {user} field and have a {bytes}
field that is greater than 100. Note that this query is enclosed in single
quotes&mdash;that's because it contains spaces. You can enter any"
i18n-values="{
esAsteriskQueryDescription: '<em>' + translations.esAsteriskQueryDescription + '</em>',
html: '<em>html</em>',
htmlQuery: '<code>.es(html)</code>',
bobQuery: '<code>.es(\'user:bob AND bytes:>100\')</code>',
bob: '<em>bob</em>',
user: '<code>user</code>',
bytes: '<code>bytes</code>',
}"
i18n-context="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></span>
<a
href="https://www.elastic.co/guide/en/elasticsearch/reference/5.1/query-dsl-query-string-query.html#query-string-syntax"
target="_blank"
rel="noopener noreferrer"
>
Lucene query string
</a>
as the first argument to the <code>.es()</code> function.
href="https://www.elastic.co/guide/en/elasticsearch/reference/5.1/query-dsl-query-string-query.html#query-string-syntax"
target="_blank"
rel="noopener noreferrer"
i18n-id="timelion.help.querying.luceneQueryLinkText"
i18n-default-message="Lucene query string"
i18n-context="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></a>
<span
i18n-id="timelion.help.querying.paragraph2Part2"
i18n-default-message="as the first argument to the {esQuery} function."
i18n-values="{
esQuery: '<code>.es()</code>',
}"
i18n-context="Part of composite text
timelion.help.querying.paragraph2Part1 +
timelion.help.querying.luceneQueryLinkText +
timelion.help.querying.paragraph2Part2"
></span>
</p>
<h4>Passing arguments</h4>
<h4
i18n-id="timelion.help.querying.passingArgumentsTitle"
i18n-default-message="Passing arguments"
></h4>
<p
i18n-id="timelion.help.querying.passingArgumentsText"
i18n-default-message="Timelion has a number of shortcuts that make it easy to do common things.
One is that for simple arguments that don't contain spaces or special
characters, you don't need to use quotes. Many functions also have defaults.
For example, {esEmptyQuery} and {esStarQuery} do the same thing.
Arguments also have names, so you don't have to specify them in a specific order.
For example, you can enter {esLogstashQuery} to tell the Elasticsearch datasource
{esIndexQueryDescription}."
i18n-values="{
esEmptyQuery: '<code>.es()</code>',
esStarQuery: '<code>.es(*)</code>',
esLogstashQuery: '<code>.es(index=\'logstash-*\', q=\'*\')</code>',
esIndexQueryDescription: '<em>' + translations.esIndexQueryDescription + '</em>',
}"
></p>
<h4
i18n-id="timelion.help.querying.countTitle"
i18n-default-message="Beyond count"
></h4>
<p>
Timelion has a number of shortcuts that make it easy to do common
things. One is that for simple arguments that don't contain spaces or
special characters, you don't need to use quotes. Many functions also
have defaults. For example, <code>.es()</code> and <code>.es(*)</code>
do the same thing. Arguments also have names, so you don't have to
specify them in a specific order. For example, you can enter
<code>.es(index='logstash-*', q='*')</code> to tell the
Elasticsearch datasource <em>use * as the q (query) for the
logstash-* index</em>.
</p>
<h4>Beyond count</h4>
<p>
Counting events is all well and good, but the Elasticsearch datasource
also supports any
<span
i18n-id="timelion.help.querying.countTextPart1"
i18n-default-message="Counting events is all well and good, but the Elasticsearch datasource also supports any"
i18n-context="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></span>
<a
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html"
target="_blank"
rel="noopener noreferrer"
>
Elasticsearch metric aggregation
</a>
that returns a single value. Some of the most useful are
<code>min</code>, <code>max</code>, <code>avg</code>, <code>sum</code>,
and <code>cardinality</code>. Let's say you want a unique count of the
<code>src_ip</code> field. Simply use the <code>cardinality</code>
metric: <code>.es(*, metric='cardinality:src_ip')</code>. To get the
average of the <code>bytes</code> field, you can use the
<code>avg</code> metric: <code>.es(metric='avg:bytes')</code>.
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html"
target="_blank"
rel="noopener noreferrer"
i18n-id="timelion.help.querying.countMetricAggregationLinkText"
i18n-default-message="Elasticsearch metric aggregation"
i18n-context="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></a>
<span
i18n-id="timelion.help.querying.countTextPart2"
i18n-default-message="that returns a single value. Some of the most useful are
{min}, {max}, {avg}, {sum}, and {cardinality}.
Let's say you want a unique count of the {srcIp} field.
Simply use the {cardinality} metric: {esCardinalityQuery}. To get the
average of the {bytes} field, you can use the {avg} metric: {esAvgQuery}."
i18n-values="{
min: '<code>min</code>',
max: '<code>max</code>',
avg: '<code>avg</code>',
sum: '<code>sum</code>',
cardinality: '<code>cardinality</code>',
bytes: '<code>bytes</code>',
srcIp: '<code>src_ip</code>',
esCardinalityQuery: '<code>.es(*, metric=\'cardinality:src_ip\')</code>',
esAvgQuery: '<code>.es(metric=\'avg:bytes\')</code>',
}"
i18n-context="Part of composite text
timelion.help.querying.countTextPart1 +
timelion.help.querying.countMetricAggregationLinkText +
timelion.help.querying.countTextPart2"
></span>
</p>
</div>
<div class="timHelp__buttons">
@ -214,7 +373,7 @@
ng-click="setPage(page-1)"
class="kuiButton kuiButton--primary"
>
Previous
{{translations.previousButtonLabel}}
</button>
@ -222,7 +381,7 @@
ng-click="setPage(page+1)"
class="kuiButton kuiButton--primary"
>
Next
{{translations.nextButtonLabel}}
</button>
</div>
@ -230,60 +389,95 @@
<div ng-show="page === 4">
<div>
<h2>Expressing yourself with expressions</h2>
<p>
Every expression starts with a datasource function. From there, you
can append new functions to the datasource to transform and augment
it.
</p>
<p>
By the way, from here on out you probably know more about your data
than we do. Feel free to replace the sample queries with something
more meaningful!
</p>
<p>
We're going to experiment, so click <strong>Add</strong> in the Kibana
toolbar to add another chart or three. Then, select a chart, copy
one of the following expressions, paste it into the input bar,
and hit enter. Rinse, repeat to try out the other expressions.
</p>
<h2
i18n-id="timelion.help.expressionsTitle"
i18n-default-message="Expressing yourself with expressions"
></h2>
<p
i18n-id="timelion.help.expressions.paragraph1"
i18n-default-message="Every expression starts with a datasource function. From there, you
can append new functions to the datasource to transform and augment it."
></p>
<p
i18n-id="timelion.help.expressions.paragraph2"
i18n-default-message="By the way, from here on out you probably know more about your data
than we do. Feel free to replace the sample queries with something
more meaningful!"
></p>
<p
i18n-id="timelion.help.expressions.paragraph3"
i18n-default-message="We're going to experiment, so click {strongAdd} in the Kibana toolbar
to add another chart or three. Then, select a chart,
copy one of the following expressions, paste it into the input bar,
and hit enter. Rinse, repeat to try out the other expressions."
i18n-values="{ strongAdd: '<strong>' + translations.strongAddText + '</strong>' }"
></p>
<table class="table table-condensed table-striped">
<tr>
<td><code>.es(*), .es(US)</code></td>
<td><strong>Double the fun.</strong> Two expressions on the same
chart.</td>
<td
i18n-id="timelion.help.expressions.examples.twoExpressionsDescription"
i18n-default-message="{descriptionTitle} Two expressions on the same chart."
i18n-values="{
descriptionTitle: '<strong>' + translations.twoExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td><code>.es(*).color(#f66), .es(US).bars(1)</code></td>
<td>
<strong>Custom styling.</strong> Colorizes the first series red
and uses 1 pixel wide bars for the second series.
</td>
<td
i18n-id="timelion.help.expressions.examples.customStylingDescription"
i18n-default-message="{descriptionTitle} Colorizes the first series red and
uses 1 pixel wide bars for the second series."
i18n-values="{
descriptionTitle: '<strong>' + translations.customStylingDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td>
<code>.es(*).color(#f66).lines(fill=3),
.es(US).bars(1).points(radius=3, weight=1)</code>
</td>
<td>
<strong>Named arguments.</strong> Forget trying to remember what
order you need to specify arguments in, use named arguments to make
the expressions easier to read and write.
</td>
<td
i18n-id="timelion.help.expressions.examples.namedArgumentsDescription"
i18n-default-message="{descriptionTitle} Forget trying to remember what order you need
to specify arguments in, use named arguments to make
the expressions easier to read and write."
i18n-values="{
descriptionTitle: '<strong>' + translations.namedArgumentsDescriptionTitle + '</strong>',
}"
></td>
</tr>
<tr>
<td><code>(.es(*), .es(GB)).points()</code></td>
<td>
<strong>Grouped expressions.</strong> You can also chain groups
of expressions to functions. Here, both series are shown as
points instead of lines.
</td>
<td
i18n-id="timelion.help.expressions.examples.groupedExpressionsDescription"
i18n-default-message="{descriptionTitle} You can also chain groups of expressions to
functions. Here, both series are shown as points instead of lines."
i18n-values="{
descriptionTitle: '<strong>' + translations.groupedExpressionsDescriptionTitle + '</strong>',
}"
></td>
</tr>
</table>
<p>
Timelion provides additional view transformation functions you can use
to customize the appearance of your charts. For the complete list, see
the <a ng-click="setPage(0)">Function reference</a>.
<span
i18n-id="timelion.help.expressions.paragraph4"
i18n-default-message="Timelion provides additional view transformation functions you can use
to customize the appearance of your charts. For the complete list, see the"
i18n-context="Part of composite text
timelion.help.expressions.paragraph4 +
timelion.help.expressions.functionReferenceLinkText"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.expressions.functionReferenceLinkText"
i18n-default-message="Function reference"
i18n-context="Part of composite text
timelion.help.expressions.paragraph4 +
timelion.help.expressions.functionReferenceLinkText"
></a>.
</p>
</div>
<div class="timHelp__buttons">
@ -292,7 +486,7 @@
ng-click="setPage(page-1)"
class="kuiButton kuiButton--primary"
>
Previous
{{translations.previousButtonLabel}}
</button>
@ -300,50 +494,93 @@
ng-click="setPage(page+1)"
class="kuiButton kuiButton--primary"
>
Next
{{translations.nextButtonLabel}}
</button>
</div>
</div>
<div ng-show="page === 5">
<div>
<h2>Transforming your data: the real fun begins!</h2>
<h2
i18n-id="timelion.help.dataTransformingTitle"
i18n-default-message="Transforming your data: the real fun begins!"
></h2>
<p
i18n-id="timelion.help.dataTransforming.paragraph1"
i18n-default-message="Now that you've mastered the basics, it's time to unleash the power of
Timelion. Let's figure out what percentage some subset of our data
represents of the whole, over time. For example, what percentage of
our web traffic comes from the US?"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph2"
i18n-default-message="First, we need to find all events that contain US: {esUsQuery}."
i18n-values="{ esUsQuery: '<code>.es(\'US\')</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph3"
i18n-default-message="Next, we want to calculate the ratio of US events to the whole.
To divide {us} by everything, we can use the {divide} function:
{divideDataQuery}."
i18n-values="{
us: '<code>\'US\'</code>',
divide: '<code>divide</code>',
divideDataQuery: '<code>.es(\'US\').divide(.es())</code>',
}"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph4"
i18n-default-message="Not bad, but this gives us a number between 0 and 1. To convert it
to a percentage, simply multiply by 100: {multiplyDataQuery}."
i18n-values="{ multiplyDataQuery: '<code>.es(\'US\').divide(.es()).multiply(100)</code>' }"
></p>
<p
i18n-id="timelion.help.dataTransforming.paragraph5"
i18n-default-message="Now we know what percentage of our traffic comes from the US, and
can see how it has changed over time! Timelion has a number of
built-in arithmetic functions, such as {sum}, {subtract}, {multiply},
and {divide}. Many of these can take a series or a number. There are
also other useful data transformation functions, such as
{movingaverage}, {abs}, and {derivative}."
i18n-values="{
sum: '<code>sum</code>',
subtract: '<code>subtract</code>',
multiply: '<code>multiply</code>',
divide: '<code>divide</code>',
movingaverage: '<code>movingaverage</code>',
abs: '<code>abs</code>',
derivative: '<code>derivative</code>',
}"
></p>
<p>
Now that you've mastered the basics, it's time to unleash the power of
Timelion. Let's figure out what percentage some subset of our data
represents of the whole, over time. For example, what percentage of
our web traffic comes from the US?
</p>
<p>
First, we need to find all events that contain US:
<code>.es('US')</code>.
</p>
<p>
Next, we want to calculate the ratio of US events to the whole. To
divide <code>'US'</code> by everything, we can use the
<code>divide</code> function: <code>.es('US').divide(.es())</code>.
</p>
<p>
Not bad, but this gives us a number between 0 and 1. To convert it
to a percentage, simply multiply by 100:
<code>.es('US').divide(.es()).multiply(100)</code>.
</p>
<p>
Now we know what percentage of our traffic comes from the US, and
can see how it has changed over time!
Timelion has a number of built-in arithmetic functions, such as
<code>sum</code>, <code>subtract</code>, <code>multiply</code>, and
<code>divide</code>. Many of these can take a series or a number.
There are also other useful data transformation functions, such as
<code>movingaverage</code>, <code>abs</code>, and
<code>derivative</code>.
</p>
<p>Now that you're familiar with the syntax, refer to the
<a ng-click="setPage(0)">Function reference</a> to see
how to use all of the available Timelion functions. You can view
the reference at any time by clicking <strong>Docs</strong>
in the Kibana toolbar. To get back to this tutorial, click the
<strong>Tutorial</strong> link at the top of the reference.
<span
i18n-id="timelion.help.dataTransforming.paragraph6Part1"
i18n-default-message="Now that you're familiar with the syntax, refer to the"
i18n-context="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></span>
<a
ng-click="setPage(0)"
i18n-id="timelion.help.dataTransforming.functionReferenceLinkText"
i18n-default-message="Function reference"
i18n-context="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></a>
<span
i18n-id="timelion.help.dataTransforming.paragraph6Part2"
i18n-default-message="to see how to use all of the available Timelion functions.
You can view the reference at any time by clicking \{Docs\}
in the Kibana toolbar. To get back to this tutorial, click the
\{Tutorial\} link at the top of the reference."
i18n-context="Part of composite text
timelion.help.dataTransforming.paragraph6Part1 +
timelion.help.dataTransforming.functionReferenceLinkText +
timelion.help.dataTransforming.paragraph6Part2"
></span>
</p>
</div>
<div class="timHelp__buttons">
@ -352,7 +589,7 @@
ng-click="setPage(page-1)"
class="kuiButton kuiButton--primary"
>
Previous
{{translations.previousButtonLabel}}
</button>
@ -361,29 +598,34 @@
ng-click="opts.dontShowHelp()"
class="kuiButton kuiButton--hollow"
>
Don't show this again
{{translations.dontShowHelpButtonLabel}}
</button>
</div>
</div>
</div>
<div ng-show="page === 0">
<h2 class="kuiLocalDropdownTitle">
Help
</h2>
<h2
class="kuiLocalDropdownTitle"
i18n-id="timelion.help.mainPageTitle"
i18n-default-message="Help"
></h2>
<tabset>
<tab heading="Function reference">
<tab heading="{{ ::'timelion.help.mainPage.functionReferenceTitle' | i18n: { defaultMessage: 'Function reference' } }}">
<div class="list-group-item list-group-item--noBorder">
<div class="kuiLocalDropdownHelpText">
Click any function for more information. Just getting started?
<span
i18n-id="timelion.help.mainPage.functionReference.gettingStartedText"
i18n-default-message="Click any function for more information. Just getting started?"
></span>
<a
i18n-id="timelion.help.mainPage.functionReference.welcomePageLinkText"
i18n-default-message="Check out the tutorial"
class="kuiLink"
ng-click="setPage(1)"
kbn-accessible-click
>
Check out the tutorial
</a>.
></a>.
</div>
<div class="timHelp__functions">
@ -409,9 +651,21 @@
ng-show="function.args.length > (function.chainable ? 1: 0)"
>
<thead>
<th scope="col">Argument Name</th>
<th scope="col">Accepted Types</th>
<th scope="col">Information</th>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.argumentNameColumnLabel"
i18n-default-message="Argument Name"
></th>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.acceptedTypesColumnLabel"
i18n-default-message="Accepted Types"
></th>
<th
scope="col"
i18n-id="timelion.help.mainPage.functionReference.detailsTable.informationColumnLabel"
i18n-default-message="Information"
></th>
</thead>
<tr
ng-repeat="arg in function.args"
@ -423,10 +677,10 @@
</tr>
</table>
<div ng-hide="function.args.length > (function.chainable ? 1: 0)">
<em>
This function does not accept any arguments.
Well that's simple, isn't it?
</em>
<em
i18n-id="timelion.help.mainPage.functionReference.noArgumentsFunctionErrorMessage"
i18n-default-message="This function does not accept any arguments. Well that's simple, isn't it?"
></em>
</div>
</div>
</td>
@ -436,26 +690,50 @@
</div>
</tab>
<tab heading="Keyboard tips">
<tab heading="{{ ::'timelion.help.mainPage.keyboardTipsTitle' | i18n: { defaultMessage: 'Keyboard tips' } }}">
<div class="list-group-item list-group-item--noBorder">
<!-- General editing tips -->
<dl class="dl-horizontal">
<dd><strong>General editing</strong></dd>
<dd>
<strong
i18n-id="timelion.help.mainPage.keyboardTips.generalEditingTitle"
i18n-default-message="General editing"
></strong></dd>
<dt></dt>
<dt>Ctrl/Cmd + Enter</dt>
<dd>Submit request</dd>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.generalEditing.submitRequestText"
i18n-default-message="Submit request"
></dd>
</dl>
<!-- Auto complete tips -->
<dl class="dl-horizontal">
<dt></dt>
<dd><strong>When auto-complete is visible</strong></dd>
<dt>Down arrow</dt>
<dd>Switch focus to auto-complete menu. Use arrows to further select a term</dd>
<dd>
<strong
i18n-id="timelion.help.mainPage.keyboardTips.autoCompleteTitle"
i18n-default-message="When auto-complete is visible"
></strong>
</dd>
<dt
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.downArrowLabel"
i18n-default-message="Down arrow"
></dt>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.downArrowDescription"
i18n-default-message="Switch focus to auto-complete menu. Use arrows to further select a term"
></dd>
<dt>Enter/Tab</dt>
<dd>Select the currently selected or the top most term in auto-complete menu</dd>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.enterTabDescription"
i18n-default-message="Select the currently selected or the top most term in auto-complete menu"
></dd>
<dt>Esc</dt>
<dd>Close auto-complete menu</dd>
<dd
i18n-id="timelion.help.mainPage.keyboardTips.autoComplete.escDescription"
i18n-default-message="Close auto-complete menu"
></dd>
</dl>
</div>
</tab>

View file

@ -24,7 +24,7 @@ import moment from 'moment';
const app = uiModules.get('apps/timelion', []);
app.directive('timelionHelp', function ($http) {
app.directive('timelionHelp', function ($http, i18n) {
return {
restrict: 'E',
template,
@ -38,6 +38,52 @@ app.directive('timelionHelp', function ($http) {
$scope.es = {
invalidCount: 0
};
$scope.translations = {
nextButtonLabel: i18n('timelion.help.nextPageButtonLabel', {
defaultMessage: 'Next',
}),
previousButtonLabel: i18n('timelion.help.previousPageButtonLabel', {
defaultMessage: 'Previous',
}),
dontShowHelpButtonLabel: i18n('timelion.help.dontShowHelpButtonLabel', {
defaultMessage: `Don't show this again`,
}),
strongNextText: i18n('timelion.help.welcome.content.strongNextText', {
defaultMessage: 'Next',
}),
emphasizedEverythingText: i18n('timelion.help.welcome.content.emphasizedEverythingText', {
defaultMessage: 'everything',
}),
notValidAdvancedSettingsPath: i18n('timelion.help.configuration.notValid.advancedSettingsPathText', {
defaultMessage: 'Management / Kibana / Advanced Settings'
}),
validAdvancedSettingsPath: i18n('timelion.help.configuration.valid.advancedSettingsPathText', {
defaultMessage: 'Management/Kibana/Advanced Settings',
}),
esAsteriskQueryDescription: i18n('timelion.help.querying.esAsteriskQueryDescriptionText', {
defaultMessage: 'hey Elasticsearch, find everything in my default index',
}),
esIndexQueryDescription: i18n('timelion.help.querying.esIndexQueryDescriptionText', {
defaultMessage: 'use * as the q (query) for the logstash-* index',
}),
strongAddText: i18n('timelion.help.expressions.strongAddText', {
defaultMessage: 'Add',
}),
twoExpressionsDescriptionTitle: i18n('timelion.help.expressions.examples.twoExpressionsDescriptionTitle', {
defaultMessage: 'Double the fun.',
}),
customStylingDescriptionTitle: i18n('timelion.help.expressions.examples.customStylingDescriptionTitle', {
defaultMessage: 'Custom styling.',
}),
namedArgumentsDescriptionTitle: i18n('timelion.help.expressions.examples.namedArgumentsDescriptionTitle', {
defaultMessage: 'Named arguments.',
}),
groupedExpressionsDescriptionTitle: i18n('timelion.help.expressions.examples.groupedExpressionsDescriptionTitle', {
defaultMessage: 'Grouped expressions.',
}),
};
getFunctions();
checkElasticsearch();
}
@ -73,7 +119,7 @@ app.directive('timelionHelp', function ($http) {
} catch (e) {
if (_.get(resp, 'data.resp.message')) return _.get(resp, 'data.resp.message');
if (_.get(resp, 'data.resp.output.payload.message')) return _.get(resp, 'data.resp.output.payload.message');
return 'Unknown error';
return i18n('timelion.help.unknownErrorMessage', { defaultMessage: 'Unknown error' });
}
}());
}

View file

@ -1,13 +1,13 @@
<input
input-focus
aria-label="Custom interval"
aria-label="{{ ::'timelion.intervals.customIntervalAriaLabel' | i18n: { defaultMessage: 'Custom interval' } }}"
class="kuiTextInput timInterval__input"
ng-show="interval === 'other'"
ng-class="{ 'timInterval__input--compact': interval === 'other' }"
ng-model="otherInterval"
><select
id="timelionInterval"
aria-label="Select interval"
aria-label="{{ ::'timelion.intervals.selectIntervalAriaLabel' | i18n: { defaultMessage: 'Select interval' } }}"
class="kuiSelect timInterval__presets"
ng-class="{ 'timInterval__presets--compact': interval === 'other'}"
ng-model="interval"

View file

@ -42,7 +42,7 @@ app.directive('timelionInterval', function ($compile, $timeout) {
'1w': '1 week',
'1M': '1 month',
'1y': '1 year',
'other': 'other'
'other': 'other',
};
$scope.$watch('model', function (newVal, oldVal) {

View file

@ -10,8 +10,13 @@
<span class="fa fa-bolt" ng-click="showStats = !showStats"></span>
&nbsp;
<span class="timApp__stats" ng-show="showStats">
Query Time {{stats.queryTime - stats.invokeTime}}ms /
Processing Time {{stats.sheetTime - stats.queryTime}}ms
<span
i18n-id="timelion.topNavMenu.statsDescription"
i18n-default-message="Query Time {queryTime}ms / Processing Time {processingTime}ms"
i18n-values="{
queryTime: stats.queryTime - stats.invokeTime,
processingTime: stats.sheetTime - stats.queryTime,
}"></span>
</span>
</span>
</div>
@ -43,7 +48,7 @@
<button
type="submit"
aria-label="Search"
aria-label="{{ ::'timelion.search.submitAriaLabel' | i18n: { defaultMessage: 'Search' } }}"
class="kuiButton kuiButton--primary fullWidth kuiVerticalRhythmSmall"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-play"></span>

View file

@ -17,7 +17,7 @@
* under the License.
*/
export default function Panel(name, config) {
export default function Panel(name, config, i18n) {
this.name = name;
@ -25,7 +25,12 @@ export default function Panel(name, config) {
this.render = config.render;
if (!config.render) throw new Error ('Panel must have a rendering function');
if (!config.render) {
throw new Error (
i18n('timelion.panels.noRenderFunctionErrorMessage', {
defaultMessage: 'Panel must have a rendering function'
})
);
}
}

View file

@ -20,8 +20,8 @@
import Panel from '../panel';
import panelRegistry from '../../lib/panel_registry';
panelRegistry.register(function timeChartProvider(Private) {
panelRegistry.register(function timeChartProvider(Private, i18n) {
// Schema is broken out so that it may be extended for use in other plugins
// Its also easier to test.
return new Panel('timechart', Private(require('./schema'))());
return new Panel('timechart', Private(require('./schema'))(), i18n);
});

View file

@ -19,11 +19,18 @@
import moment from 'moment';
export default function xaxisFormatterProvider(config) {
export default function xaxisFormatterProvider(config, i18n) {
function getFormat(esInterval) {
const parts = esInterval.match(/(\d+)(ms|s|m|h|d|w|M|y|)/);
if (parts == null || parts[1] == null || parts[2] == null) throw new Error ('Unknown interval');
if (parts == null || parts[1] == null || parts[2] == null) {
throw new Error (
i18n('timelion.panels.timechart.unknownIntervalErrorMessage', {
defaultMessage: 'Unknown interval',
})
);
}
const interval = moment.duration(Number(parts[1]), parts[2]);

View file

@ -1,7 +1,9 @@
<form role="form" ng-submit="fetch()">
<h2 class="kuiLocalDropdownTitle">
Open Sheet
</h2>
<h2
class="kuiLocalDropdownTitle"
i18n-id="timelion.topNavMenu.openSheetTitle"
i18n-default-message="Open Sheet"
></h2>
<saved-object-finder
type="timelion-sheet"

View file

@ -1,11 +1,18 @@
<div class="list-group">
<button class="list-group-item" ng-click="section = 'sheet'" type="button">
<h4 class="list-group-item-heading">Save entire Timelion sheet</h4>
<p class="list-group-item-text">
You want this option if you mostly use Timelion expressions from within the Timelion app and don't need to
add Timelion charts to Kibana dashboards. You may also want this if you make use of references to other
panels.
</p>
<h4
class="list-group-item-heading"
i18n-id="timelion.topNavMenu.save.saveEntireSheetTitle"
i18n-default-message="Save entire Timelion sheet"
></h4>
<p
class="list-group-item-text"
i18n-id="timelion.topNavMenu.save.saveEntireSheetDescription"
i18n-default-message="You want this option if you mostly use Timelion expressions from within
the Timelion app and don't need to add Timelion charts to Kibana
dashboards. You may also want this if you make use of references to
other panels."
></p>
</button>
<div class="list-group-item" ng-show="section == 'sheet'">
@ -13,17 +20,17 @@
<label
for="savedSheet"
class="kuiLabel kuiVerticalRhythmSmall"
>
Save sheet as
</label>
i18n-id="timelion.topNavMenu.save.saveEntireSheetLabel"
i18n-default-message="Save sheet as"
></label>
<input
id="savedSheet"
ng-model="opts.savedSheet.title"
input-focus="select"
class="form-control kuiVerticalRhythmSmall"
placeholder="Name this sheet..."
aria-label="Name"
placeholder="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputPlaceholder' | i18n: { defaultMessage: 'Name this sheet...' } }}"
aria-label="{{ ::'timelion.topNavMenu.save.saveEntireSheet.inputAriaLabel' | i18n: { defaultMessage: 'Name' } }}"
>
<saved-object-save-as-check-box
@ -35,34 +42,63 @@
ng-disabled="!opts.savedSheet.title"
type="submit"
class="kuiButton kuiButton--primary kuiVerticalRhythmSmall"
>
Save
</button>
i18n-id="timelion.topNavMenu.save.saveEntireSheet.submitButtonLabel"
i18n-default-message="Save"
></button>
</form>
</div>
<button class="list-group-item" ng-click="section = 'expression'" type="button">
<h4 class="list-group-item-heading">Save current expression as Kibana dashboard panel</h4>
<p class="list-group-item-text">
Need to add a chart to a Kibana dashboard? We can do that! This option will save your currently selected
expression as a panel that can be added to Kibana dashboards as you would add anything else. Note, if you
use references to other panels you will need to remove the refences by copying the referenced expression
directly into the expression you are saving. Click a chart to select a different expression to save.
</p>
<h4
class="list-group-item-heading"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelTitle"
i18n-default-message="Save current expression as Kibana dashboard panel"
></h4>
<p
class="list-group-item-text"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelDescription"
i18n-default-message="Need to add a chart to a Kibana dashboard? We can do that! This option
will save your currently selected expression as a panel that can be
added to Kibana dashboards as you would add anything else. Note, if you
use references to other panels you will need to remove the refences by
copying the referenced expression directly into the expression you are
saving. Click a chart to select a different expression to save."
></p>
</button>
<div class="list-group-item" ng-show="section == 'expression'">
<form role="form" class="container-fluid" ng-submit="opts.saveExpression(panelTitle)">
<div class="form-group">
<label class="control-label">Currently selected expression</label>
<label
class="control-label"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanel.selectedExpressionLabel"
i18n-default-message="Currently selected expression"
></label>
<code>{{opts.state.sheet[opts.state.selected]}}</code>
</div>
<div class="form-group">
<label for="savedExpression" class="control-label">Save expression as</label>
<input id="savedExpression" ng-model="panelTitle" input-focus="select" class="form-control" placeholder="Name this panel">
<label
for="savedExpression"
class="control-label"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanelLabel"
i18n-default-message="Save expression as"
></label>
<input
id="savedExpression"
ng-model="panelTitle"
input-focus="select"
class="form-control"
placeholder="{{ ::'timelion.topNavMenu.save.saveAsDashboardPanel.inputPlaceholder' | i18n: { defaultMessage: 'Name this panel' } }}"
>
</div>
<div class="form-group">
<button ng-disabled="!panelTitle" type="submit" class="kuiButton kuiButton--primary">Save</button>
<button
ng-disabled="!panelTitle"
type="submit"
class="kuiButton kuiButton--primary"
i18n-id="timelion.topNavMenu.save.saveAsDashboardPanel.submitButtonLabel"
i18n-default-message="Save"
></button>
</div>
</form>
</div>

View file

@ -1,11 +1,17 @@
<form role="form">
<h2 class="kuiLocalDropdownTitle">
Sheet options
</h2>
<h2
class="kuiLocalDropdownTitle"
i18n-id="timelion.topNavMenu.sheetOptionsTitle"
i18n-default-message="Sheet options"
></h2>
<div>
<div class="form-group col-md-6">
<label for="timelionColCount">Columns <small>Column count must divide evenly into 12</small></label>
<label
for="timelionColCount"
i18n-id="timelion.topNavMenu.options.columnsCountLabel"
i18n-default-message="Columns (Column count must divide evenly into 12)"
></label>
<select class="form-control"
id="timelionColCount"
ng-change="opts.search()"
@ -14,7 +20,11 @@
</select>
</div>
<div class="form-group col-md-6">
<label for="timelionRowCount">Rows <small>This is a target based on the current window height</small></label>
<label
for="timelionRowCount"
i18n-id="timelion.topNavMenu.options.rowsCountLabel"
i18n-default-message="Rows (This is a target based on the current window height)"
></label>
<select class="form-control"
id="timelionRowCount"
ng-change="opts.search()"

View file

@ -22,12 +22,14 @@ import {
FeatureCatalogueCategory,
} from 'ui/registry/feature_catalogue';
FeatureCatalogueRegistryProvider.register(() => {
FeatureCatalogueRegistryProvider.register(i18n => {
return {
id: 'timelion',
title: 'Timelion',
description:
'Use an expression language to analyze time series data and visualize the results.',
description: i18n('timelion.registerFeatureDescription', {
defaultMessage:
'Use an expression language to analyze time series data and visualize the results.',
}),
icon: 'timelionApp',
path: '/app/timelion',
showOnHomePage: false,

View file

@ -33,7 +33,7 @@ import editorConfigTemplate from './timelion_vis_params.html';
// register the provider with the visTypes registry so that other know it exists
VisTypesRegistryProvider.register(TimelionVisProvider);
export default function TimelionVisProvider(Private) {
export default function TimelionVisProvider(Private, i18n) {
const VisFactory = Private(VisFactoryProvider);
const timelionRequestHandler = Private(TimelionRequestHandlerProvider);
@ -43,7 +43,9 @@ export default function TimelionVisProvider(Private) {
name: 'timelion',
title: 'Timelion',
icon: 'visTimelion',
description: 'Build time-series using functional expressions',
description: i18n('timelion.timelionDescription', {
defaultMessage: 'Build time-series using functional expressions',
}),
category: CATEGORY.TIME,
visConfig: {
defaults: {

View file

@ -1,6 +1,10 @@
<div class="kuiSideBarSection">
<div class="form-group">
<label for="timelionInterval">Interval</label>
<label
for="timelionInterval"
i18n-id="timelion.vis.intervalLabel"
i18n-default-message="Interval"
></label>
<div class="form-group">
<timelion-interval model="editorState.params.interval"></timelion-interval>
</div>
@ -8,7 +12,10 @@
<div class="form-group">
<div>
<label>Timelion Expression</label>
<label
i18n-id="timelion.vis.expressionLabel"
i18n-default-message="Timelion Expression"
></label>
</div>
<timelion-expression-input

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
// Upsampling of non-cumulative sets
// Good: average, min, max
@ -27,7 +28,12 @@ import _ from 'lodash';
export default function carry(dataTuples, targetTuples) {
if (dataTuples.length > targetTuples.length) {
throw new Error (`Don't use the 'carry' fit method to down sample, use 'scale' or 'average'`);
throw new Error (
i18n.translate('timelion.fitFunctions.carry.downSampleErrorMessage', {
defaultMessage: `Don't use the 'carry' fit method to down sample, use 'scale' or 'average'`,
description: '"carry", "scale" and "average" are parameter values that must not be translated.',
})
);
}
let currentCarry = dataTuples[0][1];

View file

@ -20,6 +20,8 @@
import _ from 'lodash';
import Promise from 'bluebird';
import { i18n } from '@kbn/i18n';
import parseSheet from './lib/parse_sheet.js';
import parseDateMath from '../lib/date_math.js';
import repositionArguments from './lib/reposition_arguments.js';
@ -79,7 +81,14 @@ export default function chainRunner(tlConfig) {
case 'seriesList':
return item;
}
throw new Error ('Argument type not supported: ' + JSON.stringify(item));
throw new Error(
i18n.translate('timelion.serverSideErrors.unknownArgumentTypeErrorMessage', {
defaultMessage: 'Argument type not supported: {argument}',
values: {
argument: JSON.stringify(item),
},
})
);
} else {
return item;
}

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
// Only applies to already resolved arguments
export default function indexArguments(functionDef, orderedArgs) {
@ -26,7 +27,16 @@ export default function indexArguments(functionDef, orderedArgs) {
// This almost certainly is not required
const allowedLength = functionDef.extended ? functionDef.args.length + 2 : functionDef.args.length;
if (orderedArgs.length > allowedLength) throw new Error ('Too many arguments passed to: ' + functionDef.name);
if (orderedArgs.length > allowedLength) {
throw new Error (
i18n.translate('timelion.serverSideErrors.argumentsOverflowErrorMessage', {
defaultMessage: 'Too many arguments passed to: {functionName}',
values: {
functionName: functionDef.name,
},
})
);
}
const indexedArgs = {};
// Check and index each known argument

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
@ -30,7 +31,15 @@ export default function parseSheet(sheet) {
return Parser.parse(plot).tree;
} catch (e) {
if (e.expected) {
throw new Error('Expected: ' + e.expected[0].description + ' @ character ' + e.column);
throw new Error(
i18n.translate('timelion.serverSideErrors.sheetParseErrorMessage', {
defaultMessage: 'Expected: {expectedDescription} @ character {column}',
values: {
expectedDescription: e.expected[0].description,
column: e.column,
},
})
);
} else {
throw e;
}

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
// Applies to unresolved arguments in the AST
export default function repositionArguments(functionDef, unorderedArgs) {
@ -58,7 +59,17 @@ export default function repositionArguments(functionDef, unorderedArgs) {
value = unorderedArg;
}
if (!argDef) throw new Error('Unknown argument to ' + functionDef.name + ': ' + (unorderedArg.name || ('#' + i)));
if (!argDef) {
throw new Error(
i18n.translate('timelion.serverSideErrors.unknownArgumentErrorMessage', {
defaultMessage: 'Unknown argument to {functionName}: {argumentName}',
values: {
functionName: functionDef.name,
argumentName: (unorderedArg.name || ('#' + i)),
},
})
);
}
if (storeAsArray) {
args[targetIndex] = args[targetIndex] || [];

View file

@ -19,6 +19,7 @@
import argType from './arg_type';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
export default function validateArgFn(functionDef) {
return function validateArg(value, name, argDef) {
@ -36,7 +37,17 @@ export default function validateArgFn(functionDef) {
else return false;
if (!isCorrectType) {
throw new Error (functionDef.name + '(' + name + ') must be one of ' + JSON.stringify(required) + '. Got: ' + type);
throw new Error(
i18n.translate('timelion.serverSideErrors.wrongFunctionArgumentTypeErrorMessage', {
defaultMessage: '{functionName}({argumentName}) must be one of {requiredTypes}. Got: {actualType}',
values: {
functionName: functionDef.name,
argumentName: name,
requiredTypes: JSON.stringify(required),
actualType: type,
},
})
);
}
};
}

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import parseDateMath from '../../lib/date_math.js';
import toMS from '../../lib/to_milliseconds.js';
@ -26,9 +28,17 @@ export default function validateTime(time, tlConfig) {
const bucketCount = span / interval;
const maxBuckets = tlConfig.settings['timelion:max_buckets'];
if (bucketCount > maxBuckets) {
throw new Error('Max buckets exceeded: ' +
Math.round(bucketCount) + ' of ' + maxBuckets + ' allowed. ' +
'Choose a larger interval or a shorter time span');
throw new Error(
i18n.translate('timelion.serverSideErrors.bucketsOverflowErrorMessage', {
defaultMessage:
'Max buckets exceeded: {bucketCount} of {maxBuckets} allowed. ' +
'Choose a larger interval or a shorter time span',
values: {
bucketCount: Math.round(bucketCount),
maxBuckets,
},
})
);
}
return true;
}

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import loadFunctions from '../load_functions.js';
const fitFunctions = loadFunctions('fit_functions');
import TimelionFunction from './timelion_function';
@ -40,16 +41,25 @@ export default class Datasource extends TimelionFunction {
config.args.push({
name: 'offset',
types: ['string', 'null'],
help: 'Offset the series retrieval by a date expression, ' +
'e.g., -1M to make events from one month ago appear as if they are happening now. ' +
'Offset the series relative to the charts overall time range, by using the value "timerange", ' +
'e.g. "timerange:-2" will specify an offset that is twice the overall chart time range to the past.'
help: i18n.translate('timelion.help.functions.common.args.offsetHelpText', {
defaultMessage:
'Offset the series retrieval by a date expression, e.g., -1M to make events from ' +
'one month ago appear as if they are happening now. Offset the series relative to the charts ' +
'overall time range, by using the value "timerange", e.g. "timerange:-2" will specify an offset ' +
'that is twice the overall chart time range to the past.',
}),
});
config.args.push({
name: 'fit',
types: ['string', 'null'],
help: 'Algorithm to use for fitting series to the target time span and interval. Available: ' + _.keys(fitFunctions).join(', ')
help: i18n.translate('timelion.help.functions.common.args.fitHelpText', {
defaultMessage:
'Algorithm to use for fitting series to the target time span and interval. Available: {fitFunctions}',
values: {
fitFunctions: _.keys(fitFunctions).join(', '),
},
})
});
// Wrap the original function so we can modify inputs/outputs with offset & fit

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -28,7 +29,9 @@ export default new Chainable('abs', {
types: ['seriesList']
}
],
help: 'Return the absolute value of each value in the series list',
help: i18n.translate('timelion.help.functions.absHelpText', {
defaultMessage: 'Return the absolute value of each value in the series list',
}),
fn: function absFn(args) {
return alter(args, function (eachSeries) {
const data = _.map(eachSeries.data, function (point) {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../../lib/alter.js';
import Chainable from '../../lib/classes/chainable';
import _ from 'lodash';
@ -40,11 +41,21 @@ export default new Chainable('aggregate', {
{
name: 'function',
types: ['string'],
help: 'One of ' + _.keys(functions).join(', ')
help: i18n.translate('timelion.help.functions.aggregate.args.functionHelpText', {
defaultMessage: 'One of {functions}',
values: {
functions: _.keys(functions).join(', '),
},
}),
}
],
help: 'Creates a static line based on result of processing all points in the series.' +
' Available functions: ' + _.keys(functions).join(', '),
help: i18n.translate('timelion.help.functions.aggregateHelpText', {
defaultMessage:
'Creates a static line based on result of processing all points in the series. Available functions: {functions}',
values: {
functions: _.keys(functions).join(', '),
},
}),
fn: function aggregateFn(args) {
const fn = functions[args.byName.function];
if (!fn) throw new Error('.aggregate() function must be one of: ' + _.keys(functions).join(', '));

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -29,15 +30,21 @@ export default new Chainable('bars', {
{
name: 'width',
types: ['number', 'null'],
help: 'Width of bars in pixels'
help: i18n.translate('timelion.help.functions.bars.args.widthHelpText', {
defaultMessage: 'Width of bars in pixels',
}),
},
{
name: 'stack',
types: ['boolean', 'null'],
help: 'Should bars be stacked, true by default'
help: i18n.translate('timelion.help.functions.bars.args.stackHelpText', {
defaultMessage: 'Should bars be stacked, true by default',
}),
}
],
help: 'Show the seriesList as bars',
help: i18n.translate('timelion.help.functions.barsHelpText', {
defaultMessage: 'Show the seriesList as bars',
}),
fn: function barsFn(args) {
return alter(args, function (eachSeries, width, stack) {
eachSeries.bars = eachSeries.bars || {};

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
import tinygradient from 'tinygradient';
@ -30,11 +31,16 @@ export default new Chainable('color', {
{
name: 'color',
types: ['string'],
help: 'Color of series, as hex, e.g., #c6c6c6 is a lovely light grey. If you specify multiple colors, and have ' +
'multiple series, you will get a gradient, e.g., "#00B1CC:#00FF94:#FF3A39:#CC1A6F"'
help: i18n.translate('timelion.help.functions.color.args.colorHelpText', {
defaultMessage:
'Color of series, as hex, e.g., #c6c6c6 is a lovely light grey. If you specify multiple \
colors, and have multiple series, you will get a gradient, e.g., "#00B1CC:#00FF94:#FF3A39:#CC1A6F"',
}),
}
],
help: 'Change the color of the series',
help: i18n.translate('timelion.help.functions.colorHelpText', {
defaultMessage: 'Change the color of the series',
}),
fn: function colorFn(args) {
const colors = args.byName.color.split(':');
const gradientStops = args.byName.inputSeries.list.length;
@ -55,7 +61,11 @@ export default new Chainable('color', {
} else if (colors.length === 1) {
eachSeries.color = colors[0];
} else {
throw new Error('color not provided');
throw new Error(
i18n.translate('timelion.serverSideErrors.colorFunction.colorNotProvidedErrorMessage', {
defaultMessage: 'color not provided',
})
);
}
return eachSeries;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -31,53 +32,80 @@ export default new Chainable('condition', {
{
name: 'operator', // <, <=, >, >=, ==, !=
types: ['string'],
help: 'comparison operator to use for comparison, valid operators are eq (equal), ne (not equal), lt (less than), lte ' +
'(less than equal), gt (greater than), gte (greater than equal)',
help: i18n.translate('timelion.help.functions.condition.args.operatorHelpText', {
defaultMessage:
'comparison operator to use for comparison, valid operators are eq (equal), ' +
'ne (not equal), lt (less than), lte (less than equal), gt (greater than), gte (greater than equal)',
}),
suggestions: [
{
name: 'eq',
help: 'equal',
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.eqHelpText', {
defaultMessage: 'equal',
}),
},
{
name: 'ne',
help: 'not equal'
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.neHelpText', {
defaultMessage: 'not equal',
}),
},
{
name: 'lt',
help: 'less than'
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.ltHelpText', {
defaultMessage: 'less than',
}),
},
{
name: 'lte',
help: 'less than equal'
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.lteHelpText', {
defaultMessage: 'less than equal',
}),
},
{
name: 'gt',
help: 'greater than'
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.gtHelpText', {
defaultMessage: 'greater than',
}),
},
{
name: 'gte',
help: 'greater than equal'
}
]
help: i18n.translate('timelion.help.functions.condition.args.operator.suggestions.gteHelpText', {
defaultMessage: 'greater than equal',
}),
},
],
},
{
name: 'if',
types: ['number', 'seriesList', 'null'],
help: 'The value to which the point will be compared. If you pass a seriesList here the first series will be used'
help: i18n.translate('timelion.help.functions.condition.args.ifHelpText', {
defaultMessage:
'The value to which the point will be compared. If you pass a seriesList here the first series will be used',
}),
},
{
name: 'then',
types: ['number', 'seriesList', 'null'],
help: 'The value the point will be set to if the comparison is true. If you pass a seriesList here the first series will be used'
help: i18n.translate('timelion.help.functions.condition.args.thenHelpText', {
defaultMessage:
'The value the point will be set to if the comparison is true. If you pass a seriesList here the first series will be used',
}),
},
{
name: 'else',
types: ['number', 'seriesList', 'null'],
help: 'The value the point will be set to if the comparison is false. If you pass a seriesList here the first series will be used'
help: i18n.translate('timelion.help.functions.condition.args.elseHelpText', {
defaultMessage:
'The value the point will be set to if the comparison is false. If you pass a seriesList here the first series will be used',
}),
}
],
help: 'Compares each point to a number, or the same point in another series using an operator, then sets its value' +
'to the result if the condition proves true, with an optional else.',
help: i18n.translate('timelion.help.functions.conditionHelpText', {
defaultMessage:
'Compares each point to a number, or the same point in another series using an operator, ' +
'then sets its value to the result if the condition proves true, with an optional else.',
}),
aliases: ['if'],
fn: function conditionFn(args) {
const config = args.byName;
@ -87,7 +115,11 @@ export default new Chainable('condition', {
if (argType(source) === 'number') return source;
if (argType(source) === 'null') return null;
if (argType(source) === 'seriesList') return source.list[0].data[i][1];
throw new Error ('must be a number or a seriesList');
throw new Error(
i18n.translate('timelion.serverSideErrors.conditionFunction.wrongArgTypeErrorMessage', {
defaultMessage: 'must be a number or a seriesList',
})
);
}
const ifVal = getNumber(config.if);
@ -109,7 +141,11 @@ export default new Chainable('condition', {
case 'ne':
return point[1] !== ifVal ? thenVal : elseVal;
default:
throw new Error ('Unknown operator');
throw new Error(
i18n.translate('timelion.serverSideErrors.conditionFunction.unknownOperatorErrorMessage', {
defaultMessage: 'Unknown operator',
})
);
}
}());

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -30,10 +31,14 @@ export default new Chainable('cusum', {
{
name: 'base',
types: ['number'],
help: 'Number to start at. Basically just adds this to the beginning of the series'
help: i18n.translate('timelion.help.functions.cusum.args.baseHelpText', {
defaultMessage: 'Number to start at. Basically just adds this to the beginning of the series',
}),
}
],
help: 'Return the cumulative sum of a series, starting at a base.',
help: i18n.translate('timelion.help.functions.cusumHelpText', {
defaultMessage: 'Return the cumulative sum of a series, starting at a base.',
}),
fn: function cusumFn(args) {
return alter(args, function (eachSeries, base) {
const pairs = eachSeries.data;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -28,7 +29,9 @@ export default new Chainable('derivative', {
types: ['seriesList']
}
],
help: 'Plot the change in values over time.',
help: i18n.translate('timelion.help.functions.derivativeHelpText', {
defaultMessage: 'Plot the change in values over time.',
}),
fn: function derivativeFn(args) {
return alter(args, function (eachSeries) {
const pairs = eachSeries.data;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,10 +30,16 @@ export default new Chainable('divide', {
{
name: 'divisor',
types: ['seriesList', 'number'],
help: 'Number or series to divide by. SeriesList with multiple series will be applied label-wise.'
help: i18n.translate('timelion.help.functions.divide.args.divisorHelpText', {
defaultMessage:
'Number or series to divide by. SeriesList with multiple series will be applied label-wise.',
}),
}
],
help: 'Divides the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.divideHelpText', {
defaultMessage:
'Divides the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
fn: function divideFn(args) {
return reduce(args, function (a, b) {
return a / b;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import Datasource from '../../lib/classes/datasource';
import buildRequest from './lib/build_request';
@ -28,44 +29,73 @@ export default new Datasource('es', {
name: 'q',
types: ['string', 'null'],
multi: true,
help: 'Query in lucene query string syntax'
help: i18n.translate('timelion.help.functions.es.args.qHelpText', {
defaultMessage: 'Query in lucene query string syntax',
}),
},
{
name: 'metric',
types: ['string', 'null'],
multi: true,
help: 'An elasticsearch metric agg: avg, sum, min, max, percentiles or cardinality, followed by a field.' +
' E.g., "sum:bytes", "percentiles:bytes:95,99,99.9" or just "count"'
help: i18n.translate('timelion.help.functions.es.args.metricHelpText', {
defaultMessage:
'An elasticsearch metric agg: avg, sum, min, max, percentiles or cardinality, followed by a field. ' +
'E.g., "sum:bytes", "percentiles:bytes:95,99,99.9" or just "count"',
description:
`avg, sum, min, max, percentiles and cardinality are keywords in the expression ` +
`and must not be translated. Also don't translate the examples.`,
}),
},
{
name: 'split',
types: ['string', 'null'],
multi: true,
help: 'An elasticsearch field to split the series on and a limit. E.g., "hostname:10" to get the top 10 hostnames'
help: i18n.translate('timelion.help.functions.es.args.splitHelpText', {
defaultMessage:
'An elasticsearch field to split the series on and a limit. E.g., "{hostnameSplitArg}" to get the top 10 hostnames',
values: {
hostnameSplitArg: 'hostname:10',
},
}),
},
{
name: 'index',
types: ['string', 'null'],
help: 'Index to query, wildcards accepted. Provide Index Pattern name for scripted fields and ' +
'field name type ahead suggestions for metrics, split, and timefield arguments.'
help: i18n.translate('timelion.help.functions.es.args.indexHelpText', {
defaultMessage:
'Index to query, wildcards accepted. Provide Index Pattern name for scripted fields and ' +
'field name type ahead suggestions for metrics, split, and timefield arguments.',
description: '"metrics", "split" and "timefield" are referring to parameter names and should not be translated.',
}),
},
{
name: 'timefield',
types: ['string', 'null'],
help: 'Field of type "date" to use for x-axis'
help: i18n.translate('timelion.help.functions.es.args.timefieldHelpText', {
defaultMessage: 'Field of type "date" to use for x-axis',
description: '"date" is a field type and should not be translated.',
}),
},
{
name: 'kibana',
types: ['boolean', 'null'],
help: 'Respect filters on Kibana dashboards. Only has an effect when using on Kibana dashboards'
help: i18n.translate('timelion.help.functions.es.args.kibanaHelpText', {
defaultMessage:
'Respect filters on Kibana dashboards. Only has an effect when using on Kibana dashboards',
}),
},
{
name: 'interval', // You really shouldn't use this, use the interval picker instead
types: ['string', 'null'],
help: '**DO NOT USE THIS**. Its fun for debugging fit functions, but you really should use the interval picker'
help: i18n.translate('timelion.help.functions.es.args.intervalHelpText', {
defaultMessage:
`**DO NOT USE THIS**. It's fun for debugging fit functions, but you really should use the interval picker`,
}),
}
],
help: 'Pull data from an elasticsearch instance',
help: i18n.translate('timelion.help.functions.esHelpText', {
defaultMessage: 'Pull data from an elasticsearch instance',
}),
aliases: ['elasticsearch'],
fn: async function esFn(args, tlConfig) {
@ -100,7 +130,16 @@ export default new Datasource('es', {
const { callWithRequest } = tlConfig.server.plugins.elasticsearch.getCluster('data');
const resp = await callWithRequest(tlConfig.request, 'search', body);
if (!resp._shards.total) throw new Error('Elasticsearch index not found: ' + config.index);
if (!resp._shards.total) {
throw new Error(
i18n.translate('timelion.serverSideErrors.esFunction.indexNotFoundErrorMessage', {
defaultMessage: 'Elasticsearch index not found: {index}',
values: {
index: config.index,
},
}),
);
}
return {
type: 'seriesList',
list: toSeriesList(resp.aggregations, config)

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -27,10 +28,12 @@ export default new Chainable('first', {
types: ['seriesList']
}
],
help: 'This is an internal function that simply returns the input seriesList. Don\'t use this',
help: i18n.translate('timelion.help.functions.firstHelpText', {
defaultMessage: `This is an internal function that simply returns the input seriesList. Don't use this`,
}),
fn: function firstFn(args) {
return alter(args, function (eachSeries) {
return eachSeries;
});
}
});
});

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -32,13 +33,21 @@ export default new Chainable('fit', {
{
name: 'mode',
types: ['string'],
help: `The algorithm to use for fitting the series to the target. One of: ${_.keys(fitFunctions).join(', ')}`,
help: i18n.translate('timelion.help.functions.fit.args.modeHelpText', {
defaultMessage:
'The algorithm to use for fitting the series to the target. One of: {fitFunctions}',
values: {
fitFunctions: _.keys(fitFunctions).join(', '),
},
}),
suggestions: _.keys(fitFunctions).map(key => {
return { name: key };
})
}
],
help: 'Fills null values using a defined fit function',
help: i18n.translate('timelion.help.functions.fitHelpText', {
defaultMessage: 'Fills null values using a defined fit function',
}),
fn: function absFn(args) {
return alter(args, function (eachSeries, mode) {

View file

@ -17,21 +17,29 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import fetch from 'node-fetch';
import moment from 'moment';
import Datasource from '../lib/classes/datasource';
export default new Datasource ('graphite', {
args: [
{
name: 'metric', // _test-data.users.*.data
types: ['string'],
help: 'Graphite metric to pull, e.g., _test-data.users.*.data'
help: i18n.translate('timelion.help.functions.graphite.args.metricHelpText', {
defaultMessage: 'Graphite metric to pull, e.g., {metricExample}',
values: {
metricExample: '_test-data.users.*.data',
},
}),
}
],
help: `[experimental] Pull data from graphite. Configure your graphite server in Kibana's Advanced Settings`,
help: i18n.translate('timelion.help.functions.graphiteHelpText', {
defaultMessage:
`[experimental] Pull data from graphite. Configure your graphite server in Kibana's Advanced Settings`,
}),
fn: function graphite(args, tlConfig) {
const config = args.byName;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -29,10 +30,14 @@ export default new Chainable('hide', {
{
name: 'hide',
types: ['boolean', 'null'],
help: 'Hide or unhide the series'
help: i18n.translate('timelion.help.functions.hide.args.hideHelpText', {
defaultMessage: 'Hide or unhide the series',
}),
}
],
help: 'Hide the series by default',
help: i18n.translate('timelion.help.functions.hideHelpText', {
defaultMessage: 'Hide the series by default',
}),
fn: function hideFn(args) {
return alter(args, function (eachSeries, hide) {
eachSeries._hide = hide == null ? true : hide;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import Chainable from '../../lib/classes/chainable';
import ses from './lib/ses';
@ -33,46 +34,68 @@ export default new Chainable('holt', {
{
name: 'alpha',
types: ['number'],
help: `
help: i18n.translate('timelion.help.functions.holt.args.alphaHelpText', {
defaultMessage:
`
Smoothing weight from 0 to 1.
Increasing alpha will make the new series more closely follow the original.
Lowering it will make the series smoother`
Lowering it will make the series smoother`,
}),
},
{
name: 'beta',
types: ['number'],
help: `
help: i18n.translate('timelion.help.functions.holt.args.betaHelpText', {
defaultMessage:
`
Trending weight from 0 to 1.
Increasing beta will make rising/falling lines continue to rise/fall longer.
Lowering it will make the function learn the new trend faster`
Lowering it will make the function learn the new trend faster`,
}),
},
{
name: 'gamma',
types: ['number'],
help: `
help: i18n.translate('timelion.help.functions.holt.args.gammaHelpText', {
defaultMessage:
`
Seasonal weight from 0 to 1. Does your data look like a wave?
Increasing this will give recent seasons more importance, thus changing the wave form faster.
Lowering it will reduce the importance of new seasons, making history more important.
`
`,
}),
},
{
name: 'season',
types: ['string'],
help: 'How long is the season, e.g., 1w if you pattern repeats weekly. (Only useful with gamma)'
help: i18n.translate('timelion.help.functions.holt.args.seasonHelpText', {
defaultMessage:
'How long is the season, e.g., 1w if your pattern repeats weekly. (Only useful with gamma)',
description:
'"1w" is an expression value and should not be translated. "gamma" is a parameter name and should not be translated.',
}),
},
{
name: 'sample',
types: ['number', 'null'],
help: `
help: i18n.translate('timelion.help.functions.holt.args.sampleHelpText', {
defaultMessage:
`
The number of seasons to sample before starting to "predict" in a seasonal series.
(Only useful with gamma, Default: all)`
(Only useful with gamma, Default: all)`,
description: '"gamma" and "all" are parameter names and values and must not be translated.',
}),
}
],
help: `
help: i18n.translate('timelion.help.functions.holtHelpText', {
defaultMessage:
`
Sample the beginning of a series and use it to forecast what should happen
via several optional parameters. In general, like everything, this is crappy at predicting the
future. You're much better off using it to predict what should be happening right now, for the
purpose of anomaly detection. Note that nulls will be filled with forecasted values. Deal with it.`,
via several optional parameters. In general, this doesn't really predict the
future, but predicts what should be happening right now according to past data,
which can be useful for anomaly detection. Note that nulls will be filled with forecasted values.`,
description: '"null" is a data value here and must not be translated.',
}),
fn: function expsmoothFn(args, tlConfig) {
const newSeries = _.cloneDeep(args.byName.inputSeries);
@ -103,7 +126,11 @@ export default new Chainable('holt', {
if (alpha != null && beta != null && gamma != null) {
if (!sample || !args.byName.season || sample < 2) {
throw new Error('Must specify a season length and a sample size >= 2');
throw new Error(
i18n.translate('timelion.serverSideErrors.holtFunction.missingParamsErrorMessage', {
defaultMessage: 'Must specify a season length and a sample size >= 2',
})
);
}
const season = Math.round(toMilliseconds(args.byName.season) / toMilliseconds(tlConfig.time.interval));
points = tes(points, alpha, beta, gamma, season, sample);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
export default function des(points, alpha, beta) {
@ -27,7 +28,11 @@ export default function des(points, alpha, beta) {
let unknownCount = 0;
if (points.length < 2) {
throw new Error ('You need at least 2 points to use double exponential smoothing');
throw new Error(
i18n.translate('timelion.serverSideErrors.holtFunction.notEnoughPointsErrorMessage', {
defaultMessage: 'You need at least 2 points to use double exponential smoothing',
}),
);
}
const smoothedPoints = _.map(points, (point, i) => {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -29,15 +30,23 @@ export default new Chainable('label', {
{
name: 'label',
types: ['string'],
help: 'Legend value for series. You can use $1, $2, etc, in the string to match up with the regex capture groups'
help: i18n.translate('timelion.help.functions.label.args.labelHelpText', {
defaultMessage:
'Legend value for series. You can use $1, $2, etc, in the string to match up with the regex capture groups',
description: '"$1" and "$2" are part of the expression and must not be translated.',
}),
},
{
name: 'regex',
types: ['string', 'null'],
help: 'A regex with capture group support'
help: i18n.translate('timelion.help.functions.label.args.regexHelpText', {
defaultMessage: 'A regex with capture group support',
}),
}
],
help: 'Change the label of the series. Use %s reference the existing label',
help: i18n.translate('timelion.help.functions.labelHelpText', {
defaultMessage: 'Change the label of the series. Use %s to reference the existing label',
}),
fn: function labelFn(args) {
const config = args.byName;
return alter(args, function (eachSeries) {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
import { DEFAULT_TIME_FORMAT } from '../../common/lib';
@ -30,47 +31,87 @@ export default new Chainable('legend', {
{
name: 'position',
types: ['string', 'boolean', 'null'],
help: 'Corner to place the legend in: nw, ne, se, or sw. You can also pass false to disable the legend',
help: i18n.translate('timelion.help.functions.legend.args.positionHelpText', {
defaultMessage:
'Corner to place the legend in: nw, ne, se, or sw. You can also pass false to disable the legend',
description: '"nw", "ne", "se", "sw" and "false" are keywords and must not be translated.',
}),
suggestions: [
{
name: 'false',
help: 'disable legend',
help: i18n.translate(
'timelion.help.functions.legend.args.position.suggestions.falseHelpText',
{
defaultMessage: 'disable legend',
}
),
},
{
name: 'nw',
help: 'place legend in north west corner'
help: i18n.translate(
'timelion.help.functions.legend.args.position.suggestions.nwHelpText',
{
defaultMessage: 'place legend in north west corner',
}
),
},
{
name: 'ne',
help: 'place legend in north east corner'
help: i18n.translate(
'timelion.help.functions.legend.args.position.suggestions.neHelpText',
{
defaultMessage: 'place legend in north east corner',
}
),
},
{
name: 'se',
help: 'place legend in south east corner'
help: i18n.translate(
'timelion.help.functions.legend.args.position.suggestions.seHelpText',
{
defaultMessage: 'place legend in south east corner',
}
),
},
{
name: 'sw',
help: 'place legend in south west corner'
help: i18n.translate(
'timelion.help.functions.legend.args.position.suggestions.swHelpText',
{
defaultMessage: 'place legend in south west corner',
}
),
}
]
},
{
name: 'columns',
types: ['number', 'null'],
help: 'Number of columns to divide the legend into'
help: i18n.translate('timelion.help.functions.legend.args.columnsHelpText', {
defaultMessage: 'Number of columns to divide the legend into',
}),
},
{
name: 'showTime',
types: ['boolean'],
help: 'Show time value in legend when hovering over graph. Default: true'
help: i18n.translate('timelion.help.functions.legend.args.showTimeHelpText', {
defaultMessage: 'Show time value in legend when hovering over graph. Default: true',
}),
},
{
name: 'timeFormat',
types: ['string'],
help: `moment.js format pattern. Default: ${DEFAULT_TIME_FORMAT}`
help: i18n.translate('timelion.help.functions.legend.args.timeFormatHelpText', {
defaultMessage: 'moment.js format pattern. Default: {defaultTimeFormat}',
values: {
defaultTimeFormat: DEFAULT_TIME_FORMAT,
},
}),
}
],
help: 'Set the position and style of the legend on the plot',
help: i18n.translate('timelion.help.functions.legendHelpText', {
defaultMessage: 'Set the position and style of the legend on the plot',
}),
fn: function legendFn(args) {
return alter(args, function (eachSeries, position, columns, showTime = true, timeFormat = DEFAULT_TIME_FORMAT) {
eachSeries._global = eachSeries._global || {};

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -29,30 +30,42 @@ export default new Chainable('lines', {
{
name: 'width',
types: ['number', 'null'],
help: 'Line thickness'
help: i18n.translate('timelion.help.functions.lines.args.widthHelpText', {
defaultMessage: 'Line thickness',
}),
},
{
name: 'fill',
types: ['number', 'null'],
help: 'Number between 0 and 10. Use for making area charts'
help: i18n.translate('timelion.help.functions.lines.args.fillHelpText', {
defaultMessage: 'Number between 0 and 10. Use for making area charts',
}),
},
{
name: 'stack',
types: ['boolean', 'null'],
help: 'Stack lines, often misleading. At least use some fill if you use this.'
help: i18n.translate('timelion.help.functions.lines.args.stackHelpText', {
defaultMessage: 'Stack lines, often misleading. At least use some fill if you use this.',
}),
},
{
name: 'show',
types: ['number', 'boolean', 'null'],
help: 'Show or hide lines'
help: i18n.translate('timelion.help.functions.lines.args.showHelpText', {
defaultMessage: 'Show or hide lines',
}),
},
{
name: 'steps',
types: ['number', 'boolean', 'null'],
help: 'Show line as step, e.g., do not interpolate between points'
help: i18n.translate('timelion.help.functions.lines.args.stepsHelpText', {
defaultMessage: 'Show line as step, e.g., do not interpolate between points',
}),
}
],
help: 'Show the seriesList as lines',
help: i18n.translate('timelion.help.functions.linesHelpText', {
defaultMessage: 'Show the seriesList as lines',
}),
fn: function linesFn(args) {
return alter(args, function (eachSeries, width, fill, stack, show, steps) {
eachSeries.lines = eachSeries.lines || {};

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -30,11 +31,15 @@ export default new Chainable('log', {
{
name: 'base',
types: ['number'],
help: 'Set logarithmic base, 10 by default'
help: i18n.translate('timelion.help.functions.log.args.baseHelpText', {
defaultMessage: 'Set logarithmic base, 10 by default',
}),
}
],
help: 'Return the logarithm value of each value in the series list (default base: 10)',
help: i18n.translate('timelion.help.functions.logHelpText', {
defaultMessage:
'Return the logarithm value of each value in the series list (default base: 10)',
}),
fn: function logFn(args) {
const config = args.byName;
return alter(args, function (eachSeries) {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,13 +30,18 @@ export default new Chainable('max', {
{
name: 'value',
types: ['seriesList', 'number'],
help: 'Sets the point to whichever is higher, the existing value, or the one passed.' +
' If passing a seriesList it must contain exactly 1 series.'
help: i18n.translate('timelion.help.functions.max.args.valueHelpText', {
defaultMessage:
'Sets the point to whichever is higher, the existing value, or the one passed. ' +
'If passing a seriesList it must contain exactly 1 series.',
}),
}
],
help: 'Maximum values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.maxHelpText', {
defaultMessage:
'Maximum values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
fn: function maxFn(args) {
return reduce(args, function (a, b) {
return Math.max(a, b);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,13 +30,18 @@ export default new Chainable('min', {
{
name: 'value',
types: ['seriesList', 'number'],
help: 'Sets the point to whichever is lower, the existing value, or the one passed.' +
' If passing a seriesList it must contain exactly 1 series.'
help: i18n.translate('timelion.help.functions.min.args.valueHelpText', {
defaultMessage:
'Sets the point to whichever is lower, the existing value, or the one passed. ' +
'If passing a seriesList it must contain exactly 1 series.',
}),
}
],
help: 'Minimum values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.minHelpText', {
defaultMessage:
'Minimum values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
fn: function minFn(args) {
return reduce(args, function (a, b) {
return Math.min(a, b);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -34,14 +35,23 @@ export default new Chainable('movingaverage', {
{
name: 'window',
types: ['number', 'string'],
help: 'Number of points, or a date math expression (eg 1d, 1M) to average over. ' +
'If a date math expression is specified, the function will get as close as possible given the currently select interval' +
'If the date math expression is not evenly divisible by the interval the results may appear abnormal.'
help: i18n.translate('timelion.help.functions.movingaverage.args.windowHelpText', {
defaultMessage:
'Number of points, or a date math expression (eg 1d, 1M) to average over. If a date math expression ' +
'is specified, the function will get as close as possible given the currently select interval. ' +
'If the date math expression is not evenly divisible by the interval the results may appear abnormal.',
}),
},
{
name: 'position',
types: ['string', 'null'],
help: `Position of the averaged points relative to the result time. One of: ${validPositions.join(', ')}`,
help: i18n.translate('timelion.help.functions.movingaverage.args.positionHelpText', {
defaultMessage:
'Position of the averaged points relative to the result time. One of: {validPositions}',
values: {
validPositions: validPositions.join(', '),
},
}),
suggestions: validPositions.map(position => {
const suggestion = { name: position };
if (position === defaultPosition) {
@ -52,7 +62,10 @@ export default new Chainable('movingaverage', {
}
],
aliases: ['mvavg'],
help: 'Calculate the moving average over a given window. Nice for smoothing noisy series',
help: i18n.translate('timelion.help.functions.movingaverageHelpText', {
defaultMessage:
'Calculate the moving average over a given window. Nice for smoothing noisy series',
}),
fn: function movingaverageFn(args, tlConfig) {
return alter(args, function (eachSeries, _window, _position) {
@ -69,7 +82,16 @@ export default new Chainable('movingaverage', {
}
_position = _position || defaultPosition;
if (!_.contains(validPositions, _position)) throw new Error('Valid positions are: ' + validPositions.join(', '));
if (!_.contains(validPositions, _position)) {
throw new Error(
i18n.translate('timelion.serverSideErrors.movingaverageFunction.notValidPositionErrorMessage', {
defaultMessage: 'Valid positions are: {validPositions}',
values: {
validPositions: validPositions.join(', '),
},
})
);
}
const pairs = eachSeries.data;
const pairsLen = pairs.length;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -33,23 +34,44 @@ export default new Chainable('movingstd', {
{
name: 'window',
types: ['number'],
help: 'Number of points to compute the standard deviation over.'
help: i18n.translate('timelion.help.functions.movingstd.args.windowHelpText', {
defaultMessage: 'Number of points to compute the standard deviation over.',
}),
},
{
name: 'position',
types: ['string', 'null'],
help: `Position of the window slice relative to the result time. Options are ${positions.join(', ')}. Default: ${defaultPosition}`
help: i18n.translate('timelion.help.functions.movingstd.args.positionHelpText', {
defaultMessage:
'Position of the window slice relative to the result time. Options are {positions}. Default: {defaultPosition}',
values: {
positions: positions.join(', '),
defaultPosition,
},
}),
}
],
aliases: ['mvstd'],
help: 'Calculate the moving standard deviation over a given window. Uses naive two-pass algorithm. Rounding errors ' +
'may become more noticeable with very long series, or series with very large numbers.',
help: i18n.translate('timelion.help.functions.movingstdHelpText', {
defaultMessage:
'Calculate the moving standard deviation over a given window. Uses naive two-pass algorithm. ' +
'Rounding errors may become more noticeable with very long series, or series with very large numbers.',
}),
fn: function movingstdFn(args) {
return alter(args, function (eachSeries, _window, _position) {
_position = _position || defaultPosition;
if (!_.contains(positions, _position)) throw new Error('Valid positions are: ' + positions.join(', '));
if (!_.contains(positions, _position)) {
throw new Error(
i18n.translate('timelion.serverSideErrors.movingstdFunction.notValidPositionErrorMessage', {
defaultMessage: 'Valid positions are: {validPositions}',
values: {
validPositions: positions.join(', '),
},
}),
);
}
const pairs = eachSeries.data;
const pairsLen = pairs.length;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,10 +30,16 @@ export default new Chainable('multiply', {
{
name: 'multiplier',
types: ['seriesList', 'number'],
help: 'Number or series by which to multiply. SeriesList with multiple series will be applied label-wise.'
help: i18n.translate('timelion.help.functions.multiply.args.multiplierHelpText', {
defaultMessage:
'Number or series by which to multiply. SeriesList with multiple series will be applied label-wise.',
}),
}
],
help: 'Multiply the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.multiplyHelpText', {
defaultMessage:
'Multiply the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
fn: function multiplyFn(args) {
return reduce(args, function (a, b) {
return a * b;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -33,26 +34,39 @@ export default new Chainable('points', {
{
name: 'radius',
types: ['number', 'null'],
help: 'Size of points'
help: i18n.translate('timelion.help.functions.points.args.radiusHelpText', {
defaultMessage: 'Size of points',
}),
},
{
name: 'weight',
types: ['number', 'null'],
help: 'Thickness of line around point'
help: i18n.translate('timelion.help.functions.points.args.weightHelpText', {
defaultMessage: 'Thickness of line around point',
}),
},
{
name: 'fill',
types: ['number', 'null'],
help: 'Number between 0 and 10 representing opacity of fill'
help: i18n.translate('timelion.help.functions.points.args.fillHelpText', {
defaultMessage: 'Number between 0 and 10 representing opacity of fill',
}),
},
{
name: 'fillColor',
types: ['string', 'null'],
help: 'Color with which to fill point'
help: i18n.translate('timelion.help.functions.points.args.fillColorHelpText', {
defaultMessage: 'Color with which to fill point',
}),
},
{
name: 'symbol',
help: `point symbol. One of: ${validSymbols.join(', ')}`,
help: i18n.translate('timelion.help.functions.points.args.symbolHelpText', {
defaultMessage: 'point symbol. One of: {validSymbols}',
values: {
validSymbols: validSymbols.join(', '),
},
}),
types: ['string', 'null'],
suggestions: validSymbols.map(symbol => {
const suggestion = { name: symbol };
@ -65,10 +79,14 @@ export default new Chainable('points', {
{
name: 'show',
types: ['boolean', 'null'],
help: 'Show points or not'
help: i18n.translate('timelion.help.functions.points.args.showHelpText', {
defaultMessage: 'Show points or not',
}),
}
],
help: 'Show the series as points',
help: i18n.translate('timelion.help.functions.pointsHelpText', {
defaultMessage: 'Show the series as points',
}),
fn: function pointsFn(args) {
return alter(args, function (eachSeries, radius, weight, fill, fillColor, symbol, show) {
eachSeries.points = eachSeries.points || {};
@ -88,7 +106,14 @@ export default new Chainable('points', {
symbol = symbol || defaultSymbol;
if (!_.contains(validSymbols, symbol)) {
throw new Error('Valid symbols are: ' + validSymbols.join(', '));
throw new Error(
i18n.translate('timelion.serverSideErrors.pointsFunction.notValidSymbolErrorMessage', {
defaultMessage: 'Valid symbols are: {validSymbols}',
values: {
validSymbols: validSymbols.join(', '),
},
})
);
}
eachSeries.points.symbol = symbol;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -30,10 +31,14 @@ export default new Chainable('precision', {
{
name: 'precision',
types: ['number'],
help: 'Number of digits to round each value to'
help: i18n.translate('timelion.help.functions.precision.args.precisionHelpText', {
defaultMessage: 'Number of digits to round each value to',
}),
}
],
help: 'number of digits to round the decimal portion of the value to',
help: i18n.translate('timelion.help.functions.precisionHelpText', {
defaultMessage: 'number of digits to round the decimal portion of the value to',
}),
fn: async function precisionFn(args) {
await alter(args, function (eachSeries, precision) {
eachSeries._meta = eachSeries._meta || {};

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
import _ from 'lodash';
@ -49,7 +50,9 @@ export default new Chainable('props', {
{
name: 'global',
types: ['boolean', 'null'],
help: 'Set props on the seriesList vs on each series'
help: i18n.translate('timelion.help.functions.props.args.globalHelpText', {
defaultMessage: 'Set props on the seriesList vs on each series',
}),
}
],
extended: {
@ -60,7 +63,13 @@ export default new Chainable('props', {
},
// extended means you can pass arguments that aren't listed. They just won't be in the ordered array
// They will be passed as args._extended:{}
help: 'Use at your own risk, sets arbitrary properties on the series. For example .props(label=bears!)',
help: i18n.translate('timelion.help.functions.propsHelpText', {
defaultMessage:
'Use at your own risk, sets arbitrary properties on the series. For example {example}',
values: {
example: '.props(label=bears!)',
},
}),
fn: function firstFn(args) {
const properties = unflatten(_.omit(args.byName, 'inputSeries', 'global'));

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import fetch from 'node-fetch';
import moment from 'moment';
@ -24,28 +25,37 @@ fetch.Promise = require('bluebird');
//var parseDateMath = require('../utils/date_math.js');
import Datasource from '../lib/classes/datasource';
export default new Datasource ('quandl', {
dataSource: true,
args: [
{
name: 'code',
types: ['string', 'null'],
help: 'The quandl code to plot. You can find these on quandl.com.'
help: i18n.translate('timelion.help.functions.quandl.args.codeHelpText', {
defaultMessage: 'The quandl code to plot. You can find these on quandl.com.',
}),
},
{
name: 'position',
types: ['number', 'null'],
help: 'Some quandl sources return multiple series, which one should I use? 1 based index.'
help: i18n.translate('timelion.help.functions.quandl.args.positionHelpText', {
defaultMessage:
'Some quandl sources return multiple series, which one should I use? 1 based index.',
}),
}
],
help: `
help: i18n.translate('timelion.help.functions.quandlHelpText', {
defaultMessage:
`
[experimental]
Pull data from quandl.com using the quandl code. Set "timelion:quandl.key" to your free API key in Kibana's
Pull data from quandl.com using the quandl code. Set {quandlKeyField} to your free API key in Kibana's
Advanced Settings. The API has a really low rate limit without a key.`,
values: {
quandlKeyField: '"timelion:quandl.key"',
},
}),
fn: function quandlFn(args, tlConfig) {
const intervalMap = {
'1d': 'daily',
@ -62,8 +72,15 @@ export default new Datasource ('quandl', {
});
if (!config.interval) {
throw new Error('quandl() unsupported interval: ' + tlConfig.time.interval +
'. quandl() supports: ' + _.keys(intervalMap).join(', '));
throw new Error(
i18n.translate('timelion.serverSideErrors.quandlFunction.unsupportedIntervalErrorMessage', {
defaultMessage: 'quandl() unsupported interval: {interval}. quandl() supports: {intervals}',
values: {
interval: tlConfig.time.interval,
intervals: _.keys(intervalMap).join(', '),
},
})
);
}
const time = {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -30,15 +31,21 @@ export default new Chainable('range', {
{
name: 'min',
types: ['number'],
help: 'New minimum value'
help: i18n.translate('timelion.help.functions.range.args.minHelpText', {
defaultMessage: 'New minimum value',
}),
},
{
name: 'max',
types: ['number'],
help: 'New maximum value'
help: i18n.translate('timelion.help.functions.range.args.maxHelpText', {
defaultMessage: 'New maximum value',
}),
}
],
help: 'Changes the max and min of a series while keeping the same shape',
help: i18n.translate('timelion.help.functions.rangeHelpText', {
defaultMessage: 'Changes the max and min of a series while keeping the same shape',
}),
fn: function range(args) {
return alter(args, function (eachSeries) {
const values = _.map(eachSeries.data, 1);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import toMS from '../lib/to_milliseconds.js';
import _ from 'lodash';
@ -31,10 +32,16 @@ export default new Chainable('scale_interval', {
{
name: 'interval',
types: ['string'],
help: 'The new interval in date math notation, e.g., 1s for 1 second. 1m, 5m, 1M, 1w, 1y, etc.'
help: i18n.translate('timelion.help.functions.scaleInterval.args.intervalHelpText', {
defaultMessage:
'The new interval in date math notation, e.g., 1s for 1 second. 1m, 5m, 1M, 1w, 1y, etc.',
}),
}
],
help: 'Changes scales a value (usually a sum or a count) to a new interval. For example, as a per-second rate',
help: i18n.translate('timelion.help.functions.scaleIntervalHelpText', {
defaultMessage:
'Changes scales a value (usually a sum or a count) to a new interval. For example, as a per-second rate',
}),
fn: function scaleIntervalFn(args, tlConfig) {
const currentInterval = toMS(tlConfig.time.interval);
const scaleInterval = toMS(args.byName.interval);

View file

@ -17,27 +17,34 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import Datasource from '../lib/classes/datasource';
import Promise from 'bluebird';
export default new Datasource ('static', {
aliases: ['value'],
args: [
{
name: 'value', // _test-data.users.*.data
types: ['number', 'string'],
help: 'The single value to to display, you can also pass several values and I will interpolate them evenly ' +
'across your time range.'
help: i18n.translate('timelion.help.functions.static.args.valueHelpText', {
defaultMessage:
'The single value to to display, you can also pass several values and I will interpolate them evenly across your time range.',
}),
},
{
name: 'label',
types: ['string', 'null'],
help: 'A quick way to set the label for the series. You could also use the .label() function'
help: i18n.translate('timelion.help.functions.static.args.labelHelpText', {
defaultMessage:
'A quick way to set the label for the series. You could also use the .label() function',
}),
}
],
help: 'Draws a single value across the chart',
help: i18n.translate('timelion.help.functions.staticHelpText', {
defaultMessage: 'Draws a single value across the chart',
}),
fn: function staticFn(args, tlConfig) {
let data;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,10 +30,16 @@ export default new Chainable('subtract', {
{
name: 'term',
types: ['seriesList', 'number'],
help: 'Number or series to subtract from input. SeriesList with multiple series will be applied label-wise.'
help: i18n.translate('timelion.help.functions.subtract.args.termHelpText', {
defaultMessage:
'Number or series to subtract from input. SeriesList with multiple series will be applied label-wise.',
}),
}
],
help: 'Subtract the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.subtractHelpText', {
defaultMessage:
'Subtract the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
fn: function subtractFn(args) {
return reduce(args, function (a, b) {
return a - b;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import reduce from '../lib/reduce.js';
import Chainable from '../lib/classes/chainable';
@ -29,11 +30,16 @@ export default new Chainable('sum', {
{
name: 'term',
types: ['seriesList', 'number'],
help: 'Number or series to sum with the input series. SeriesList with multiple series will be applied label-wise.'
help: i18n.translate('timelion.help.functions.sum.args.termHelpText', {
defaultMessage:
'Number or series to sum with the input series. SeriesList with multiple series will be applied label-wise.',
}),
}
],
help: 'Adds the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
help: i18n.translate('timelion.help.functions.sumHelpText', {
defaultMessage:
'Adds the values of one or more series in a seriesList to each position, in each series, of the input seriesList',
}),
aliases: ['add', 'plus'],
fn: function sumFn(args) {
return reduce(args, function (a, b) {

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -29,10 +30,15 @@ export default new Chainable('title', {
{
name: 'title',
types: ['string', 'null'],
help: 'Title for the plot.'
help: i18n.translate('timelion.help.functions.title.args.titleHelpText', {
defaultMessage: 'Title for the plot.',
}),
}
],
help: 'Adds a title to the top of the plot. If called on more than 1 seriesList the last call will be used.',
help: i18n.translate('timelion.help.functions.titleHelpText', {
defaultMessage:
'Adds a title to the top of the plot. If called on more than 1 seriesList the last call will be used.',
}),
fn: function hideFn(args) {
return alter(args, function (eachSeries, title) {
eachSeries._title = title;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import Chainable from '../../lib/classes/chainable';
import { linear, log } from './lib/regress';
@ -35,7 +36,13 @@ export default new Chainable('trend', {
{
name: 'mode',
types: ['string'],
help: `The algorithm to use for generating the trend line. One of: ${_.keys(validRegressions).join(', ')}`,
help: i18n.translate('timelion.help.functions.trend.args.modeHelpText', {
defaultMessage:
'The algorithm to use for generating the trend line. One of: {validRegressions}',
values: {
validRegressions: _.keys(validRegressions).join(', '),
},
}),
suggestions: _.keys(validRegressions).map(key => {
return { name: key, help: validRegressions[key] };
})
@ -43,17 +50,25 @@ export default new Chainable('trend', {
{
name: 'start',
types: ['number', 'null'],
help: 'Where to start calculating from the beginning or end. For example -10 would start calculating 10 points from' +
' the end, +15 would start 15 points from the beginning. Default: 0',
help: i18n.translate('timelion.help.functions.trend.args.startHelpText', {
defaultMessage:
'Where to start calculating from the beginning or end. For example -10 would start ' +
'calculating 10 points from the end, +15 would start 15 points from the beginning. Default: 0',
}),
},
{
name: 'end',
types: ['number', 'null'],
help: 'Where to stop calculating from the beginning or end. For example -10 would stop calculating 10 points from' +
' the end, +15 would stop 15 points from the beginning. Default: 0',
help: i18n.translate('timelion.help.functions.trend.args.endHelpText', {
defaultMessage:
'Where to stop calculating from the beginning or end. For example -10 would stop ' +
'calculating 10 points from the end, +15 would stop 15 points from the beginning. Default: 0',
}),
},
],
help: 'Draws a trend line using a specified regression algorithm',
help: i18n.translate('timelion.help.functions.trendHelpText', {
defaultMessage: 'Draws a trend line using a specified regression algorithm',
}),
fn: function absFn(args) {
const newSeries = _.cloneDeep(args.byName.inputSeries);

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
@ -30,15 +31,22 @@ export default new Chainable('trim', {
{
name: 'start',
types: ['number', 'null'],
help: 'Buckets to trim from the beginning of the series. Default: 1'
help: i18n.translate('timelion.help.functions.trim.args.startHelpText', {
defaultMessage: 'Buckets to trim from the beginning of the series. Default: 1',
}),
},
{
name: 'end',
types: ['number', 'null'],
help: 'Buckets to trim from the end of the series. Default: 1'
help: i18n.translate('timelion.help.functions.trim.args.endHelpText', {
defaultMessage: 'Buckets to trim from the end of the series. Default: 1',
}),
}
],
help: 'Set N buckets at the start or end of a series to null to fit the "partial bucket issue"',
help: i18n.translate('timelion.help.functions.trimHelpText', {
defaultMessage:
'Set N buckets at the start or end of a series to null to fit the "partial bucket issue"',
}),
fn: function conditionFn(args) {
const config = args.byName;
if (config.start == null) config.start = 1;

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import fetch from 'node-fetch';
import moment from 'moment';
@ -27,17 +28,28 @@ export default new Datasource ('worldbank', {
{
name: 'code', // countries/all/indicators/SP.POP.TOTL
types: ['string', 'null'],
help: 'Worldbank API path.' +
' This is usually everything after the domain, before the querystring. E.g.: ' +
'/en/countries/ind;chn/indicators/DPANUSSPF.'
help: i18n.translate('timelion.help.functions.worldbank.args.codeHelpText', {
defaultMessage:
'Worldbank API path. This is usually everything after the domain, before the querystring. E.g.: {apiPathExample}.',
values: {
apiPathExample: '/en/countries/ind;chn/indicators/DPANUSSPF',
},
}),
}
],
aliases: ['wb'],
help: `
help: i18n.translate('timelion.help.functions.worldbankHelpText', {
defaultMessage:
`
[experimental]
Pull data from http://data.worldbank.org/ using path to series.
Pull data from {worldbankUrl} using path to series.
The worldbank provides mostly yearly data, and often has no data for the current year.
Try offset=-1y if you get no data for recent time ranges.`,
Try {offsetQuery} if you get no data for recent time ranges.`,
values: {
worldbankUrl: 'http://data.worldbank.org/',
offsetQuery: 'offset=-1y',
},
}),
fn: function worldbank(args, tlConfig) {
// http://api.worldbank.org/en/countries/ind;chn/indicators/DPANUSSPF?date=2000:2006&MRV=5
@ -74,7 +86,16 @@ export default new Datasource ('worldbank', {
return [moment(date, 'YYYY').valueOf(), Number(val)];
}));
if (!hasData) throw new Error('Worldbank request succeeded, but there was no data for ' + config.code);
if (!hasData) {
throw new Error(
i18n.translate('timelion.serverSideErrors.worldbankFunction.noDataErrorMessage', {
defaultMessage: 'Worldbank request succeeded, but there was no data for {code}',
values: {
code: config.code,
},
})
);
}
return {
type: 'seriesList',

View file

@ -17,32 +17,48 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import worldbank from './worldbank.js';
import Promise from 'bluebird';
import Datasource from '../lib/classes/datasource';
export default new Datasource ('worldbank_indicators', {
args: [
{
name: 'country', // countries/all/indicators/SP.POP.TOTL
types: ['string', 'null'],
help: 'Worldbank country identifier. Usually the country\'s 2 letter code'
help: i18n.translate('timelion.help.functions.worldbankIndicators.args.countryHelpText', {
defaultMessage: `Worldbank country identifier. Usually the country's 2 letter code`,
}),
},
{
name: 'indicator',
types: ['string', 'null'],
help: 'The indicator code to use. You\'ll have to look this up on data.worldbank.org.' +
' Often pretty obtuse. E.g., SP.POP.TOTL is population'
help: i18n.translate('timelion.help.functions.worldbankIndicators.args.indicatorHelpText', {
defaultMessage:
`The indicator code to use. You'll have to look this up on {worldbankUrl}. ` +
'Often pretty obtuse. E.g., {indicatorExample} is population',
values: {
worldbankUrl: 'data.worldbank.org',
indicatorExample: 'SP.POP.TOTL',
},
}),
}
],
aliases: ['wbi'],
help: `
help: i18n.translate('timelion.help.functions.worldbankIndicatorsHelpText', {
defaultMessage:
`
[experimental]
Pull data from http://data.worldbank.org/ using the country name and indicator. The worldbank provides
mostly yearly data, and often has no data for the current year. Try offset=-1y if you get no data for recent
Pull data from {worldbankUrl} using the country name and indicator. The worldbank provides
mostly yearly data, and often has no data for the current year. Try {offsetQuery} if you get no data for recent
time ranges.`,
values: {
worldbankUrl: 'http://data.worldbank.org/',
offsetQuery: 'offset=-1y',
},
}),
fn: function worldbankIndicators(args, tlConfig) {
const config = _.defaults(args.byName, {
country: 'wld',

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
@ -39,37 +40,55 @@ export default new Chainable('yaxis', {
{
name: 'yaxis',
types: ['number', 'null'],
help: 'The numbered y-axis to plot this series on, e.g., .yaxis(2) for a 2nd y-axis.'
help: i18n.translate('timelion.help.functions.yaxis.args.yaxisHelpText', {
defaultMessage:
'The numbered y-axis to plot this series on, e.g., .yaxis(2) for a 2nd y-axis.',
}),
},
{
name: 'min',
types: ['number', 'null'],
help: 'Min value'
help: i18n.translate('timelion.help.functions.yaxis.args.minHelpText', {
defaultMessage: 'Min value',
}),
},
{
name: 'max',
types: ['number', 'null'],
help: 'Max value'
help: i18n.translate('timelion.help.functions.yaxis.args.maxHelpText', {
defaultMessage: 'Max value',
}),
},
{
name: 'position',
types: ['string', 'null'],
help: 'left or right'
help: i18n.translate('timelion.help.functions.yaxis.args.positionHelpText', {
defaultMessage: 'left or right',
}),
},
{
name: 'label',
types: ['string', 'null'],
help: 'Label for axis'
help: i18n.translate('timelion.help.functions.yaxis.args.labelHelpText', {
defaultMessage: 'Label for axis',
}),
},
{
name: 'color',
types: ['string', 'null'],
help: 'Color of axis label'
help: i18n.translate('timelion.help.functions.yaxis.args.colorHelpText', {
defaultMessage: 'Color of axis label',
}),
},
{
name: 'units',
types: ['string', 'null'],
help: `The function to use for formatting y-axis labels. One of: ${_.values(tickFormatters).join(', ')}`,
help: i18n.translate('timelion.help.functions.yaxis.args.unitsHelpText', {
defaultMessage: 'The function to use for formatting y-axis labels. One of: {formatters}',
values: {
formatters: _.values(tickFormatters).join(', '),
},
}),
suggestions: _.keys(tickFormatters).map(key => {
return { name: key, help: tickFormatters[key] };
})
@ -77,10 +96,15 @@ export default new Chainable('yaxis', {
{
name: 'tickDecimals',
types: ['number', 'null'],
help: 'tick decimal precision'
help: i18n.translate('timelion.help.functions.yaxis.args.tickDecimalsHelpText', {
defaultMessage: 'tick decimal precision',
}),
},
],
help: 'Configures a variety of y-axis options, the most important likely being the ability to add an Nth (eg 2nd) y-axis',
help: i18n.translate('timelion.help.functions.yaxisHelpText', {
defaultMessage:
'Configures a variety of y-axis options, the most important likely being the ability to add an Nth (eg 2nd) y-axis',
}),
fn: function yaxisFn(args) {
return alter(args, function (eachSeries, yaxis, min, max, position, label, color, units, tickDecimals) {
yaxis = yaxis || 1;
@ -108,13 +132,24 @@ export default new Chainable('yaxis', {
const unitTokens = units.split(':');
const unitType = unitTokens[0];
if (!tickFormatters[unitType]) {
throw new Error (`${units} is not a supported unit type.`);
throw new Error (
i18n.translate(
'timelion.serverSideErrors.yaxisFunction.notSupportedUnitTypeErrorMessage',
{
defaultMessage: '{units} is not a supported unit type.',
values: { units },
})
);
}
if (unitType === 'currency') {
const threeLetterCode = /^[A-Za-z]{3}$/;
const currency = unitTokens[1];
if (currency && !threeLetterCode.test(currency)) {
throw new Error('Currency must be a three letter code');
throw new Error(
i18n.translate('timelion.serverSideErrors.yaxisFunction.notValidCurrencyFormatErrorMessage', {
defaultMessage: 'Currency must be a three letter code',
})
);
}
}