Migrate timelion to the NP. (#69160) (#71676)

This commit is contained in:
Uladzislau Lasitsa 2020-07-17 10:38:45 +03:00 committed by GitHub
parent b590bc2df9
commit 9b0c006e16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 7419 additions and 2331 deletions

View file

@ -26,6 +26,7 @@ target
/src/plugins/data/common/es_query/kuery/ast/_generated_/**
/src/plugins/vis_type_timelion/public/_generated_/**
/src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.*
/src/plugins/timelion/public/webpackShims/jquery.flot.*
/x-pack/legacy/plugins/**/__tests__/fixtures/**
/x-pack/plugins/apm/e2e/**/snapshots.js
/x-pack/plugins/apm/e2e/tmp/*

View file

@ -44,7 +44,7 @@
"src/plugins/telemetry_management_section"
],
"tileMap": "src/plugins/tile_map",
"timelion": ["src/legacy/core_plugins/timelion", "src/plugins/vis_type_timelion"],
"timelion": ["src/plugins/timelion", "src/plugins/vis_type_timelion"],
"uiActions": "src/plugins/ui_actions",
"visDefaultEditor": "src/plugins/vis_default_editor",
"visTypeMarkdown": "src/plugins/vis_type_markdown",

View file

@ -1,7 +1,7 @@
files:
include:
- 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss'
- 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss'
- 'src/plugins/timelion/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_xy/**/*.s+(a|c)ss'
- 'x-pack/plugins/canvas/**/*.s+(a|c)ss'

View file

@ -1,189 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { LegacyPluginApi, LegacyPluginInitializer } from 'src/legacy/plugin_discovery/types';
import { DEFAULT_APP_CATEGORIES } from '../../../core/server';
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
defaultMessage: 'experimental',
});
const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
new Plugin({
require: ['kibana', 'elasticsearch'],
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
ui: Joi.object({
enabled: Joi.boolean().default(false),
}).default(),
graphiteUrls: Joi.array()
.items(Joi.string().uri({ scheme: ['http', 'https'] }))
.default([]),
}).default();
},
// @ts-ignore
// https://github.com/elastic/kibana/pull/44039#discussion_r326582255
uiCapabilities() {
return {
timelion: {
save: true,
},
};
},
publicDir: resolve(__dirname, 'public'),
uiExports: {
app: {
title: 'Timelion',
order: 8000,
icon: 'plugins/timelion/icon.svg',
euiIconType: 'timelionApp',
main: 'plugins/timelion/app',
category: DEFAULT_APP_CATEGORIES.kibana,
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [resolve(__dirname, 'public/legacy')],
uiSettingDefaults: {
'timelion:showTutorial': {
name: i18n.translate('timelion.uiSettings.showTutorialLabel', {
defaultMessage: 'Show tutorial',
}),
value: false,
description: i18n.translate('timelion.uiSettings.showTutorialDescription', {
defaultMessage: 'Should I show the tutorial by default when entering the timelion app?',
}),
category: ['timelion'],
},
'timelion:es.timefield': {
name: i18n.translate('timelion.uiSettings.timeFieldLabel', {
defaultMessage: 'Time field',
}),
value: '@timestamp',
description: i18n.translate('timelion.uiSettings.timeFieldDescription', {
defaultMessage: 'Default field containing a timestamp when using {esParam}',
values: { esParam: '.es()' },
}),
category: ['timelion'],
},
'timelion:es.default_index': {
name: i18n.translate('timelion.uiSettings.defaultIndexLabel', {
defaultMessage: 'Default index',
}),
value: '_all',
description: i18n.translate('timelion.uiSettings.defaultIndexDescription', {
defaultMessage: 'Default elasticsearch index to search with {esParam}',
values: { esParam: '.es()' },
}),
category: ['timelion'],
},
'timelion:target_buckets': {
name: i18n.translate('timelion.uiSettings.targetBucketsLabel', {
defaultMessage: 'Target buckets',
}),
value: 200,
description: i18n.translate('timelion.uiSettings.targetBucketsDescription', {
defaultMessage: 'The number of buckets to shoot for when using auto intervals',
}),
category: ['timelion'],
},
'timelion:max_buckets': {
name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', {
defaultMessage: 'Maximum buckets',
}),
value: 2000,
description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', {
defaultMessage: 'The maximum number of buckets a single datasource can return',
}),
category: ['timelion'],
},
'timelion:default_columns': {
name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', {
defaultMessage: 'Default columns',
}),
value: 2,
description: i18n.translate('timelion.uiSettings.defaultColumnsDescription', {
defaultMessage: 'Number of columns on a timelion sheet by default',
}),
category: ['timelion'],
},
'timelion:default_rows': {
name: i18n.translate('timelion.uiSettings.defaultRowsLabel', {
defaultMessage: 'Default rows',
}),
value: 2,
description: i18n.translate('timelion.uiSettings.defaultRowsDescription', {
defaultMessage: 'Number of rows on a timelion sheet by default',
}),
category: ['timelion'],
},
'timelion:min_interval': {
name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', {
defaultMessage: 'Minimum interval',
}),
value: '1ms',
description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', {
defaultMessage: 'The smallest interval that will be calculated when using "auto"',
description:
'"auto" is a technical value in that context, that should not be translated.',
}),
category: ['timelion'],
},
'timelion:graphite.url': {
name: i18n.translate('timelion.uiSettings.graphiteURLLabel', {
defaultMessage: 'Graphite URL',
description:
'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
}),
value: (server: Legacy.Server) => {
const urls = server.config().get('timelion.graphiteUrls') as string[];
if (urls.length === 0) {
return null;
} else {
return urls[0];
}
},
description: i18n.translate('timelion.uiSettings.graphiteURLDescription', {
defaultMessage:
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
type: 'select',
options: (server: Legacy.Server) => server.config().get('timelion.graphiteUrls'),
category: ['timelion'],
},
'timelion:quandl.key': {
name: i18n.translate('timelion.uiSettings.quandlKeyLabel', {
defaultMessage: 'Quandl key',
}),
value: 'someKeyHere',
description: i18n.translate('timelion.uiSettings.quandlKeyDescription', {
defaultMessage: '{experimentalLabel} Your API key from www.quandl.com',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
category: ['timelion'],
},
},
},
});
// eslint-disable-next-line import/no-default-export
export default timelionPluginInitializer;

View file

@ -1,5 +0,0 @@
{
"author": "Rashid Khan <rashid@elastic.co>",
"name": "timelion",
"version": "kibana"
}

View file

@ -1,518 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
// required for `ngSanitize` angular module
import 'angular-sanitize';
import { i18n } from '@kbn/i18n';
import routes from 'ui/routes';
import { capabilities } from 'ui/capabilities';
import { docTitle } from 'ui/doc_title';
import { fatalError, toastNotifications } from 'ui/notify';
import { timefilter } from 'ui/timefilter';
import { npStart } from 'ui/new_platform';
import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs';
import { getTimezone } from '../../../../plugins/vis_type_timelion/public';
import 'uiExports/savedObjectTypes';
require('ui/i18n');
require('ui/autoload/all');
// TODO: remove ui imports completely (move to plugins)
import 'ui/directives/input_focus';
import './directives/saved_object_finder';
import 'ui/directives/listen';
import './directives/saved_object_save_as_checkbox';
import './services/saved_sheet_register';
import rootTemplate from 'plugins/timelion/index.html';
import { uiModules } from 'ui/modules';
import { loadKbnTopNavDirectives } from '../../../../plugins/kibana_legacy/public';
loadKbnTopNavDirectives(npStart.plugins.navigation.ui);
require('plugins/timelion/directives/cells/cells');
require('plugins/timelion/directives/fixed_element');
require('plugins/timelion/directives/fullscreen/fullscreen');
require('plugins/timelion/directives/timelion_expression_input');
require('plugins/timelion/directives/timelion_help/timelion_help');
require('plugins/timelion/directives/timelion_interval/timelion_interval');
require('plugins/timelion/directives/timelion_save_sheet');
require('plugins/timelion/directives/timelion_load_sheet');
require('plugins/timelion/directives/timelion_options_sheet');
document.title = 'Timelion - Kibana';
const app = uiModules.get('apps/timelion', ['i18n', 'ngSanitize']);
routes.enable();
routes.when('/:id?', {
template: rootTemplate,
reloadOnSearch: false,
k7Breadcrumbs: ($injector, $route) =>
$injector.invoke($route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs),
badge: (uiCapabilities) => {
if (uiCapabilities.timelion.save) {
return undefined;
}
return {
text: i18n.translate('timelion.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('timelion.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Timelion sheets',
}),
iconType: 'glasses',
};
},
resolve: {
savedSheet: function (redirectWhenMissing, savedSheets, $route) {
return savedSheets
.get($route.current.params.id)
.then((savedSheet) => {
if ($route.current.params.id) {
npStart.core.chrome.recentlyAccessed.add(
savedSheet.getFullPath(),
savedSheet.title,
savedSheet.id
);
}
return savedSheet;
})
.catch(
redirectWhenMissing({
search: '/',
})
);
},
},
});
const location = 'Timelion';
app.controller('timelion', function (
$http,
$route,
$routeParams,
$scope,
$timeout,
AppState,
config,
kbnUrl
) {
// Keeping this at app scope allows us to keep the current page when the user
// switches to say, the timepicker.
$scope.page = config.get('timelion:showTutorial', true) ? 1 : 0;
$scope.setPage = (page) => ($scope.page = page);
timefilter.enableAutoRefreshSelector();
timefilter.enableTimeRangeSelector();
const savedVisualizations = npStart.plugins.visualizations.savedVisualizationsLoader;
const timezone = getTimezone(config);
const defaultExpression = '.es(*)';
const savedSheet = $route.current.locals.savedSheet;
$scope.topNavMenu = getTopNavMenu();
$timeout(function () {
if (config.get('timelion:showTutorial', true)) {
$scope.toggleMenu('showHelp');
}
}, 0);
$scope.transient = {};
$scope.state = new AppState(getStateDefaults());
function getStateDefaults() {
return {
sheet: savedSheet.timelion_sheet,
selected: 0,
columns: savedSheet.timelion_columns,
rows: savedSheet.timelion_rows,
interval: savedSheet.timelion_interval,
};
}
function getTopNavMenu() {
const newSheetAction = {
id: 'new',
label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', {
defaultMessage: 'New',
}),
description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', {
defaultMessage: 'New Sheet',
}),
run: function () {
kbnUrl.change('/');
},
testId: 'timelionNewButton',
};
const addSheetAction = {
id: 'add',
label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', {
defaultMessage: 'Add',
}),
description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', {
defaultMessage: 'Add a chart',
}),
run: function () {
$scope.$evalAsync(() => $scope.newCell());
},
testId: 'timelionAddChartButton',
};
const saveSheetAction = {
id: 'save',
label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', {
defaultMessage: 'Save',
}),
description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', {
defaultMessage: 'Save Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showSave'));
},
testId: 'timelionSaveButton',
};
const deleteSheetAction = {
id: 'delete',
label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', {
defaultMessage: 'Delete',
}),
description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', {
defaultMessage: 'Delete current sheet',
}),
disableButton: function () {
return !savedSheet.id;
},
run: function () {
const title = savedSheet.title;
function doDelete() {
savedSheet
.delete()
.then(() => {
toastNotifications.addSuccess(
i18n.translate('timelion.topNavMenu.delete.modal.successNotificationText', {
defaultMessage: `Deleted '{title}'`,
values: { title },
})
);
kbnUrl.change('/');
})
.catch((error) => fatalError(error, location));
}
const confirmModalOptions = {
confirmButtonText: i18n.translate('timelion.topNavMenu.delete.modal.confirmButtonLabel', {
defaultMessage: 'Delete',
}),
title: i18n.translate('timelion.topNavMenu.delete.modalTitle', {
defaultMessage: `Delete Timelion sheet '{title}'?`,
values: { title },
}),
};
$scope.$evalAsync(() => {
npStart.core.overlays
.openConfirm(
i18n.translate('timelion.topNavMenu.delete.modal.warningText', {
defaultMessage: `You can't recover deleted sheets.`,
}),
confirmModalOptions
)
.then((isConfirmed) => {
if (isConfirmed) {
doDelete();
}
});
});
},
testId: 'timelionDeleteButton',
};
const openSheetAction = {
id: 'open',
label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', {
defaultMessage: 'Open',
}),
description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', {
defaultMessage: 'Open Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showLoad'));
},
testId: 'timelionOpenButton',
};
const optionsAction = {
id: 'options',
label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', {
defaultMessage: 'Options',
}),
description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', {
defaultMessage: 'Options',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showOptions'));
},
testId: 'timelionOptionsButton',
};
const helpAction = {
id: 'help',
label: i18n.translate('timelion.topNavMenu.helpButtonLabel', {
defaultMessage: 'Help',
}),
description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', {
defaultMessage: 'Help',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showHelp'));
},
testId: 'timelionDocsButton',
};
if (capabilities.get().timelion.save) {
return [
newSheetAction,
addSheetAction,
saveSheetAction,
deleteSheetAction,
openSheetAction,
optionsAction,
helpAction,
];
}
return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction];
}
let refresher;
const setRefreshData = function () {
if (refresher) $timeout.cancel(refresher);
const interval = timefilter.getRefreshInterval();
if (interval.value > 0 && !interval.pause) {
function startRefresh() {
refresher = $timeout(function () {
if (!$scope.running) $scope.search();
startRefresh();
}, interval.value);
}
startRefresh();
}
};
const init = function () {
$scope.running = false;
$scope.search();
setRefreshData();
$scope.model = {
timeRange: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
};
$scope.$listen($scope.state, 'fetch_with_changes', $scope.search);
timefilter.getFetch$().subscribe($scope.search);
$scope.opts = {
saveExpression: saveExpression,
saveSheet: saveSheet,
savedSheet: savedSheet,
state: $scope.state,
search: $scope.search,
dontShowHelp: function () {
config.set('timelion:showTutorial', false);
$scope.setPage(0);
$scope.closeMenus();
},
};
$scope.menus = {
showHelp: false,
showSave: false,
showLoad: false,
showOptions: false,
};
$scope.toggleMenu = (menuName) => {
const curState = $scope.menus[menuName];
$scope.closeMenus();
$scope.menus[menuName] = !curState;
};
$scope.closeMenus = () => {
_.forOwn($scope.menus, function (value, key) {
$scope.menus[key] = false;
});
};
};
$scope.onTimeUpdate = function ({ dateRange }) {
$scope.model.timeRange = {
...dateRange,
};
timefilter.setTime(dateRange);
};
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {
$scope.model.refreshInterval = {
pause: isPaused,
value: refreshInterval,
};
timefilter.setRefreshInterval({
pause: isPaused,
value: refreshInterval ? refreshInterval : $scope.refreshInterval.value,
});
setRefreshData();
};
$scope.$watch(
function () {
return savedSheet.lastSavedTitle;
},
function (newTitle) {
docTitle.change(savedSheet.id ? newTitle : undefined);
}
);
$scope.toggle = function (property) {
$scope[property] = !$scope[property];
};
$scope.newSheet = function () {
kbnUrl.change('/', {});
};
$scope.newCell = function () {
$scope.state.sheet.push(defaultExpression);
$scope.state.selected = $scope.state.sheet.length - 1;
$scope.safeSearch();
};
$scope.setActiveCell = function (cell) {
$scope.state.selected = cell;
};
$scope.search = function () {
$scope.state.save();
$scope.running = true;
// parse the time range client side to make sure it behaves like other charts
const timeRangeBounds = timefilter.getBounds();
const httpResult = $http
.post('../api/timelion/run', {
sheet: $scope.state.sheet,
time: _.assignIn(
{
from: timeRangeBounds.min,
to: timeRangeBounds.max,
},
{
interval: $scope.state.interval,
timezone: timezone,
}
),
})
.then((resp) => resp.data)
.catch((resp) => {
throw resp.data;
});
httpResult
.then(function (resp) {
$scope.stats = resp.stats;
$scope.sheet = resp.sheet;
_.each(resp.sheet, function (cell) {
if (cell.exception) {
$scope.state.selected = cell.plot;
}
});
$scope.running = false;
})
.catch(function (resp) {
$scope.sheet = [];
$scope.running = false;
const err = new Error(resp.message);
err.stack = resp.stack;
toastNotifications.addError(err, {
title: i18n.translate('timelion.searchErrorTitle', {
defaultMessage: 'Timelion request error',
}),
});
});
};
$scope.safeSearch = _.debounce($scope.search, 500);
function saveSheet() {
savedSheet.timelion_sheet = $scope.state.sheet;
savedSheet.timelion_interval = $scope.state.interval;
savedSheet.timelion_columns = $scope.state.columns;
savedSheet.timelion_rows = $scope.state.rows;
savedSheet.save().then(function (id) {
if (id) {
toastNotifications.addSuccess({
title: i18n.translate('timelion.saveSheet.successNotificationText', {
defaultMessage: `Saved sheet '{title}'`,
values: { title: savedSheet.title },
}),
'data-test-subj': 'timelionSaveSuccessToast',
});
if (savedSheet.id !== $routeParams.id) {
kbnUrl.change('/{{id}}', { id: savedSheet.id });
}
}
});
}
function saveExpression(title) {
savedVisualizations.get({ type: 'timelion' }).then(function (savedExpression) {
savedExpression.visState.params = {
expression: $scope.state.sheet[$scope.state.selected],
interval: $scope.state.interval,
};
savedExpression.title = title;
savedExpression.visState.title = title;
savedExpression.save().then(function (id) {
if (id) {
toastNotifications.addSuccess(
i18n.translate('timelion.saveExpression.successNotificationText', {
defaultMessage: `Saved expression '{title}'`,
values: { title: savedExpression.title },
})
);
}
});
});
}
init();
});

View file

@ -1,54 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { move } from 'ui/utils/collection';
import { uiModules } from 'ui/modules';
require('angular-sortable-view');
require('plugins/timelion/directives/chart/chart');
require('plugins/timelion/directives/timelion_grid');
const app = uiModules.get('apps/timelion', ['angular-sortable-view']);
import html from './cells.html';
app.directive('timelionCells', function () {
return {
restrict: 'E',
scope: {
sheet: '=',
state: '=',
transient: '=',
onSearch: '=',
onSelect: '=',
},
template: html,
link: function ($scope) {
$scope.removeCell = function (index) {
_.pullAt($scope.state.sheet, index);
$scope.onSearch();
};
$scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) {
$scope.onSelect(indexTo);
move($scope.sheet, indexFrom, indexTo);
};
},
};
});

View file

@ -1,50 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import { uiModules } from 'ui/modules';
const app = uiModules.get('apps/timelion', []);
app.directive('fixedElementRoot', function () {
return {
restrict: 'A',
link: function ($elem) {
let fixedAt;
$(window).bind('scroll', function () {
const fixed = $('[fixed-element]', $elem);
const body = $('[fixed-element-body]', $elem);
const top = fixed.offset().top;
if ($(window).scrollTop() > top) {
// This is a gross hack, but its better than it was. I guess
fixedAt = $(window).scrollTop();
fixed.addClass(fixed.attr('fixed-element'));
body.addClass(fixed.attr('fixed-element-body'));
body.css({ top: fixed.height() });
}
if ($(window).scrollTop() < fixedAt) {
fixed.removeClass(fixed.attr('fixed-element'));
body.removeClass(fixed.attr('fixed-element-body'));
body.removeAttr('style');
}
});
},
};
});

View file

@ -1,315 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import rison from 'rison-node';
import { uiModules } from 'ui/modules';
import 'ui/directives/input_focus';
import savedObjectFinderTemplate from './saved_object_finder.html';
import { savedSheetLoader } from '../services/saved_sheets';
import { keyMap } from 'ui/directives/key_map';
import {
PaginateControlsDirectiveProvider,
PaginateDirectiveProvider,
} from '../../../../../plugins/kibana_legacy/public';
import { PER_PAGE_SETTING } from '../../../../../plugins/saved_objects/common';
import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../../plugins/visualizations/public';
const module = uiModules.get('kibana');
module
.directive('paginate', PaginateDirectiveProvider)
.directive('paginateControls', PaginateControlsDirectiveProvider)
.directive('savedObjectFinder', function ($location, kbnUrl, Private, config) {
return {
restrict: 'E',
scope: {
type: '@',
// optional make-url attr, sets the userMakeUrl in our scope
userMakeUrl: '=?makeUrl',
// optional on-choose attr, sets the userOnChoose in our scope
userOnChoose: '=?onChoose',
// optional useLocalManagement attr, removes link to management section
useLocalManagement: '=?useLocalManagement',
/**
* @type {function} - an optional function. If supplied an `Add new X` button is shown
* and this function is called when clicked.
*/
onAddNew: '=',
/**
* @{type} boolean - set this to true, if you don't want the search box above the
* table to automatically gain focus once loaded
*/
disableAutoFocus: '=',
},
template: savedObjectFinderTemplate,
controllerAs: 'finder',
controller: function ($scope, $element) {
const self = this;
// the text input element
const $input = $element.find('input[ng-model=filter]');
// The number of items to show in the list
$scope.perPage = config.get(PER_PAGE_SETTING);
// the list that will hold the suggestions
const $list = $element.find('ul');
// the current filter string, used to check that returned results are still useful
let currentFilter = $scope.filter;
// the most recently entered search/filter
let prevSearch;
// the list of hits, used to render display
self.hits = [];
self.service = savedSheetLoader;
self.properties = self.service.loaderProperties;
filterResults();
/**
* Boolean that keeps track of whether hits are sorted ascending (true)
* or descending (false) by title
* @type {Boolean}
*/
self.isAscending = true;
/**
* Sorts saved object finder hits either ascending or descending
* @param {Array} hits Array of saved finder object hits
* @return {Array} Array sorted either ascending or descending
*/
self.sortHits = function (hits) {
self.isAscending = !self.isAscending;
self.hits = self.isAscending
? _.sortBy(hits, 'title')
: _.sortBy(hits, 'title').reverse();
};
/**
* Passed the hit objects and will determine if the
* hit should have a url in the UI, returns it if so
* @return {string|null} - the url or nothing
*/
self.makeUrl = function (hit) {
if ($scope.userMakeUrl) {
return $scope.userMakeUrl(hit);
}
if (!$scope.userOnChoose) {
return hit.url;
}
return '#';
};
self.preventClick = function ($event) {
$event.preventDefault();
};
/**
* Called when a hit object is clicked, can override the
* url behavior if necessary.
*/
self.onChoose = function (hit, $event) {
if ($scope.userOnChoose) {
$scope.userOnChoose(hit, $event);
}
const url = self.makeUrl(hit);
if (!url || url === '#' || url.charAt(0) !== '#') return;
$event.preventDefault();
// we want the '/path', not '#/path'
kbnUrl.change(url.substr(1));
};
$scope.$watch('filter', function (newFilter) {
// ensure that the currentFilter changes from undefined to ''
// which triggers
currentFilter = newFilter || '';
filterResults();
});
$scope.pageFirstItem = 0;
$scope.pageLastItem = 0;
$scope.onPageChanged = (page) => {
$scope.pageFirstItem = page.firstItem;
$scope.pageLastItem = page.lastItem;
};
//manages the state of the keyboard selector
self.selector = {
enabled: false,
index: -1,
};
self.getLabel = function () {
return _.words(self.properties.nouns).map(_.upperFirst).join(' ');
};
//key handler for the filter text box
self.filterKeyDown = function ($event) {
switch (keyMap[$event.keyCode]) {
case 'enter':
if (self.hitCount !== 1) return;
const hit = self.hits[0];
if (!hit) return;
self.onChoose(hit, $event);
$event.preventDefault();
break;
}
};
//key handler for the list items
self.hitKeyDown = function ($event, page, paginate) {
switch (keyMap[$event.keyCode]) {
case 'tab':
if (!self.selector.enabled) break;
self.selector.index = -1;
self.selector.enabled = false;
//if the user types shift-tab return to the textbox
//if the user types tab, set the focus to the currently selected hit.
if ($event.shiftKey) {
$input.focus();
} else {
$list.find('li.active a').focus();
}
$event.preventDefault();
break;
case 'down':
if (!self.selector.enabled) break;
if (self.selector.index + 1 < page.length) {
self.selector.index += 1;
}
$event.preventDefault();
break;
case 'up':
if (!self.selector.enabled) break;
if (self.selector.index > 0) {
self.selector.index -= 1;
}
$event.preventDefault();
break;
case 'right':
if (!self.selector.enabled) break;
if (page.number < page.count) {
paginate.goToPage(page.number + 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'left':
if (!self.selector.enabled) break;
if (page.number > 1) {
paginate.goToPage(page.number - 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'escape':
if (!self.selector.enabled) break;
$input.focus();
$event.preventDefault();
break;
case 'enter':
if (!self.selector.enabled) break;
const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index;
const hit = self.hits[hitIndex];
if (!hit) break;
self.onChoose(hit, $event);
$event.preventDefault();
break;
case 'shift':
break;
default:
$input.focus();
break;
}
};
self.hitBlur = function () {
self.selector.index = -1;
self.selector.enabled = false;
};
self.manageObjects = function (type) {
$location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type }));
};
self.hitCountNoun = function () {
return (self.hitCount === 1 ? self.properties.noun : self.properties.nouns).toLowerCase();
};
function selectTopHit() {
setTimeout(function () {
//triggering a focus event kicks off a new angular digest cycle.
$list.find('a:first').focus();
}, 0);
}
function filterResults() {
if (!self.service) return;
if (!self.properties) return;
// track the filter that we use for this search,
// but ensure that we don't search for the same
// thing twice. This is called from multiple places
// and needs to be smart about when it actually searches
const filter = currentFilter;
if (prevSearch === filter) return;
prevSearch = filter;
const isLabsEnabled = config.get(VISUALIZE_ENABLE_LABS_SETTING);
self.service.find(filter).then(function (hits) {
hits.hits = hits.hits.filter(
(hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental'
);
hits.total = hits.hits.length;
// ensure that we don't display old results
// as we can't really cancel requests
if (currentFilter === filter) {
self.hitCount = hits.total;
self.hits = _.sortBy(hits.hits, 'title');
}
});
}
},
};
});

View file

@ -1,281 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Timelion Expression Autocompleter
*
* This directive allows users to enter multiline timelion expressions. If the user has entered
* a valid expression and then types a ".", this directive will display a list of suggestions.
*
* Users can navigate suggestions using the arrow keys. When a user selects a suggestion, it's
* inserted into the expression and the caret position is updated to be inside of the newly-
* added function's parentheses.
*
* Beneath the hood, we use a PEG grammar to validate the Timelion expression and detect if
* the caret is in a position within the expression that allows functions to be suggested.
*
* NOTE: This directive doesn't work well with contenteditable divs. Challenges include:
* - You have to replace markup with newline characters and spaces when passing the expression
* to the grammar.
* - You have to do the opposite when loading a saved expression, so that it appears correctly
* within the contenteditable (i.e. replace newlines with <br> markup).
* - The Range and Selection APIs ignore newlines when providing caret position, so there is
* literally no way to insert suggestions into the correct place in a multiline expression
* that has more than a single consecutive newline.
*/
import _ from 'lodash';
import $ from 'jquery';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../../../../plugins/vis_type_timelion/common/chain.peg';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,
Suggestions,
suggest,
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeys } from '@elastic/eui';
import { npStart } from 'ui/new_platform';
const Parser = PEG.generate(grammar);
export function TimelionExpInput($http, $timeout) {
return {
restrict: 'E',
scope: {
rows: '=',
sheet: '=',
updateChart: '&',
shouldPopoverSuggestions: '@',
},
replace: true,
template: timelionExpressionInputTemplate,
link: function (scope, elem) {
const argValueSuggestions = npStart.plugins.visTypeTimelion.getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
scope.suggestions = new Suggestions();
function init() {
$http.get('../api/timelion/functions').then(function (resp) {
Object.assign(functionReference, {
byName: _.keyBy(resp.data, 'name'),
list: resp.data,
});
});
}
function setCaretOffset(caretOffset) {
// Wait for Angular to update the input with the new expression and *then* we can set
// the caret position.
$timeout(() => {
expressionInput.focus();
expressionInput[0].selectionStart = expressionInput[0].selectionEnd = caretOffset;
scope.$apply();
}, 0);
}
function insertSuggestionIntoExpression(suggestionIndex) {
if (scope.suggestions.isEmpty()) {
return;
}
const { min, max } = suggestibleFunctionLocation;
let insertedValue;
let insertPositionMinOffset = 0;
switch (scope.suggestions.type) {
case SUGGESTION_TYPE.FUNCTIONS: {
// Position the caret inside of the function parentheses.
insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`;
// min advanced one to not replace function '.'
insertPositionMinOffset = 1;
break;
}
case SUGGESTION_TYPE.ARGUMENTS: {
// Position the caret after the '='
insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`;
break;
}
case SUGGESTION_TYPE.ARGUMENT_VALUE: {
// Position the caret after the argument value
insertedValue = `${scope.suggestions.list[suggestionIndex].name}`;
break;
}
}
const updatedExpression = insertAtLocation(
insertedValue,
scope.sheet,
min + insertPositionMinOffset,
max
);
scope.sheet = updatedExpression;
const newCaretOffset = min + insertedValue.length;
setCaretOffset(newCaretOffset);
}
function scrollToSuggestionAt(index) {
// We don't cache these because the list changes based on user input.
const suggestionsList = $('[data-suggestions-list]');
const suggestionListItem = $('[data-suggestion-list-item]')[index];
// Scroll to the position of the item relative to the list, not to the window.
suggestionsList.scrollTop(suggestionListItem.offsetTop - suggestionsList[0].offsetTop);
}
function getCursorPosition() {
if (expressionInput.length) {
return expressionInput[0].selectionStart;
}
return null;
}
async function getSuggestions() {
const suggestions = await suggest(
scope.sheet,
functionReference.list,
Parser,
getCursorPosition(),
argValueSuggestions
);
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
scope.$apply(() => {
if (suggestions) {
scope.suggestions.setList(suggestions.list, suggestions.type);
scope.suggestions.show();
suggestibleFunctionLocation = suggestions.location;
$timeout(() => {
const suggestionsList = $('[data-suggestions-list]');
suggestionsList.scrollTop(0);
}, 0);
return;
}
suggestibleFunctionLocation = undefined;
scope.suggestions.reset();
});
}
function isNavigationalKey(key) {
const keyCodes = _.values(comboBoxKeys);
return keyCodes.includes(key);
}
scope.onFocusInput = () => {
// Wait for the caret position of the input to update and then we can get suggestions
// (which depends on the caret position).
$timeout(getSuggestions, 0);
};
scope.onBlurInput = () => {
scope.suggestions.hide();
};
scope.onKeyDownInput = (e) => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation. The keyup handler will pull up suggestions.
if (!isNavigationalKey(e.key)) {
return;
}
switch (e.keyCode) {
case comboBoxKeys.ARROW_UP:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepForward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeys.ARROW_DOWN:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepBackward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeys.TAB:
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) {
// Before letting the tab be handled to focus the next element
// we need to hide the suggestions, otherwise it will focus these
// instead of the time interval select.
scope.suggestions.hide();
return;
}
// If we have suggestions, complete the selected one.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
break;
case comboBoxKeys.ENTER:
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
scope.updateChart();
} else if (!scope.suggestions.isEmpty()) {
// If the suggestions are open, complete the expression with the suggestion.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
}
break;
case comboBoxKeys.ESCAPE:
e.preventDefault();
scope.suggestions.hide();
break;
}
};
scope.onKeyUpInput = (e) => {
// If the user isn't navigating, then we should update the suggestions based on their input.
if (!isNavigationalKey(e.key)) {
getSuggestions();
}
};
scope.onClickExpression = () => {
getSuggestions();
};
scope.onClickSuggestion = (index) => {
insertSuggestionIntoExpression(index);
};
scope.getActiveSuggestionId = () => {
if (scope.suggestions.isVisible && scope.suggestions.index > -1) {
return `timelionSuggestion${scope.suggestions.index}`;
}
return '';
};
init();
},
};
}

View file

@ -1,68 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import { uiModules } from 'ui/modules';
const app = uiModules.get('apps/timelion', []);
app.directive('timelionGrid', function () {
return {
restrict: 'A',
scope: {
timelionGridRows: '=',
timelionGridColumns: '=',
},
link: function ($scope, $elem) {
function init() {
setDimensions();
}
$scope.$on('$destroy', function () {
$(window).off('resize'); //remove the handler added earlier
});
$(window).resize(function () {
setDimensions();
});
$scope.$watchMulti(['timelionGridColumns', 'timelionGridRows'], function () {
setDimensions();
});
function setDimensions() {
const borderSize = 2;
const headerSize = 45 + 35 + 28 + 20 * 2; // chrome + subnav + buttons + (container padding)
const verticalPadding = 10;
if ($scope.timelionGridColumns != null) {
$elem.width($elem.parent().width() / $scope.timelionGridColumns - borderSize * 2);
}
if ($scope.timelionGridRows != null) {
$elem.height(
($(window).height() - headerSize) / $scope.timelionGridRows -
(verticalPadding + borderSize * 2)
);
}
}
init();
},
};
});

View file

@ -1,168 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import template from './timelion_help.html';
import { i18n } from '@kbn/i18n';
import { uiModules } from 'ui/modules';
import _ from 'lodash';
import moment from 'moment';
import '../../components/timelionhelp_tabs_directive';
const app = uiModules.get('apps/timelion', []);
app.directive('timelionHelp', function ($http) {
return {
restrict: 'E',
template,
controller: function ($scope) {
$scope.functions = {
list: [],
details: null,
};
$scope.activeTab = 'funcref';
$scope.activateTab = function (tabName) {
$scope.activeTab = tabName;
};
function init() {
$scope.es = {
invalidCount: 0,
};
$scope.translations = {
nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', {
defaultMessage: 'Next',
}),
previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', {
defaultMessage: 'Previous',
}),
dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', {
defaultMessage: `Don't show this again`,
}),
strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', {
defaultMessage: 'Next',
}),
emphasizedEverythingText: i18n.translate(
'timelion.help.welcome.content.emphasizedEverythingText',
{
defaultMessage: 'everything',
}
),
notValidAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.notValid.advancedSettingsPathText',
{
defaultMessage: 'Management / Kibana / Advanced Settings',
}
),
validAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.valid.advancedSettingsPathText',
{
defaultMessage: 'Management/Kibana/Advanced Settings',
}
),
esAsteriskQueryDescription: i18n.translate(
'timelion.help.querying.esAsteriskQueryDescriptionText',
{
defaultMessage: 'hey Elasticsearch, find everything in my default index',
}
),
esIndexQueryDescription: i18n.translate(
'timelion.help.querying.esIndexQueryDescriptionText',
{
defaultMessage: 'use * as the q (query) for the logstash-* index',
}
),
strongAddText: i18n.translate('timelion.help.expressions.strongAddText', {
defaultMessage: 'Add',
}),
twoExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.twoExpressionsDescriptionTitle',
{
defaultMessage: 'Double the fun.',
}
),
customStylingDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.customStylingDescriptionTitle',
{
defaultMessage: 'Custom styling.',
}
),
namedArgumentsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.namedArgumentsDescriptionTitle',
{
defaultMessage: 'Named arguments.',
}
),
groupedExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.groupedExpressionsDescriptionTitle',
{
defaultMessage: 'Grouped expressions.',
}
),
};
getFunctions();
checkElasticsearch();
}
function getFunctions() {
return $http.get('../api/timelion/functions').then(function (resp) {
$scope.functions.list = resp.data;
});
}
$scope.recheckElasticsearch = function () {
$scope.es.valid = null;
checkElasticsearch().then(function (valid) {
if (!valid) $scope.es.invalidCount++;
});
};
function checkElasticsearch() {
return $http.get('../api/timelion/validate/es').then(function (resp) {
if (resp.data.ok) {
$scope.es.valid = true;
$scope.es.stats = {
min: moment(resp.data.min).format('LLL'),
max: moment(resp.data.max).format('LLL'),
field: resp.data.field,
};
} else {
$scope.es.valid = false;
$scope.es.invalidReason = (function () {
try {
const esResp = JSON.parse(resp.data.resp.response);
return _.get(esResp, 'error.root_cause[0].reason');
} 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 i18n.translate('timelion.help.unknownErrorMessage', {
defaultMessage: 'Unknown error',
});
}
})();
}
return $scope.es.valid;
});
}
init();
},
};
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="120mm"
height="120mm"
viewBox="0 0 425.19685039 425.19685039"
id="svg2816"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="icon.svg">
<defs
id="defs2818">
<clipPath
id="clipPath8447"
clipPathUnits="userSpaceOnUse">
<path
inkscape:connector-curvature="0"
id="path8449"
d="m 0,440.426 600,0 L 600,0 0,0 0,440.426 Z" />
</clipPath>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="131.22671"
inkscape:cy="378.13107"
inkscape:document-units="mm"
inkscape:current-layer="Template"
id="namedview2820"
showgrid="false"
units="mm"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="true"
inkscape:window-width="1469"
inkscape:window-height="956"
inkscape:window-maximized="0"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-grids="true"
inkscape:window-x="219"
inkscape:window-y="0">
<sodipodi:guide
orientation="1,0"
position="212.59843,0"
id="guide3337" />
<sodipodi:guide
orientation="0,1"
position="0,212.59843"
id="guide3339" />
<inkscape:grid
type="xygrid"
id="grid3351"
units="mm"
spacingx="1mm"
spacingy="1mm"
empspacing="5" />
</sodipodi:namedview>
<metadata
id="metadata2822">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="Template"
inkscape:label="Layer 1"
style="fill:none;stroke:#ffffff;stroke-width:0.35433071px">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.40064034px"
d="m 212.51556,43.816204 c -9.94498,-0.10448 -19.76629,0.48399 -29.2095,1.7876 -20.46985,2.82579 -44.80699,10.19267 -63.24889,19.14519 l -4.40117,2.1364 -6.10156,-1.45334 c -9.20676,-2.19337 -20.126509,-3.40347 -34.109639,-3.78106 -24.50715,-0.66175 -46.57065,2.51896 -60.56992,8.73208 l -5.9513901,2.64021 -1.93292,6.24447 c -1.06277,3.43372 -2.4151,8.63049 -3.00598,11.54913 -1.53125,7.56139 -2.05401,22.687276 -1.0682,30.902626 1.65858,13.82185 6.7527,29.78901 13.1066101,41.06865 l 3.20218,5.68496 -2.17516,6.27838 c -6.02018,17.37428 -8.0762301,29.39951 -8.0684001,47.17021 0,15.17828 1.2843301,23.63498 5.5904801,36.97997 12.82745,39.75098 49.84408,72.52427 101.466619,89.83514 5.80702,1.94729 6.68198,2.47716 11.54186,7.0099 6.66504,6.21636 14.39167,11.3493 23.05222,15.31566 9.78729,4.48231 16.27558,6.2191 29.79809,7.9691 20.28599,2.62519 35.75405,3.02196 53.96213,1.38551 10.00303,-0.89906 22.3711,-2.84396 27.60841,-4.34061 12.30523,-3.51643 24.69915,-10.50496 34.98405,-19.72652 7.02577,-6.29943 7.51497,-6.60945 14.28867,-9.0155 47.51848,-16.87912 81.2963,-46.48266 95.69689,-83.87405 6.04823,-15.70401 8.67135,-34.47172 7.17944,-51.3631 -1.01092,-11.44237 -3.54478,-23.93594 -6.7943,-33.49439 -1.35294,-3.97957 -2.45855,-7.52644 -2.45855,-7.88189 0,-0.35549 1.88245,-4.08107 4.18315,-8.27913 14.6159,-26.67106 17.6998,-57.67627 8.47534,-85.211126 l -1.51872,-4.53195 -4.69426,-2.10492 c -21.65103,-9.70728 -66.27117,-12.03073 -96.50835,-5.02609 l -6.55693,1.51872 -12.08444,-5.25862 c -6.64658,-2.89168 -15.19046,-6.27668 -18.98775,-7.5234 -20.33766,-6.6772 -42.81112,-10.25834 -64.69011,-10.48821 z M 114.9584,120.1596 c 0.89827,0 32.56973,11.89177 33.22312,12.4744 1.08465,0.96712 -1.52492,5.9751 -4.57314,8.7757 -1.55336,1.42707 -4.13505,3.09699 -5.73581,3.70841 -15.64828,5.97722 -31.56691,-8.02023 -24.881,-21.87746 0.8177,-1.69469 1.70241,-3.08105 1.96683,-3.08105 z m 184.42024,0.62977 c 0.10351,0.009 0.1807,0.0276 0.22527,0.0606 1.25451,0.92826 3.06168,6.41925 3.06168,9.30132 0,5.85906 -4.47397,11.95187 -10.69651,14.56961 -7.49256,3.15191 -14.37348,2.33937 -20.76566,-2.45612 -2.33787,-1.75398 -5.64618,-6.93055 -5.64618,-8.81687 0.005,-1.53511 30.61194,-12.90841 33.8214,-12.6585 z m -88.55862,28.93337 c 14.37063,-0.027 28.75317,2.4358 36.61178,7.55488 10.61987,6.91764 7.19471,20.25668 -8.13622,31.68501 -5.51941,4.11429 -15.42291,8.75917 -21.67882,10.16846 -7.75654,1.74733 -13.53947,0.90038 -23.13459,-3.38625 -14.44842,-6.45479 -25.26854,-17.48329 -26.11875,-26.625 -0.46187,-4.96531 0.28073,-7.30805 3.1513,-9.94318 6.7457,-6.19289 23.01858,-9.42333 39.3053,-9.45392 z m -0.46507,83.43806 c 8.82994,1e-5 21.90971,3.55346 32.18398,8.74421 15.77369,7.9691 30.57783,21.1565 35.90208,31.98051 3.68786,7.4971 4.18188,16.00812 1.26924,21.86534 -5.22267,10.50284 -25.03716,18.03508 -53.7199,20.42411 -18.1235,1.50956 -40.77834,-0.15131 -56.23658,-4.12259 -16.44895,-4.22581 -26.40281,-10.46652 -28.75897,-18.03341 -4.53633,-14.56882 8.41138,-34.36087 31.45973,-48.0931 12.762,-7.60349 28.08632,-12.76507 37.90042,-12.76507 z"
id="path4206"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1,35 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializerContext } from 'kibana/public';
import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from '.';
import { TimelionPluginSetupDependencies } from './plugin';
import { LegacyDependenciesPlugin } from './shim';
const setupPlugins: Readonly<TimelionPluginSetupDependencies> = {
// Temporary solution
// It will be removed when all dependent services are migrated to the new platform.
__LEGACY: new LegacyDependenciesPlugin(),
};
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
export const start = pluginInstance.start(npStart.core, npStart.plugins);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,74 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
CoreSetup,
Plugin,
PluginInitializerContext,
IUiSettingsClient,
CoreStart,
} from 'kibana/public';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim';
import { KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public';
/** @internal */
export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup {
uiSettings: IUiSettingsClient;
timelionPanels: Map<string, Panel>;
}
/** @internal */
export interface TimelionPluginSetupDependencies {
// Temporary solution
__LEGACY: LegacyDependenciesPlugin;
}
/** @internal */
export class TimelionPlugin implements Plugin<Promise<void>, void> {
initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public async setup(core: CoreSetup, { __LEGACY }: TimelionPluginSetupDependencies) {
const timelionPanels: Map<string, Panel> = new Map();
const dependencies: TimelionVisualizationDependencies = {
uiSettings: core.uiSettings,
timelionPanels,
...(await __LEGACY.setup(core, timelionPanels)),
};
this.registerPanels(dependencies);
}
private registerPanels(dependencies: TimelionVisualizationDependencies) {
const timeChartPanel: Panel = getTimeChart(dependencies);
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) {
kibanaLegacy.loadFontAwesome();
}
public stop(): void {}
}

View file

@ -1,52 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { npStart } from 'ui/new_platform';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public';
import { createSavedSheetClass } from './_saved_sheet';
const module = uiModules.get('app/sheet');
const savedObjectsClient = npStart.core.savedObjects.client;
const services = {
savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};
const SavedSheet = createSavedSheetClass(services, npStart.core.uiSettings);
export const savedSheetLoader = new SavedObjectLoader(
SavedSheet,
savedObjectsClient,
npStart.core.chrome
);
savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`;
// Customize loader properties since adding an 's' on type doesn't work for type 'timelion-sheet'.
savedSheetLoader.loaderProperties = {
name: 'timelion-sheet',
noun: 'Saved Sheets',
nouns: 'saved sheets',
};
// This is the only thing that gets injected into controllers
module.service('savedSheets', () => savedSheetLoader);

View file

@ -1,55 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import 'ngreact';
import 'brace/mode/hjson';
import 'brace/ext/searchbox';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import { once } from 'lodash';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { Panel } from '../panels/panel';
// @ts-ignore
import { Chart } from '../directives/chart/chart';
// @ts-ignore
import { TimelionInterval } from '../directives/timelion_interval/timelion_interval';
// @ts-ignore
import { TimelionExpInput } from '../directives/timelion_expression_input';
// @ts-ignore
import { TimelionExpressionSuggestions } from '../directives/timelion_expression_suggestions/timelion_expression_suggestions';
/** @internal */
export const initTimelionLegacyModule = once((timelionPanels: Map<string, Panel>): void => {
require('ui/state_management/app_state');
uiModules
.get('apps/timelion', [])
.controller('TimelionVisController', function ($scope: any) {
$scope.$on('timelionChartRendered', (event: any) => {
event.stopPropagation();
$scope.renderComplete();
});
})
.constant('timelionPanels', timelionPanels)
.directive('chart', Chart)
.directive('timelionInterval', TimelionInterval)
.directive('timelionExpressionSuggestions', TimelionExpressionSuggestions)
.directive('timelionExpressionInput', TimelionExpInput);
});

View file

@ -21,6 +21,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { encode as encodeRison } from 'rison-node';
import uiRoutes from 'ui/routes';
import '../../private';
import { toastNotifications } from '../../notify';
import * as FatalErrorNS from '../../notify/fatal_error';
@ -38,6 +39,8 @@ describe('State Management', () => {
const sandbox = sinon.createSandbox();
afterEach(() => sandbox.restore());
uiRoutes.enable();
describe('Enabled', () => {
let $rootScope;
let $location;

View file

@ -36,6 +36,7 @@ export {
isErrorNonFatal,
} from './saved_object';
export { SavedObjectSaveOpts, SavedObjectKibanaServices, SavedObject } from './types';
export { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common';
export { SavedObjectsStart } from './plugin';
export const plugin = () => new SavedObjectsPublicPlugin();

View file

@ -1,8 +1,19 @@
{
"id": "timelion",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": "timelion",
"ui": false,
"server": true
"version": "kibana",
"ui": true,
"server": true,
"requiredBundles": [
"kibanaLegacy",
"kibanaUtils",
"savedObjects",
"visTypeTimelion"
],
"requiredPlugins": [
"visualizations",
"data",
"navigation",
"visTypeTimelion",
"kibanaLegacy"
]
}

View file

@ -0,0 +1,661 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { createHashHistory } from 'history';
import { createKbnUrlStateStorage } from '../../kibana_utils/public';
import { syncQueryStateWithUrl } from '../../data/public';
import { getSavedSheetBreadcrumbs, getCreateBreadcrumbs } from './breadcrumbs';
import {
addFatalError,
registerListenEventListener,
watchMultiDecorator,
} from '../../kibana_legacy/public';
import { getTimezone } from '../../vis_type_timelion/public';
import { initCellsDirective } from './directives/cells/cells';
import { initFullscreenDirective } from './directives/fullscreen/fullscreen';
import { initFixedElementDirective } from './directives/fixed_element';
import { initTimelionLoadSheetDirective } from './directives/timelion_load_sheet';
import { initTimelionHelpDirective } from './directives/timelion_help/timelion_help';
import { initTimelionSaveSheetDirective } from './directives/timelion_save_sheet';
import { initTimelionOptionsSheetDirective } from './directives/timelion_options_sheet';
import { initSavedObjectSaveAsCheckBoxDirective } from './directives/saved_object_save_as_checkbox';
import { initSavedObjectFinderDirective } from './directives/saved_object_finder';
import { initTimelionTabsDirective } from './components/timelionhelp_tabs_directive';
import { initInputFocusDirective } from './directives/input_focus';
import { Chart } from './directives/chart/chart';
import { TimelionInterval } from './directives/timelion_interval/timelion_interval';
import { timelionExpInput } from './directives/timelion_expression_input';
import { TimelionExpressionSuggestions } from './directives/timelion_expression_suggestions/timelion_expression_suggestions';
import { initSavedSheetService } from './services/saved_sheets';
import { initTimelionAppState } from './timelion_app_state';
import rootTemplate from './index.html';
export function initTimelionApp(app, deps) {
app.run(registerListenEventListener);
const savedSheetLoader = initSavedSheetService(app, deps);
app.factory('history', () => createHashHistory());
app.factory('kbnUrlStateStorage', (history) =>
createKbnUrlStateStorage({
history,
useHash: deps.core.uiSettings.get('state:storeInSessionStorage'),
})
);
app.config(watchMultiDecorator);
app
.controller('TimelionVisController', function ($scope) {
$scope.$on('timelionChartRendered', (event) => {
event.stopPropagation();
$scope.renderComplete();
});
})
.constant('timelionPanels', deps.timelionPanels)
.directive('chart', Chart)
.directive('timelionInterval', TimelionInterval)
.directive('timelionExpressionSuggestions', TimelionExpressionSuggestions)
.directive('timelionExpressionInput', timelionExpInput(deps));
initTimelionHelpDirective(app);
initInputFocusDirective(app);
initTimelionTabsDirective(app, deps);
initSavedObjectFinderDirective(app, savedSheetLoader, deps.core.uiSettings);
initSavedObjectSaveAsCheckBoxDirective(app);
initCellsDirective(app);
initFixedElementDirective(app);
initFullscreenDirective(app);
initTimelionSaveSheetDirective(app);
initTimelionLoadSheetDirective(app);
initTimelionOptionsSheetDirective(app);
const location = 'Timelion';
app.directive('timelionApp', function () {
return {
restrict: 'E',
controllerAs: 'timelionApp',
controller: timelionController,
};
});
function timelionController(
$http,
$route,
$routeParams,
$scope,
$timeout,
history,
kbnUrlStateStorage
) {
// Keeping this at app scope allows us to keep the current page when the user
// switches to say, the timepicker.
$scope.page = deps.core.uiSettings.get('timelion:showTutorial', true) ? 1 : 0;
$scope.setPage = (page) => ($scope.page = page);
const timefilter = deps.plugins.data.query.timefilter.timefilter;
timefilter.enableAutoRefreshSelector();
timefilter.enableTimeRangeSelector();
deps.core.chrome.docTitle.change('Timelion - Kibana');
// starts syncing `_g` portion of url with query services
const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl(
deps.plugins.data.query,
kbnUrlStateStorage
);
const savedSheet = $route.current.locals.savedSheet;
function getStateDefaults() {
return {
sheet: savedSheet.timelion_sheet,
selected: 0,
columns: savedSheet.timelion_columns,
rows: savedSheet.timelion_rows,
interval: savedSheet.timelion_interval,
};
}
const { stateContainer, stopStateSync } = initTimelionAppState({
stateDefaults: getStateDefaults(),
kbnUrlStateStorage,
});
$scope.state = _.cloneDeep(stateContainer.getState());
$scope.expression = _.clone($scope.state.sheet[$scope.state.selected]);
$scope.updatedSheets = [];
const savedVisualizations = deps.plugins.visualizations.savedVisualizationsLoader;
const timezone = getTimezone(deps.core.uiSettings);
const defaultExpression = '.es(*)';
$scope.topNavMenu = getTopNavMenu();
$timeout(function () {
if (deps.core.uiSettings.get('timelion:showTutorial', true)) {
$scope.toggleMenu('showHelp');
}
}, 0);
$scope.transient = {};
function getTopNavMenu() {
const newSheetAction = {
id: 'new',
label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', {
defaultMessage: 'New',
}),
description: i18n.translate('timelion.topNavMenu.newSheetButtonAriaLabel', {
defaultMessage: 'New Sheet',
}),
run: function () {
history.push('/');
$route.reload();
},
testId: 'timelionNewButton',
};
const addSheetAction = {
id: 'add',
label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', {
defaultMessage: 'Add',
}),
description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', {
defaultMessage: 'Add a chart',
}),
run: function () {
$scope.$evalAsync(() => $scope.newCell());
},
testId: 'timelionAddChartButton',
};
const saveSheetAction = {
id: 'save',
label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', {
defaultMessage: 'Save',
}),
description: i18n.translate('timelion.topNavMenu.saveSheetButtonAriaLabel', {
defaultMessage: 'Save Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showSave'));
},
testId: 'timelionSaveButton',
};
const deleteSheetAction = {
id: 'delete',
label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', {
defaultMessage: 'Delete',
}),
description: i18n.translate('timelion.topNavMenu.deleteSheetButtonAriaLabel', {
defaultMessage: 'Delete current sheet',
}),
disableButton: function () {
return !savedSheet.id;
},
run: function () {
const title = savedSheet.title;
function doDelete() {
savedSheet
.delete()
.then(() => {
deps.core.notifications.toasts.addSuccess(
i18n.translate('timelion.topNavMenu.delete.modal.successNotificationText', {
defaultMessage: `Deleted '{title}'`,
values: { title },
})
);
history.push('/');
})
.catch((error) => addFatalError(deps.core.fatalErrors, error, location));
}
const confirmModalOptions = {
confirmButtonText: i18n.translate(
'timelion.topNavMenu.delete.modal.confirmButtonLabel',
{
defaultMessage: 'Delete',
}
),
title: i18n.translate('timelion.topNavMenu.delete.modalTitle', {
defaultMessage: `Delete Timelion sheet '{title}'?`,
values: { title },
}),
};
$scope.$evalAsync(() => {
deps.core.overlays
.openConfirm(
i18n.translate('timelion.topNavMenu.delete.modal.warningText', {
defaultMessage: `You can't recover deleted sheets.`,
}),
confirmModalOptions
)
.then((isConfirmed) => {
if (isConfirmed) {
doDelete();
}
});
});
},
testId: 'timelionDeleteButton',
};
const openSheetAction = {
id: 'open',
label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', {
defaultMessage: 'Open',
}),
description: i18n.translate('timelion.topNavMenu.openSheetButtonAriaLabel', {
defaultMessage: 'Open Sheet',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showLoad'));
},
testId: 'timelionOpenButton',
};
const optionsAction = {
id: 'options',
label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', {
defaultMessage: 'Options',
}),
description: i18n.translate('timelion.topNavMenu.optionsButtonAriaLabel', {
defaultMessage: 'Options',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showOptions'));
},
testId: 'timelionOptionsButton',
};
const helpAction = {
id: 'help',
label: i18n.translate('timelion.topNavMenu.helpButtonLabel', {
defaultMessage: 'Help',
}),
description: i18n.translate('timelion.topNavMenu.helpButtonAriaLabel', {
defaultMessage: 'Help',
}),
run: () => {
$scope.$evalAsync(() => $scope.toggleMenu('showHelp'));
},
testId: 'timelionDocsButton',
};
if (deps.core.application.capabilities.timelion.save) {
return [
newSheetAction,
addSheetAction,
saveSheetAction,
deleteSheetAction,
openSheetAction,
optionsAction,
helpAction,
];
}
return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction];
}
let refresher;
const setRefreshData = function () {
if (refresher) $timeout.cancel(refresher);
const interval = timefilter.getRefreshInterval();
if (interval.value > 0 && !interval.pause) {
function startRefresh() {
refresher = $timeout(function () {
if (!$scope.running) $scope.search();
startRefresh();
}, interval.value);
}
startRefresh();
}
};
const init = function () {
$scope.running = false;
$scope.search();
setRefreshData();
$scope.model = {
timeRange: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
};
const unsubscribeStateUpdates = stateContainer.subscribe((state) => {
const clonedState = _.cloneDeep(state);
$scope.updatedSheets.forEach((updatedSheet) => {
clonedState.sheet[updatedSheet.id] = updatedSheet.expression;
});
$scope.state = clonedState;
$scope.opts.state = clonedState;
$scope.expression = _.clone($scope.state.sheet[$scope.state.selected]);
$scope.search();
});
timefilter.getFetch$().subscribe($scope.search);
$scope.opts = {
saveExpression: saveExpression,
saveSheet: saveSheet,
savedSheet: savedSheet,
state: _.cloneDeep(stateContainer.getState()),
search: $scope.search,
dontShowHelp: function () {
deps.core.uiSettings.set('timelion:showTutorial', false);
$scope.setPage(0);
$scope.closeMenus();
},
};
$scope.$watch('opts.state.rows', function (newRow) {
const state = stateContainer.getState();
if (state.rows !== newRow) {
stateContainer.transitions.set('rows', newRow);
}
});
$scope.$watch('opts.state.columns', function (newColumn) {
const state = stateContainer.getState();
if (state.columns !== newColumn) {
stateContainer.transitions.set('columns', newColumn);
}
});
$scope.menus = {
showHelp: false,
showSave: false,
showLoad: false,
showOptions: false,
};
$scope.toggleMenu = (menuName) => {
const curState = $scope.menus[menuName];
$scope.closeMenus();
$scope.menus[menuName] = !curState;
};
$scope.closeMenus = () => {
_.forOwn($scope.menus, function (value, key) {
$scope.menus[key] = false;
});
};
$scope.$on('$destroy', () => {
stopSyncingQueryServiceStateWithUrl();
unsubscribeStateUpdates();
stopStateSync();
});
};
$scope.onTimeUpdate = function ({ dateRange }) {
$scope.model.timeRange = {
...dateRange,
};
timefilter.setTime(dateRange);
if (!$scope.running) $scope.search();
};
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {
$scope.model.refreshInterval = {
pause: isPaused,
value: refreshInterval,
};
timefilter.setRefreshInterval({
pause: isPaused,
value: refreshInterval ? refreshInterval : $scope.refreshInterval.value,
});
setRefreshData();
};
$scope.$watch(
function () {
return savedSheet.lastSavedTitle;
},
function (newTitle) {
if (savedSheet.id && newTitle) {
deps.core.chrome.docTitle.change(newTitle);
}
}
);
$scope.$watch('expression', function (newExpression) {
const state = stateContainer.getState();
if (state.sheet[state.selected] !== newExpression) {
const updatedSheet = $scope.updatedSheets.find(
(updatedSheet) => updatedSheet.id === state.selected
);
if (updatedSheet) {
updatedSheet.expression = newExpression;
} else {
$scope.updatedSheets.push({
id: state.selected,
expression: newExpression,
});
}
}
});
$scope.toggle = function (property) {
$scope[property] = !$scope[property];
};
$scope.changeInterval = function (interval) {
$scope.currentInterval = interval;
};
$scope.updateChart = function () {
const state = stateContainer.getState();
const newSheet = _.clone(state.sheet);
if ($scope.updatedSheets.length) {
$scope.updatedSheets.forEach((updatedSheet) => {
newSheet[updatedSheet.id] = updatedSheet.expression;
});
$scope.updatedSheets = [];
}
stateContainer.transitions.updateState({
interval: $scope.currentInterval ? $scope.currentInterval : state.interval,
sheet: newSheet,
});
};
$scope.newSheet = function () {
history.push('/');
};
$scope.removeSheet = function (removedIndex) {
const state = stateContainer.getState();
const newSheet = state.sheet.filter((el, index) => index !== removedIndex);
$scope.updatedSheets = $scope.updatedSheets.filter((el) => el.id !== removedIndex);
stateContainer.transitions.updateState({
sheet: newSheet,
selected: removedIndex ? removedIndex - 1 : removedIndex,
});
};
$scope.newCell = function () {
const state = stateContainer.getState();
const newSheet = [...state.sheet, defaultExpression];
stateContainer.transitions.updateState({ sheet: newSheet, selected: newSheet.length - 1 });
};
$scope.setActiveCell = function (cell) {
const state = stateContainer.getState();
if (state.selected !== cell) {
stateContainer.transitions.updateState({ sheet: $scope.state.sheet, selected: cell });
}
};
$scope.search = function () {
$scope.running = true;
const state = stateContainer.getState();
// parse the time range client side to make sure it behaves like other charts
const timeRangeBounds = timefilter.getBounds();
const httpResult = $http
.post('../api/timelion/run', {
sheet: state.sheet,
time: _.assignIn(
{
from: timeRangeBounds.min,
to: timeRangeBounds.max,
},
{
interval: state.interval,
timezone: timezone,
}
),
})
.then((resp) => resp.data)
.catch((resp) => {
throw resp.data;
});
httpResult
.then(function (resp) {
$scope.stats = resp.stats;
$scope.sheet = resp.sheet;
_.forEach(resp.sheet, function (cell) {
if (cell.exception && cell.plot !== state.selected) {
stateContainer.transitions.set('selected', cell.plot);
}
});
$scope.running = false;
})
.catch(function (resp) {
$scope.sheet = [];
$scope.running = false;
const err = new Error(resp.message);
err.stack = resp.stack;
deps.core.notifications.toasts.addError(err, {
title: i18n.translate('timelion.searchErrorTitle', {
defaultMessage: 'Timelion request error',
}),
});
});
};
$scope.safeSearch = _.debounce($scope.search, 500);
function saveSheet() {
const state = stateContainer.getState();
savedSheet.timelion_sheet = state.sheet;
savedSheet.timelion_interval = state.interval;
savedSheet.timelion_columns = state.columns;
savedSheet.timelion_rows = state.rows;
savedSheet.save().then(function (id) {
if (id) {
deps.core.notifications.toasts.addSuccess({
title: i18n.translate('timelion.saveSheet.successNotificationText', {
defaultMessage: `Saved sheet '{title}'`,
values: { title: savedSheet.title },
}),
'data-test-subj': 'timelionSaveSuccessToast',
});
if (savedSheet.id !== $routeParams.id) {
history.push(`/${savedSheet.id}`);
}
}
});
}
async function saveExpression(title) {
const vis = await deps.plugins.visualizations.createVis('timelion', {
title,
params: {
expression: $scope.state.sheet[$scope.state.selected],
interval: $scope.state.interval,
},
});
const state = deps.plugins.visualizations.convertFromSerializedVis(vis.serialize());
const visSavedObject = await savedVisualizations.get();
Object.assign(visSavedObject, state);
const id = await visSavedObject.save();
if (id) {
deps.core.notifications.toasts.addSuccess(
i18n.translate('timelion.saveExpression.successNotificationText', {
defaultMessage: `Saved expression '{title}'`,
values: { title: state.title },
})
);
}
}
init();
}
app.config(function ($routeProvider) {
$routeProvider
.when('/:id?', {
template: rootTemplate,
reloadOnSearch: false,
k7Breadcrumbs: ($injector, $route) =>
$injector.invoke(
$route.current.params.id ? getSavedSheetBreadcrumbs : getCreateBreadcrumbs
),
badge: () => {
if (deps.core.application.capabilities.timelion.save) {
return undefined;
}
return {
text: i18n.translate('timelion.badge.readOnly.text', {
defaultMessage: 'Read only',
}),
tooltip: i18n.translate('timelion.badge.readOnly.tooltip', {
defaultMessage: 'Unable to save Timelion sheets',
}),
iconType: 'glasses',
};
},
resolve: {
savedSheet: function (savedSheets, $route) {
return savedSheets
.get($route.current.params.id)
.then((savedSheet) => {
if ($route.current.params.id) {
deps.core.chrome.recentlyAccessed.add(
savedSheet.getFullPath(),
savedSheet.title,
savedSheet.id
);
}
return savedSheet;
})
.catch();
},
},
})
.otherwise('/');
});
}

View file

@ -0,0 +1,153 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import './index.scss';
import { EuiIcon } from '@elastic/eui';
import angular, { IModule } from 'angular';
// required for `ngSanitize` angular module
import 'angular-sanitize';
// required for ngRoute
import 'angular-route';
import 'angular-sortable-view';
import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular';
import {
IUiSettingsClient,
CoreStart,
PluginInitializerContext,
AppMountParameters,
} from 'kibana/public';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import {
configureAppAngularModule,
createTopNavDirective,
createTopNavHelper,
} from '../../kibana_legacy/public';
import { TimelionPluginDependencies } from './plugin';
import { DataPublicPluginStart } from '../../data/public';
// @ts-ignore
import { initTimelionApp } from './app';
export interface RenderDeps {
pluginInitializerContext: PluginInitializerContext;
mountParams: AppMountParameters;
core: CoreStart;
plugins: TimelionPluginDependencies;
timelionPanels: Map<string, Panel>;
}
export interface TimelionVisualizationDependencies {
uiSettings: IUiSettingsClient;
timelionPanels: Map<string, Panel>;
data: DataPublicPluginStart;
$rootScope: any;
$compile: any;
}
let angularModuleInstance: IModule | null = null;
export const renderApp = (deps: RenderDeps) => {
if (!angularModuleInstance) {
angularModuleInstance = createLocalAngularModule(deps);
// global routing stuff
configureAppAngularModule(
angularModuleInstance,
{ core: deps.core, env: deps.pluginInitializerContext.env },
true
);
initTimelionApp(angularModuleInstance, deps);
}
const $injector = mountTimelionApp(deps.mountParams.appBasePath, deps.mountParams.element, deps);
return () => {
$injector.get('$rootScope').$destroy();
};
};
function registerPanels(dependencies: TimelionVisualizationDependencies) {
const timeChartPanel: Panel = getTimeChart(dependencies);
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
const mainTemplate = (basePath: string) => `<div ng-view class="timelionAppContainer">
<base href="${basePath}" />
</div>`;
const moduleName = 'app/timelion';
const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'angular-sortable-view'];
function mountTimelionApp(appBasePath: string, element: HTMLElement, deps: RenderDeps) {
const mountpoint = document.createElement('div');
mountpoint.setAttribute('class', 'timelionAppContainer');
// eslint-disable-next-line
mountpoint.innerHTML = mainTemplate(appBasePath);
// bootstrap angular into detached element and attach it later to
// make angular-within-angular possible
const $injector = angular.bootstrap(mountpoint, [moduleName]);
registerPanels({
uiSettings: deps.core.uiSettings,
timelionPanels: deps.timelionPanels,
data: deps.plugins.data,
$rootScope: $injector.get('$rootScope'),
$compile: $injector.get('$compile'),
});
element.appendChild(mountpoint);
return $injector;
}
function createLocalAngularModule(deps: RenderDeps) {
createLocalI18nModule();
createLocalIconModule();
createLocalTopNavModule(deps.plugins.navigation);
const dashboardAngularModule = angular.module(moduleName, [
...thirdPartyAngularDependencies,
'app/timelion/TopNav',
'app/timelion/I18n',
'app/timelion/icon',
]);
return dashboardAngularModule;
}
function createLocalIconModule() {
angular
.module('app/timelion/icon', ['react'])
.directive('icon', (reactDirective) => reactDirective(EuiIcon));
}
function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) {
angular
.module('app/timelion/TopNav', ['react'])
.directive('kbnTopNav', createTopNavDirective)
.directive('kbnTopNavHelper', createTopNavHelper(navigation.ui));
}
function createLocalI18nModule() {
angular
.module('app/timelion/I18n', [])
.provider('i18n', I18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective);
}

View file

@ -54,6 +54,6 @@ export function TimelionHelpTabs(props) {
}
TimelionHelpTabs.propTypes = {
activeTab: PropTypes.string.isRequired,
activateTab: PropTypes.func.isRequired,
activeTab: PropTypes.string,
activateTab: PropTypes.func,
};

View file

@ -17,25 +17,27 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import React from 'react';
import { TimelionHelpTabs } from './timelionhelp_tabs';
import 'angular-sortable-view';
import 'plugins/timelion/directives/chart/chart';
import 'plugins/timelion/directives/timelion_grid';
const app = uiModules.get('apps/timelion', ['angular-sortable-view']);
import html from './fullscreen.html';
app.directive('timelionFullscreen', function () {
return {
restrict: 'E',
scope: {
expression: '=',
series: '=',
state: '=',
transient: '=',
onSearch: '=',
},
template: html,
};
});
export function initTimelionTabsDirective(app, deps) {
app.directive('timelionHelpTabs', function (reactDirective) {
return reactDirective(
(props) => {
return (
<deps.core.i18n.Context>
<TimelionHelpTabs {...props} />
</deps.core.i18n.Context>
);
},
[['activeTab'], ['activateTab', { watchDepth: 'reference' }]],
{
restrict: 'E',
scope: {
activeTab: '=',
activateTab: '=',
},
}
);
});
}

View file

@ -17,31 +17,36 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { CoreSetup, Plugin } from 'kibana/public';
import { initTimelionLegacyModule } from './timelion_legacy_module';
import { Panel } from '../panels/panel';
import { move } from './collection';
import { initTimelionGridDirective } from '../timelion_grid';
/** @internal */
export interface LegacyDependenciesPluginSetup {
$rootScope: any;
$compile: any;
}
import html from './cells.html';
export class LegacyDependenciesPlugin
implements Plugin<Promise<LegacyDependenciesPluginSetup>, void> {
public async setup(core: CoreSetup, timelionPanels: Map<string, Panel>) {
initTimelionLegacyModule(timelionPanels);
const $injector = await chrome.dangerouslyGetActiveInjector();
export function initCellsDirective(app) {
initTimelionGridDirective(app);
app.directive('timelionCells', function () {
return {
$rootScope: $injector.get('$rootScope'),
$compile: $injector.get('$compile'),
} as LegacyDependenciesPluginSetup;
}
restrict: 'E',
scope: {
sheet: '=',
state: '=',
transient: '=',
onSearch: '=',
onSelect: '=',
onRemoveSheet: '=',
},
template: html,
link: function ($scope) {
$scope.removeCell = function (index) {
$scope.onRemoveSheet(index);
};
public start() {
// nothing to do here yet
}
$scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) {
move($scope.sheet, indexFrom, indexTo);
$scope.onSelect(indexTo);
};
},
};
});
}

View file

@ -0,0 +1,76 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
/**
* move an obj either up or down in the collection by
* injecting it either before/after the prev/next obj that
* satisfied the qualifier
*
* or, just from one index to another...
*
* @param {array} objs - the list to move the object within
* @param {number|any} obj - the object that should be moved, or the index that the object is currently at
* @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down
* @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck
* @return {array} - the objs argument
*/
export function move(
objs: any[],
obj: object | number,
below: number | boolean,
qualifier?: ((object: object, index: number) => any) | Record<string, any> | string
): object[] {
const origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
if (origI === -1) {
return objs;
}
if (_.isNumber(below)) {
// move to a specific index
objs.splice(below, 0, objs.splice(origI, 1)[0]);
return objs;
}
below = !!below;
qualifier = qualifier && _.iteratee(qualifier);
const above = !below;
const finder = below ? _.findIndex : _.findLastIndex;
// find the index of the next/previous obj that meets the qualifications
const targetI = finder(objs, (otherAgg, otherI) => {
if (below && otherI <= origI) {
return;
}
if (above && otherI >= origI) {
return;
}
return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI));
});
if (targetI === -1) {
return objs;
}
// place the obj at it's new index
objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
return objs;
}

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
export function initFixedElementDirective(app) {
app.directive('fixedElementRoot', function () {
return {
restrict: 'A',
link: function ($elem) {
let fixedAt;
$(window).bind('scroll', function () {
const fixed = $('[fixed-element]', $elem);
const body = $('[fixed-element-body]', $elem);
const top = fixed.offset().top;
if ($(window).scrollTop() > top) {
// This is a gross hack, but its better than it was. I guess
fixedAt = $(window).scrollTop();
fixed.addClass(fixed.attr('fixed-element'));
body.addClass(fixed.attr('fixed-element-body'));
body.css({ top: fixed.height() });
}
if ($(window).scrollTop() < fixedAt) {
fixed.removeClass(fixed.attr('fixed-element'));
body.removeClass(fixed.attr('fixed-element-body'));
body.removeAttr('style');
}
});
},
};
});
}

View file

@ -1,5 +1,5 @@
<div class="timCell col-md-12 col-sm-12 col-xs-12" timelion-grid timelion-grid-rows="1">
<div chart="series" class="timChart" search="onSearch" interval="state.interval"></div>
<div chart="series" class="timChart" search="onSearch" interval="state.interval"></div>
<div class="timCell__actions">
<button
class="timCell__action"

View file

@ -17,14 +17,20 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import saveTemplate from 'plugins/timelion/partials/save_sheet.html';
const app = uiModules.get('apps/timelion', []);
import html from './fullscreen.html';
app.directive('timelionSave', function () {
return {
replace: true,
restrict: 'E',
template: saveTemplate,
};
});
export function initFullscreenDirective(app) {
app.directive('timelionFullscreen', function () {
return {
restrict: 'E',
scope: {
expression: '=',
series: '=',
state: '=',
transient: '=',
onSearch: '=',
},
template: html,
};
});
}

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function initInputFocusDirective(app) {
app.directive('inputFocus', function ($parse, $timeout) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
const isDisabled = attrs.disableInputFocus && $parse(attrs.disableInputFocus)($scope);
if (!isDisabled) {
$timeout(function () {
$elem.focus();
if (attrs.inputFocus === 'select') $elem.select();
});
}
},
};
});
}

View file

@ -0,0 +1,314 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import rison from 'rison-node';
import savedObjectFinderTemplate from './saved_object_finder.html';
import { keyMap } from './key_map';
import {
PaginateControlsDirectiveProvider,
PaginateDirectiveProvider,
} from '../../../kibana_legacy/public';
import { PER_PAGE_SETTING } from '../../../saved_objects/public';
import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public';
export function initSavedObjectFinderDirective(app, savedSheetLoader, uiSettings) {
app
.directive('paginate', PaginateDirectiveProvider)
.directive('paginateControls', PaginateControlsDirectiveProvider)
.directive('savedObjectFinder', function () {
return {
restrict: 'E',
scope: {
type: '@',
// optional make-url attr, sets the userMakeUrl in our scope
userMakeUrl: '=?makeUrl',
// optional on-choose attr, sets the userOnChoose in our scope
userOnChoose: '=?onChoose',
// optional useLocalManagement attr, removes link to management section
useLocalManagement: '=?useLocalManagement',
/**
* @type {function} - an optional function. If supplied an `Add new X` button is shown
* and this function is called when clicked.
*/
onAddNew: '=',
/**
* @{type} boolean - set this to true, if you don't want the search box above the
* table to automatically gain focus once loaded
*/
disableAutoFocus: '=',
},
template: savedObjectFinderTemplate,
controllerAs: 'finder',
controller: function ($scope, $element, $location, history) {
const self = this;
// the text input element
const $input = $element.find('input[ng-model=filter]');
// The number of items to show in the list
$scope.perPage = uiSettings.get(PER_PAGE_SETTING);
// the list that will hold the suggestions
const $list = $element.find('ul');
// the current filter string, used to check that returned results are still useful
let currentFilter = $scope.filter;
// the most recently entered search/filter
let prevSearch;
// the list of hits, used to render display
self.hits = [];
self.service = savedSheetLoader;
self.properties = self.service.loaderProperties;
filterResults();
/**
* Boolean that keeps track of whether hits are sorted ascending (true)
* or descending (false) by title
* @type {Boolean}
*/
self.isAscending = true;
/**
* Sorts saved object finder hits either ascending or descending
* @param {Array} hits Array of saved finder object hits
* @return {Array} Array sorted either ascending or descending
*/
self.sortHits = function (hits) {
self.isAscending = !self.isAscending;
self.hits = self.isAscending
? _.sortBy(hits, ['title'])
: _.sortBy(hits, ['title']).reverse();
};
/**
* Passed the hit objects and will determine if the
* hit should have a url in the UI, returns it if so
* @return {string|null} - the url or nothing
*/
self.makeUrl = function (hit) {
if ($scope.userMakeUrl) {
return $scope.userMakeUrl(hit);
}
if (!$scope.userOnChoose) {
return hit.url;
}
return '#';
};
self.preventClick = function ($event) {
$event.preventDefault();
};
/**
* Called when a hit object is clicked, can override the
* url behavior if necessary.
*/
self.onChoose = function (hit, $event) {
if ($scope.userOnChoose) {
$scope.userOnChoose(hit, $event);
}
const url = self.makeUrl(hit);
if (!url || url === '#' || url.charAt(0) !== '#') return;
$event.preventDefault();
history.push(url.substr(1));
};
$scope.$watch('filter', function (newFilter) {
// ensure that the currentFilter changes from undefined to ''
// which triggers
currentFilter = newFilter || '';
filterResults();
});
$scope.pageFirstItem = 0;
$scope.pageLastItem = 0;
$scope.onPageChanged = (page) => {
$scope.pageFirstItem = page.firstItem;
$scope.pageLastItem = page.lastItem;
};
//manages the state of the keyboard selector
self.selector = {
enabled: false,
index: -1,
};
self.getLabel = function () {
return _.words(self.properties.nouns).map(_.capitalize).join(' ');
};
//key handler for the filter text box
self.filterKeyDown = function ($event) {
switch (keyMap[$event.keyCode]) {
case 'enter':
if (self.hitCount !== 1) return;
const hit = self.hits[0];
if (!hit) return;
self.onChoose(hit, $event);
$event.preventDefault();
break;
}
};
//key handler for the list items
self.hitKeyDown = function ($event, page, paginate) {
switch (keyMap[$event.keyCode]) {
case 'tab':
if (!self.selector.enabled) break;
self.selector.index = -1;
self.selector.enabled = false;
//if the user types shift-tab return to the textbox
//if the user types tab, set the focus to the currently selected hit.
if ($event.shiftKey) {
$input.focus();
} else {
$list.find('li.active a').focus();
}
$event.preventDefault();
break;
case 'down':
if (!self.selector.enabled) break;
if (self.selector.index + 1 < page.length) {
self.selector.index += 1;
}
$event.preventDefault();
break;
case 'up':
if (!self.selector.enabled) break;
if (self.selector.index > 0) {
self.selector.index -= 1;
}
$event.preventDefault();
break;
case 'right':
if (!self.selector.enabled) break;
if (page.number < page.count) {
paginate.goToPage(page.number + 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'left':
if (!self.selector.enabled) break;
if (page.number > 1) {
paginate.goToPage(page.number - 1);
self.selector.index = 0;
selectTopHit();
}
$event.preventDefault();
break;
case 'escape':
if (!self.selector.enabled) break;
$input.focus();
$event.preventDefault();
break;
case 'enter':
if (!self.selector.enabled) break;
const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index;
const hit = self.hits[hitIndex];
if (!hit) break;
self.onChoose(hit, $event);
$event.preventDefault();
break;
case 'shift':
break;
default:
$input.focus();
break;
}
};
self.hitBlur = function () {
self.selector.index = -1;
self.selector.enabled = false;
};
self.manageObjects = function (type) {
$location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type }));
};
self.hitCountNoun = function () {
return (self.hitCount === 1
? self.properties.noun
: self.properties.nouns
).toLowerCase();
};
function selectTopHit() {
setTimeout(function () {
//triggering a focus event kicks off a new angular digest cycle.
$list.find('a:first').focus();
}, 0);
}
function filterResults() {
if (!self.service) return;
if (!self.properties) return;
// track the filter that we use for this search,
// but ensure that we don't search for the same
// thing twice. This is called from multiple places
// and needs to be smart about when it actually searches
const filter = currentFilter;
if (prevSearch === filter) return;
prevSearch = filter;
const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING);
self.service.find(filter).then(function (hits) {
hits.hits = hits.hits.filter(
(hit) => isLabsEnabled || _.get(hit, 'type.stage') !== 'experimental'
);
hits.total = hits.hits.length;
// ensure that we don't display old results
// as we can't really cancel requests
if (currentFilter === filter) {
self.hitCount = hits.total;
self.hits = _.sortBy(hits.hits, ['title']);
}
});
}
},
};
});
}

View file

@ -17,16 +17,17 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import saveObjectSaveAsCheckboxTemplate from './saved_object_save_as_checkbox.html';
uiModules.get('kibana').directive('savedObjectSaveAsCheckBox', function () {
return {
restrict: 'E',
template: saveObjectSaveAsCheckboxTemplate,
replace: true,
scope: {
savedObject: '=',
},
};
});
export function initSavedObjectSaveAsCheckBoxDirective(app) {
app.directive('savedObjectSaveAsCheckBox', function () {
return {
restrict: 'E',
template: saveObjectSaveAsCheckboxTemplate,
replace: true,
scope: {
savedObject: '=',
},
};
});
}

View file

@ -0,0 +1,282 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Timelion Expression Autocompleter
*
* This directive allows users to enter multiline timelion expressions. If the user has entered
* a valid expression and then types a ".", this directive will display a list of suggestions.
*
* Users can navigate suggestions using the arrow keys. When a user selects a suggestion, it's
* inserted into the expression and the caret position is updated to be inside of the newly-
* added function's parentheses.
*
* Beneath the hood, we use a PEG grammar to validate the Timelion expression and detect if
* the caret is in a position within the expression that allows functions to be suggested.
*
* NOTE: This directive doesn't work well with contenteditable divs. Challenges include:
* - You have to replace markup with newline characters and spaces when passing the expression
* to the grammar.
* - You have to do the opposite when loading a saved expression, so that it appears correctly
* within the contenteditable (i.e. replace newlines with <br> markup).
* - The Range and Selection APIs ignore newlines when providing caret position, so there is
* literally no way to insert suggestions into the correct place in a multiline expression
* that has more than a single consecutive newline.
*/
import _ from 'lodash';
import $ from 'jquery';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../../vis_type_timelion/common/chain.peg';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,
Suggestions,
suggest,
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
const Parser = PEG.generate(grammar);
export function timelionExpInput(deps) {
return ($http, $timeout) => {
return {
restrict: 'E',
scope: {
rows: '=',
sheet: '=',
updateChart: '&',
shouldPopoverSuggestions: '@',
},
replace: true,
template: timelionExpressionInputTemplate,
link: function (scope, elem) {
const argValueSuggestions = deps.plugins.visTypeTimelion.getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};
scope.suggestions = new Suggestions();
function init() {
$http.get('../api/timelion/functions').then(function (resp) {
Object.assign(functionReference, {
byName: _.keyBy(resp.data, 'name'),
list: resp.data,
});
});
}
function setCaretOffset(caretOffset) {
// Wait for Angular to update the input with the new expression and *then* we can set
// the caret position.
$timeout(() => {
expressionInput.focus();
expressionInput[0].selectionStart = expressionInput[0].selectionEnd = caretOffset;
scope.$apply();
}, 0);
}
function insertSuggestionIntoExpression(suggestionIndex) {
if (scope.suggestions.isEmpty()) {
return;
}
const { min, max } = suggestibleFunctionLocation;
let insertedValue;
let insertPositionMinOffset = 0;
switch (scope.suggestions.type) {
case SUGGESTION_TYPE.FUNCTIONS: {
// Position the caret inside of the function parentheses.
insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`;
// min advanced one to not replace function '.'
insertPositionMinOffset = 1;
break;
}
case SUGGESTION_TYPE.ARGUMENTS: {
// Position the caret after the '='
insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`;
break;
}
case SUGGESTION_TYPE.ARGUMENT_VALUE: {
// Position the caret after the argument value
insertedValue = `${scope.suggestions.list[suggestionIndex].name}`;
break;
}
}
const updatedExpression = insertAtLocation(
insertedValue,
scope.sheet,
min + insertPositionMinOffset,
max
);
scope.sheet = updatedExpression;
const newCaretOffset = min + insertedValue.length;
setCaretOffset(newCaretOffset);
}
function scrollToSuggestionAt(index) {
// We don't cache these because the list changes based on user input.
const suggestionsList = $('[data-suggestions-list]');
const suggestionListItem = $('[data-suggestion-list-item]')[index];
// Scroll to the position of the item relative to the list, not to the window.
suggestionsList.scrollTop(suggestionListItem.offsetTop - suggestionsList[0].offsetTop);
}
function getCursorPosition() {
if (expressionInput.length) {
return expressionInput[0].selectionStart;
}
return null;
}
async function getSuggestions() {
const suggestions = await suggest(
scope.sheet,
functionReference.list,
Parser,
getCursorPosition(),
argValueSuggestions
);
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
scope.$apply(() => {
if (suggestions) {
scope.suggestions.setList(suggestions.list, suggestions.type);
scope.suggestions.show();
suggestibleFunctionLocation = suggestions.location;
$timeout(() => {
const suggestionsList = $('[data-suggestions-list]');
suggestionsList.scrollTop(0);
}, 0);
return;
}
suggestibleFunctionLocation = undefined;
scope.suggestions.reset();
});
}
function isNavigationalKey(keyCode) {
const keyCodes = _.values(comboBoxKeyCodes);
return keyCodes.includes(keyCode);
}
scope.onFocusInput = () => {
// Wait for the caret position of the input to update and then we can get suggestions
// (which depends on the caret position).
$timeout(getSuggestions, 0);
};
scope.onBlurInput = () => {
scope.suggestions.hide();
};
scope.onKeyDownInput = (e) => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation. The keyup handler will pull up suggestions.
if (!isNavigationalKey(e.keyCode)) {
return;
}
switch (e.keyCode) {
case comboBoxKeyCodes.UP:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepForward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.DOWN:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
scope.suggestions.stepBackward();
scrollToSuggestionAt(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.TAB:
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) {
// Before letting the tab be handled to focus the next element
// we need to hide the suggestions, otherwise it will focus these
// instead of the time interval select.
scope.suggestions.hide();
return;
}
// If we have suggestions, complete the selected one.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
break;
case comboBoxKeyCodes.ENTER:
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
scope.updateChart();
} else if (!scope.suggestions.isEmpty()) {
// If the suggestions are open, complete the expression with the suggestion.
e.preventDefault();
insertSuggestionIntoExpression(scope.suggestions.index);
}
break;
case comboBoxKeyCodes.ESCAPE:
e.preventDefault();
scope.suggestions.hide();
break;
}
};
scope.onKeyUpInput = (e) => {
// If the user isn't navigating, then we should update the suggestions based on their input.
if (!isNavigationalKey(e.keyCode)) {
getSuggestions();
}
};
scope.onClickExpression = () => {
getSuggestions();
};
scope.onClickSuggestion = (index) => {
insertSuggestionIntoExpression(index);
};
scope.getActiveSuggestionId = () => {
if (scope.suggestions.isVisible && scope.suggestions.index > -1) {
return `timelionSuggestion${scope.suggestions.index}`;
}
return '';
};
init();
},
};
};
}

View file

@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
export function initTimelionGridDirective(app) {
app.directive('timelionGrid', function () {
return {
restrict: 'A',
scope: {
timelionGridRows: '=',
timelionGridColumns: '=',
},
link: function ($scope, $elem) {
function init() {
setDimensions();
}
$scope.$on('$destroy', function () {
$(window).off('resize'); //remove the handler added earlier
});
$(window).resize(function () {
setDimensions();
});
$scope.$watchMulti(['timelionGridColumns', 'timelionGridRows'], function () {
setDimensions();
});
function setDimensions() {
const borderSize = 2;
const headerSize = 45 + 35 + 28 + 20 * 2; // chrome + subnav + buttons + (container padding)
const verticalPadding = 10;
if ($scope.timelionGridColumns != null) {
$elem.width($elem.parent().width() / $scope.timelionGridColumns - borderSize * 2);
}
if ($scope.timelionGridRows != null) {
$elem.height(
($(window).height() - headerSize) / $scope.timelionGridRows -
(verticalPadding + borderSize * 2)
);
}
}
init();
},
};
});
}

View file

@ -612,8 +612,8 @@
></h2>
<timelion-help-tabs
activateTab="activateTab"
activeTab="activeTab"
activate-tab="activateTab"
active-tab="activeTab"
>
</timelion-help-tabs>

View file

@ -0,0 +1,166 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import template from './timelion_help.html';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import moment from 'moment';
export function initTimelionHelpDirective(app) {
app.directive('timelionHelp', function ($http) {
return {
restrict: 'E',
template,
controller: function ($scope) {
$scope.functions = {
list: [],
details: null,
};
$scope.activeTab = 'funcref';
$scope.activateTab = function (tabName) {
$scope.activeTab = tabName;
};
function init() {
$scope.es = {
invalidCount: 0,
};
$scope.translations = {
nextButtonLabel: i18n.translate('timelion.help.nextPageButtonLabel', {
defaultMessage: 'Next',
}),
previousButtonLabel: i18n.translate('timelion.help.previousPageButtonLabel', {
defaultMessage: 'Previous',
}),
dontShowHelpButtonLabel: i18n.translate('timelion.help.dontShowHelpButtonLabel', {
defaultMessage: `Don't show this again`,
}),
strongNextText: i18n.translate('timelion.help.welcome.content.strongNextText', {
defaultMessage: 'Next',
}),
emphasizedEverythingText: i18n.translate(
'timelion.help.welcome.content.emphasizedEverythingText',
{
defaultMessage: 'everything',
}
),
notValidAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.notValid.advancedSettingsPathText',
{
defaultMessage: 'Management / Kibana / Advanced Settings',
}
),
validAdvancedSettingsPath: i18n.translate(
'timelion.help.configuration.valid.advancedSettingsPathText',
{
defaultMessage: 'Management/Kibana/Advanced Settings',
}
),
esAsteriskQueryDescription: i18n.translate(
'timelion.help.querying.esAsteriskQueryDescriptionText',
{
defaultMessage: 'hey Elasticsearch, find everything in my default index',
}
),
esIndexQueryDescription: i18n.translate(
'timelion.help.querying.esIndexQueryDescriptionText',
{
defaultMessage: 'use * as the q (query) for the logstash-* index',
}
),
strongAddText: i18n.translate('timelion.help.expressions.strongAddText', {
defaultMessage: 'Add',
}),
twoExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.twoExpressionsDescriptionTitle',
{
defaultMessage: 'Double the fun.',
}
),
customStylingDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.customStylingDescriptionTitle',
{
defaultMessage: 'Custom styling.',
}
),
namedArgumentsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.namedArgumentsDescriptionTitle',
{
defaultMessage: 'Named arguments.',
}
),
groupedExpressionsDescriptionTitle: i18n.translate(
'timelion.help.expressions.examples.groupedExpressionsDescriptionTitle',
{
defaultMessage: 'Grouped expressions.',
}
),
};
getFunctions();
checkElasticsearch();
}
function getFunctions() {
return $http.get('../api/timelion/functions').then(function (resp) {
$scope.functions.list = resp.data;
});
}
$scope.recheckElasticsearch = function () {
$scope.es.valid = null;
checkElasticsearch().then(function (valid) {
if (!valid) $scope.es.invalidCount++;
});
};
function checkElasticsearch() {
return $http.get('../api/timelion/validate/es').then(function (resp) {
if (resp.data.ok) {
$scope.es.valid = true;
$scope.es.stats = {
min: moment(resp.data.min).format('LLL'),
max: moment(resp.data.max).format('LLL'),
field: resp.data.field,
};
} else {
$scope.es.valid = false;
$scope.es.invalidReason = (function () {
try {
const esResp = JSON.parse(resp.data.resp.response);
return _.get(esResp, 'error.root_cause[0].reason');
} 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 i18n.translate('timelion.help.unknownErrorMessage', {
defaultMessage: 'Unknown error',
});
}
})();
}
return $scope.es.valid;
});
}
init();
},
};
});
}

View file

@ -27,6 +27,7 @@ export function TimelionInterval($timeout) {
scope: {
// The interval model
model: '=',
changeInterval: '=',
},
template,
link: function ($scope, $elem) {
@ -59,23 +60,23 @@ export function TimelionInterval($timeout) {
});
$scope.$watch('interval', function (newVal, oldVal) {
if (newVal === oldVal) return;
if (newVal === oldVal || $scope.model === newVal) return;
if (newVal === 'other') {
$scope.otherInterval = oldVal;
$scope.model = $scope.otherInterval;
$scope.changeInterval($scope.otherInterval);
$timeout(function () {
$('input', $elem).select();
}, 0);
} else {
$scope.otherInterval = $scope.interval;
$scope.model = $scope.interval;
$scope.changeInterval($scope.interval);
}
});
$scope.$watch('otherInterval', function (newVal, oldVal) {
if (newVal === oldVal) return;
$scope.model = newVal;
if (newVal === oldVal || $scope.model === newVal) return;
$scope.changeInterval(newVal);
});
},
};

View file

@ -17,4 +17,14 @@
* under the License.
*/
export * from './legacy_dependencies_plugin';
import template from '../partials/load_sheet.html';
export function initTimelionLoadSheetDirective(app) {
app.directive('timelionLoad', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
}

View file

@ -16,4 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import './saved_sheets';
import template from '../partials/sheet_options.html';
export function initTimelionOptionsSheetDirective(app) {
app.directive('timelionOptions', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
}

View file

@ -17,14 +17,14 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import template from 'plugins/timelion/partials/load_sheet.html';
const app = uiModules.get('apps/timelion', []);
import saveTemplate from '../partials/save_sheet.html';
app.directive('timelionLoad', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
export function initTimelionSaveSheetDirective(app) {
app.directive('timelionSave', function () {
return {
replace: true,
restrict: 'E',
template: saveTemplate,
};
});
}

View file

@ -17,14 +17,10 @@
* under the License.
*/
import { uiModules } from 'ui/modules';
import template from 'plugins/timelion/partials/sheet_options.html';
const app = uiModules.get('apps/timelion', []);
app.directive('timelionOptions', function () {
return {
replace: true,
restrict: 'E',
template,
};
});
import './webpackShims/jquery.flot';
import './webpackShims/jquery.flot.time';
import './webpackShims/jquery.flot.symbol';
import './webpackShims/jquery.flot.crosshair';
import './webpackShims/jquery.flot.selection';
import './webpackShims/jquery.flot.stack';
import './webpackShims/jquery.flot.axislabels';

View file

@ -1,4 +1,4 @@
<div class="timApp app-container" ng-controller="timelion">
<timelion-app class="timApp app-container">
<span class="kuiLocalTitle">
<span class="timApp__stats" ng-show="stats">
<span
@ -39,14 +39,14 @@
<!-- Search. -->
<form
role="form"
ng-submit="search()"
ng-submit="updateChart()"
class="kuiFieldGroup kuiFieldGroup--alignTop kuiVerticalRhythm"
>
<div class="kuiFieldGroupSection kuiFieldGroupSection--wide">
<timelion-expression-input
sheet="state.sheet[state.selected]"
sheet="expression"
rows="1"
update-chart="search()"
update-chart="updateChart()"
should-popover-suggestions="true"
></timelion-expression-input>
</div>
@ -55,6 +55,7 @@
<timelion-interval
class="kuiVerticalRhythmSmall"
model="state.interval"
change-interval="changeInterval"
></timelion-interval>
<button
@ -84,6 +85,7 @@
sheet="sheet"
on-search="search"
on-select="setActiveCell"
on-remove-sheet="removeSheet"
></timelion-cells>
</div>
</div>

View file

@ -17,31 +17,32 @@
* under the License.
*/
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import '../../../../../../plugins/vis_type_timelion/public/flot';
import '../../flot';
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment-timezone';
import { timefilter } from 'ui/timefilter';
// @ts-ignore
import observeResize from '../../lib/observe_resize';
import {
calculateInterval,
DEFAULT_TIME_FORMAT,
// @ts-ignore
} from '../../../../../../plugins/vis_type_timelion/common/lib';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { tickFormatters } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_formatters';
import { TimelionVisualizationDependencies } from '../../plugin';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { xaxisFormatterProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/xaxis_formatter';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { generateTicksProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/tick_generator';
tickFormatters,
xaxisFormatterProvider,
generateTicksProvider,
} from '../../../../vis_type_timelion/public';
import { TimelionVisualizationDependencies } from '../../application';
const DEBOUNCE_DELAY = 50;
export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const { $rootScope, $compile, uiSettings } = dependencies;
const {
$rootScope,
$compile,
uiSettings,
data: {
query: { timefilter },
},
} = dependencies;
return function () {
return {
@ -199,7 +200,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
});
$elem.on('plotselected', function (event: any, ranges: any) {
timefilter.setTime({
timefilter.timefilter.setTime({
from: moment(ranges.xaxis.from),
to: moment(ranges.xaxis.to),
});
@ -299,7 +300,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const options = _.cloneDeep(defaultOptions) as any;
// Get the X-axis tick format
const time = timefilter.getBounds() as any;
const time = timefilter.timefilter.getBounds() as any;
const interval = calculateInterval(
time.min.valueOf(),
time.max.valueOf(),

View file

@ -19,7 +19,7 @@
import { timechartFn } from './schema';
import { Panel } from '../panel';
import { TimelionVisualizationDependencies } from '../../plugin';
import { TimelionVisualizationDependencies } from '../../application';
export function getTimeChart(dependencies: TimelionVisualizationDependencies) {
// Schema is broken out so that it may be extended for use in other plugins

View file

@ -0,0 +1,134 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import {
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
DEFAULT_APP_CATEGORIES,
AppMountParameters,
AppUpdater,
ScopedHistory,
} from '../../../core/public';
import { Panel } from './panels/panel';
import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public';
import { createKbnUrlTracker } from '../../kibana_utils/public';
import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
import { VisualizationsStart } from '../../visualizations/public';
import { VisTypeTimelionPluginStart } from '../../vis_type_timelion/public';
export interface TimelionPluginDependencies {
data: DataPublicPluginStart;
navigation: NavigationPublicPluginStart;
visualizations: VisualizationsStart;
visTypeTimelion: VisTypeTimelionPluginStart;
}
/** @internal */
export class TimelionPlugin implements Plugin<void, void> {
initializerContext: PluginInitializerContext;
private appStateUpdater = new BehaviorSubject<AppUpdater>(() => ({}));
private stopUrlTracking: (() => void) | undefined = undefined;
private currentHistory: ScopedHistory | undefined = undefined;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, { data }: { data: DataPublicPluginSetup }) {
const timelionPanels: Map<string, Panel> = new Map();
const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({
baseUrl: core.http.basePath.prepend('/app/timelion'),
defaultSubUrl: '#/',
storageKey: `lastUrl:${core.http.basePath.get()}:timelion`,
navLinkUpdater$: this.appStateUpdater,
toastNotifications: core.notifications.toasts,
stateParams: [
{
kbnUrlKey: '_g',
stateUpdate$: data.query.state$.pipe(
filter(
({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval)
),
map(({ state }) => ({
...state,
filters: state.filters?.filter(esFilters.isFilterPinned),
}))
),
},
],
getHistory: () => this.currentHistory!,
});
this.stopUrlTracking = () => {
stopUrlTracker();
};
initAngularBootstrap();
core.application.register({
id: 'timelion',
title: 'Timelion',
order: 8000,
defaultPath: '#/',
euiIconType: 'timelionApp',
category: DEFAULT_APP_CATEGORIES.kibana,
updater$: this.appStateUpdater.asObservable(),
mount: async (params: AppMountParameters) => {
const [coreStart, pluginsStart] = await core.getStartServices();
this.currentHistory = params.history;
appMounted();
const unlistenParentHistory = params.history.listen(() => {
window.dispatchEvent(new HashChangeEvent('hashchange'));
});
const { renderApp } = await import('./application');
params.element.classList.add('timelionAppContainer');
const unmount = renderApp({
mountParams: params,
pluginInitializerContext: this.initializerContext,
timelionPanels,
core: coreStart,
plugins: pluginsStart as TimelionPluginDependencies,
});
return () => {
unlistenParentHistory();
unmount();
appUnMounted();
};
},
});
}
public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) {
kibanaLegacy.loadFontAwesome();
}
public stop(): void {
if (this.stopUrlTracking) {
this.stopUrlTracking();
}
}
}

View file

@ -18,10 +18,7 @@
*/
import { IUiSettingsClient } from 'kibana/public';
import {
createSavedObjectClass,
SavedObjectKibanaServices,
} from '../../../../../plugins/saved_objects/public';
import { createSavedObjectClass, SavedObjectKibanaServices } from '../../../saved_objects/public';
// Used only by the savedSheets service, usually no reason to change this
export function createSavedSheetClass(

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObjectLoader } from '../../../saved_objects/public';
import { createSavedSheetClass } from './_saved_sheet';
import { RenderDeps } from '../application';
export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) {
const savedObjectsClient = deps.core.savedObjects.client;
const services = {
savedObjectsClient,
indexPatterns: deps.plugins.data.indexPatterns,
search: deps.plugins.data.search,
chrome: deps.core.chrome,
overlays: deps.core.overlays,
};
const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings);
const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient, deps.core.chrome);
savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`;
// Customize loader properties since adding an 's' on type doesn't work for type 'timelion-sheet'.
savedSheetLoader.loaderProperties = {
name: 'timelion-sheet',
noun: 'Saved Sheets',
nouns: 'saved sheets',
};
// This is the only thing that gets injected into controllers
app.service('savedSheets', function () {
return savedSheetLoader;
});
return savedSheetLoader;
}

View file

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createStateContainer, syncState, IKbnUrlStateStorage } from '../../kibana_utils/public';
import { TimelionAppState, TimelionAppStateTransitions } from './types';
const STATE_STORAGE_KEY = '_a';
interface Arguments {
kbnUrlStateStorage: IKbnUrlStateStorage;
stateDefaults: TimelionAppState;
}
export function initTimelionAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) {
const urlState = kbnUrlStateStorage.get<TimelionAppState>(STATE_STORAGE_KEY);
const initialState = {
...stateDefaults,
...urlState,
};
/*
make sure url ('_a') matches initial state
Initializing appState does two things - first it translates the defaults into AppState,
second it updates appState based on the url (the url trumps the defaults). This means if
we update the state format at all and want to handle BWC, we must not only migrate the
data stored with saved vis, but also any old state in the url.
*/
kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true });
const stateContainer = createStateContainer<TimelionAppState, TimelionAppStateTransitions>(
initialState,
{
set: (state) => (prop, value) => ({ ...state, [prop]: value }),
updateState: (state) => (newValues) => ({ ...state, ...newValues }),
}
);
const { start: startStateSync, stop: stopStateSync } = syncState({
storageKey: STATE_STORAGE_KEY,
stateContainer: {
...stateContainer,
set: (state) => {
if (state) {
// syncState utils requires to handle incoming "null" value
stateContainer.set(state);
}
},
},
stateStorage: kbnUrlStateStorage,
});
// start syncing the appState with the ('_a') url
startStateSync();
return { stateContainer, stopStateSync };
}

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface TimelionAppState {
sheet: string[];
selected: number;
columns: number;
rows: number;
interval: string;
}
export interface TimelionAppStateTransitions {
set: (
state: TimelionAppState
) => <T extends keyof TimelionAppState>(prop: T, value: TimelionAppState[T]) => TimelionAppState;
updateState: (
state: TimelionAppState
) => <T extends keyof TimelionAppState>(newValues: Partial<TimelionAppState>) => TimelionAppState;
}

View file

@ -0,0 +1,462 @@
/*
Axis Labels Plugin for flot.
http://github.com/markrcote/flot-axislabels
Original code is Copyright (c) 2010 Xuan Luo.
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
(function ($) {
var options = {
axisLabels: {
show: true
}
};
function canvasSupported() {
return !!document.createElement('canvas').getContext;
}
function canvasTextSupported() {
if (!canvasSupported()) {
return false;
}
var dummy_canvas = document.createElement('canvas');
var context = dummy_canvas.getContext('2d');
return typeof context.fillText == 'function';
}
function css3TransitionSupported() {
var div = document.createElement('div');
return typeof div.style.MozTransition != 'undefined' // Gecko
|| typeof div.style.OTransition != 'undefined' // Opera
|| typeof div.style.webkitTransition != 'undefined' // WebKit
|| typeof div.style.transition != 'undefined';
}
function AxisLabel(axisName, position, padding, plot, opts) {
this.axisName = axisName;
this.position = position;
this.padding = padding;
this.plot = plot;
this.opts = opts;
this.width = 0;
this.height = 0;
}
AxisLabel.prototype.cleanup = function() {
};
CanvasAxisLabel.prototype = new AxisLabel();
CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
function CanvasAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position, padding,
plot, opts);
}
CanvasAxisLabel.prototype.calculateSize = function() {
if (!this.opts.axisLabelFontSizePixels)
this.opts.axisLabelFontSizePixels = 14;
if (!this.opts.axisLabelFontFamily)
this.opts.axisLabelFontFamily = 'sans-serif';
var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
if (this.position == 'left' || this.position == 'right') {
this.width = this.opts.axisLabelFontSizePixels + this.padding;
this.height = 0;
} else {
this.width = 0;
this.height = this.opts.axisLabelFontSizePixels + this.padding;
}
};
CanvasAxisLabel.prototype.draw = function(box) {
if (!this.opts.axisLabelColour)
this.opts.axisLabelColour = 'black';
var ctx = this.plot.getCanvas().getContext('2d');
ctx.save();
ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
this.opts.axisLabelFontFamily;
ctx.fillStyle = this.opts.axisLabelColour;
var width = ctx.measureText(this.opts.axisLabel).width;
var height = this.opts.axisLabelFontSizePixels;
var x, y, angle = 0;
if (this.position == 'top') {
x = box.left + box.width/2 - width/2;
y = box.top + height*0.72;
} else if (this.position == 'bottom') {
x = box.left + box.width/2 - width/2;
y = box.top + box.height - height*0.72;
} else if (this.position == 'left') {
x = box.left + height*0.72;
y = box.height/2 + box.top + width/2;
angle = -Math.PI/2;
} else if (this.position == 'right') {
x = box.left + box.width - height*0.72;
y = box.height/2 + box.top - width/2;
angle = Math.PI/2;
}
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(this.opts.axisLabel, 0, 0);
ctx.restore();
};
HtmlAxisLabel.prototype = new AxisLabel();
HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
function HtmlAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
this.elem = null;
}
HtmlAxisLabel.prototype.calculateSize = function() {
var elem = $('<div class="axisLabels" style="position:absolute;">' +
this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(elem);
// store height and width of label itself, for use in draw()
this.labelWidth = elem.outerWidth(true);
this.labelHeight = elem.outerHeight(true);
elem.remove();
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelWidth + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
HtmlAxisLabel.prototype.cleanup = function() {
if (this.elem) {
this.elem.remove();
}
};
HtmlAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
this.elem = $('<div id="' + this.axisName +
'Label" " class="axisLabels" style="position:absolute;">'
+ this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
if (this.position == 'top') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + 'px');
} else if (this.position == 'bottom') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + box.height - this.labelHeight +
'px');
} else if (this.position == 'left') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + 'px');
} else if (this.position == 'right') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + box.width - this.labelWidth +
'px');
}
};
CssTransformAxisLabel.prototype = new HtmlAxisLabel();
CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
}
CssTransformAxisLabel.prototype.calculateSize = function() {
HtmlAxisLabel.prototype.calculateSize.call(this);
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelHeight + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
var stransforms = {
'-moz-transform': '',
'-webkit-transform': '',
'-o-transform': '',
'-ms-transform': ''
};
if (x != 0 || y != 0) {
var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
stransforms['-moz-transform'] += stdTranslate;
stransforms['-webkit-transform'] += stdTranslate;
stransforms['-o-transform'] += stdTranslate;
stransforms['-ms-transform'] += stdTranslate;
}
if (degrees != 0) {
var rotation = degrees / 90;
var stdRotate = ' rotate(' + degrees + 'deg)';
stransforms['-moz-transform'] += stdRotate;
stransforms['-webkit-transform'] += stdRotate;
stransforms['-o-transform'] += stdRotate;
stransforms['-ms-transform'] += stdRotate;
}
var s = 'top: 0; left: 0; ';
for (var prop in stransforms) {
if (stransforms[prop]) {
s += prop + ':' + stransforms[prop] + ';';
}
}
s += ';';
return s;
};
CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = { x: 0, y: 0, degrees: 0 };
if (this.position == 'bottom') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top + box.height - this.labelHeight;
} else if (this.position == 'top') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top;
} else if (this.position == 'left') {
offsets.degrees = -90;
offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
offsets.y = box.height/2 + box.top;
} else if (this.position == 'right') {
offsets.degrees = 90;
offsets.x = box.left + box.width - this.labelWidth/2
- this.labelHeight/2;
offsets.y = box.height/2 + box.top;
}
offsets.x = Math.round(offsets.x);
offsets.y = Math.round(offsets.y);
return offsets;
};
CssTransformAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
var offsets = this.calculateOffsets(box);
this.elem = $('<div class="axisLabels ' + this.axisName +
'Label" style="position:absolute; ' +
this.transforms(offsets.degrees, offsets.x, offsets.y) +
'">' + this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
};
IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
CssTransformAxisLabel.prototype.constructor.call(this, axisName,
position, padding,
plot, opts);
this.requiresResize = false;
}
IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
// I didn't feel like learning the crazy Matrix stuff, so this uses
// a combination of the rotation transform and CSS positioning.
var s = '';
if (degrees != 0) {
var rotation = degrees/90;
while (rotation < 0) {
rotation += 4;
}
s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
// see below
this.requiresResize = (this.position == 'right');
}
if (x != 0) {
s += 'left: ' + x + 'px; ';
}
if (y != 0) {
s += 'top: ' + y + 'px; ';
}
return s;
};
IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
this, box);
// adjust some values to take into account differences between
// CSS and IE rotations.
if (this.position == 'top') {
// FIXME: not sure why, but placing this exactly at the top causes
// the top axis label to flip to the bottom...
offsets.y = box.top + 1;
} else if (this.position == 'left') {
offsets.x = box.left;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
} else if (this.position == 'right') {
offsets.x = box.left + box.width - this.labelHeight;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
}
return offsets;
};
IeTransformAxisLabel.prototype.draw = function(box) {
CssTransformAxisLabel.prototype.draw.call(this, box);
if (this.requiresResize) {
this.elem = this.plot.getPlaceholder().find("." + this.axisName +
"Label");
// Since we used CSS positioning instead of transforms for
// translating the element, and since the positioning is done
// before any rotations, we have to reset the width and height
// in case the browser wrapped the text (specifically for the
// y2axis).
this.elem.css('width', this.labelWidth);
this.elem.css('height', this.labelHeight);
}
};
function init(plot) {
plot.hooks.processOptions.push(function (plot, options) {
if (!options.axisLabels.show)
return;
// This is kind of a hack. There are no hooks in Flot between
// the creation and measuring of the ticks (setTicks, measureTickLabels
// in setupGrid() ) and the drawing of the ticks and plot box
// (insertAxisLabels in setupGrid() ).
//
// Therefore, we use a trick where we run the draw routine twice:
// the first time to get the tick measurements, so that we can change
// them, and then have it draw it again.
var secondPass = false;
var axisLabels = {};
var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
var defaultPadding = 2; // padding between axis and tick labels
plot.hooks.draw.push(function (plot, ctx) {
var hasAxisLabels = false;
if (!secondPass) {
// MEASURE AND SET OPTIONS
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
// Handle redraws initiated outside of this plug-in.
if (axisName in axisLabels) {
axis.labelHeight = axis.labelHeight -
axisLabels[axisName].height;
axis.labelWidth = axis.labelWidth -
axisLabels[axisName].width;
opts.labelHeight = axis.labelHeight;
opts.labelWidth = axis.labelWidth;
axisLabels[axisName].cleanup();
delete axisLabels[axisName];
}
if (!opts || !opts.axisLabel || !axis.show)
return;
hasAxisLabels = true;
var renderer = null;
if (!opts.axisLabelUseHtml &&
navigator.appName == 'Microsoft Internet Explorer') {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null) {
rv = parseFloat(RegExp.$1);
}
if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = CssTransformAxisLabel;
} else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = IeTransformAxisLabel;
} else if (opts.axisLabelUseCanvas) {
renderer = CanvasAxisLabel;
} else {
renderer = HtmlAxisLabel;
}
} else {
if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
renderer = HtmlAxisLabel;
} else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
renderer = CanvasAxisLabel;
} else {
renderer = CssTransformAxisLabel;
}
}
var padding = opts.axisLabelPadding === undefined ?
defaultPadding : opts.axisLabelPadding;
axisLabels[axisName] = new renderer(axisName,
axis.position, padding,
plot, opts);
// flot interprets axis.labelHeight and .labelWidth as
// the height and width of the tick labels. We increase
// these values to make room for the axis label and
// padding.
axisLabels[axisName].calculateSize();
// AxisLabel.height and .width are the size of the
// axis label and padding.
// Just set opts here because axis will be sorted out on
// the redraw.
opts.labelHeight = axis.labelHeight +
axisLabels[axisName].height;
opts.labelWidth = axis.labelWidth +
axisLabels[axisName].width;
});
// If there are axis labels, re-draw with new label widths and
// heights.
if (hasAxisLabels) {
secondPass = true;
plot.setupGrid();
plot.draw();
}
} else {
secondPass = false;
// DRAW
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
if (!opts || !opts.axisLabel || !axis.show)
return;
axisLabels[axisName].draw(axis.box);
});
}
});
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'axisLabels',
version: '2.0'
});
})(jQuery);

View file

@ -0,0 +1,176 @@
/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin supports these options:
crosshair: {
mode: null or "x" or "y" or "xy"
color: color
lineWidth: number
}
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
crosshair that lets you trace the values on the x axis, "y" enables a
horizontal crosshair and "xy" enables them both. "color" is the color of the
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
the drawn lines (default is 1).
The plugin also adds four public methods:
- setCrosshair( pos )
Set the position of the crosshair. Note that this is cleared if the user
moves the mouse. "pos" is in coordinates of the plot and should be on the
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
axes), which is coincidentally the same format as what you get from a
"plothover" event. If "pos" is null, the crosshair is cleared.
- clearCrosshair()
Clear the crosshair.
- lockCrosshair(pos)
Cause the crosshair to lock to the current location, no longer updating if
the user moves the mouse. Optionally supply a position (passed on to
setCrosshair()) to move it to.
Example usage:
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
$("#graph").bind( "plothover", function ( evt, position, item ) {
if ( item ) {
// Lock the crosshair to the data point being hovered
myFlot.lockCrosshair({
x: item.datapoint[ 0 ],
y: item.datapoint[ 1 ]
});
} else {
// Return normal crosshair operation
myFlot.unlockCrosshair();
}
});
- unlockCrosshair()
Free the crosshair to move again after locking it.
*/
(function ($) {
var options = {
crosshair: {
mode: null, // one of null, "x", "y" or "xy",
color: "rgba(170, 0, 0, 0.80)",
lineWidth: 1
}
};
function init(plot) {
// position of crosshair in pixels
var crosshair = { x: -1, y: -1, locked: false };
plot.setCrosshair = function setCrosshair(pos) {
if (!pos)
crosshair.x = -1;
else {
var o = plot.p2c(pos);
crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
}
plot.triggerRedrawOverlay();
};
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
plot.lockCrosshair = function lockCrosshair(pos) {
if (pos)
plot.setCrosshair(pos);
crosshair.locked = true;
};
plot.unlockCrosshair = function unlockCrosshair() {
crosshair.locked = false;
};
function onMouseOut(e) {
if (crosshair.locked)
return;
if (crosshair.x != -1) {
crosshair.x = -1;
plot.triggerRedrawOverlay();
}
}
function onMouseMove(e) {
if (crosshair.locked)
return;
if (plot.getSelection && plot.getSelection()) {
crosshair.x = -1; // hide the crosshair while selecting
return;
}
var offset = plot.offset();
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
plot.triggerRedrawOverlay();
}
plot.hooks.bindEvents.push(function (plot, eventHolder) {
if (!plot.getOptions().crosshair.mode)
return;
eventHolder.mouseout(onMouseOut);
eventHolder.mousemove(onMouseMove);
});
plot.hooks.drawOverlay.push(function (plot, ctx) {
var c = plot.getOptions().crosshair;
if (!c.mode)
return;
var plotOffset = plot.getPlotOffset();
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
if (crosshair.x != -1) {
var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
ctx.strokeStyle = c.color;
ctx.lineWidth = c.lineWidth;
ctx.lineJoin = "round";
ctx.beginPath();
if (c.mode.indexOf("x") != -1) {
var drawX = Math.floor(crosshair.x) + adj;
ctx.moveTo(drawX, 0);
ctx.lineTo(drawX, plot.height());
}
if (c.mode.indexOf("y") != -1) {
var drawY = Math.floor(crosshair.y) + adj;
ctx.moveTo(0, drawY);
ctx.lineTo(plot.width(), drawY);
}
ctx.stroke();
}
ctx.restore();
});
plot.hooks.shutdown.push(function (plot, eventHolder) {
eventHolder.unbind("mouseout", onMouseOut);
eventHolder.unbind("mousemove", onMouseMove);
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'crosshair',
version: '1.0'
});
})(jQuery);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,360 @@
/* Flot plugin for selecting regions of a plot.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin supports these options:
selection: {
mode: null or "x" or "y" or "xy",
color: color,
shape: "round" or "miter" or "bevel",
minSize: number of pixels
}
Selection support is enabled by setting the mode to one of "x", "y" or "xy".
In "x" mode, the user will only be able to specify the x range, similarly for
"y" mode. For "xy", the selection becomes a rectangle where both ranges can be
specified. "color" is color of the selection (if you need to change the color
later on, you can get to it with plot.getOptions().selection.color). "shape"
is the shape of the corners of the selection.
"minSize" is the minimum size a selection can be in pixels. This value can
be customized to determine the smallest size a selection can be and still
have the selection rectangle be displayed. When customizing this value, the
fact that it refers to pixels, not axis units must be taken into account.
Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
minute, setting "minSize" to 1 will not make the minimum selection size 1
minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
"plotunselected" events from being fired when the user clicks the mouse without
dragging.
When selection support is enabled, a "plotselected" event will be emitted on
the DOM element you passed into the plot function. The event handler gets a
parameter with the ranges selected on the axes, like this:
placeholder.bind( "plotselected", function( event, ranges ) {
alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
// similar for yaxis - with multiple axes, the extra ones are in
// x2axis, x3axis, ...
});
The "plotselected" event is only fired when the user has finished making the
selection. A "plotselecting" event is fired during the process with the same
parameters as the "plotselected" event, in case you want to know what's
happening while it's happening,
A "plotunselected" event with no arguments is emitted when the user clicks the
mouse to remove the selection. As stated above, setting "minSize" to 0 will
destroy this behavior.
The plugin also adds the following methods to the plot object:
- setSelection( ranges, preventEvent )
Set the selection rectangle. The passed in ranges is on the same form as
returned in the "plotselected" event. If the selection mode is "x", you
should put in either an xaxis range, if the mode is "y" you need to put in
an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
this:
setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
setSelection will trigger the "plotselected" event when called. If you don't
want that to happen, e.g. if you're inside a "plotselected" handler, pass
true as the second parameter. If you are using multiple axes, you can
specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
xaxis, the plugin picks the first one it sees.
- clearSelection( preventEvent )
Clear the selection rectangle. Pass in true to avoid getting a
"plotunselected" event.
- getSelection()
Returns the current selection in the same format as the "plotselected"
event. If there's currently no selection, the function returns null.
*/
(function ($) {
function init(plot) {
var selection = {
first: { x: -1, y: -1}, second: { x: -1, y: -1},
show: false,
active: false
};
// FIXME: The drag handling implemented here should be
// abstracted out, there's some similar code from a library in
// the navigation plugin, this should be massaged a bit to fit
// the Flot cases here better and reused. Doing this would
// make this plugin much slimmer.
var savedhandlers = {};
var mouseUpHandler = null;
function onMouseMove(e) {
if (selection.active) {
updateSelection(e);
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
}
}
function onMouseDown(e) {
if (e.which != 1) // only accept left-click
return;
// cancel out any text selections
document.body.focus();
// prevent text selection and drag in old-school browsers
if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
savedhandlers.onselectstart = document.onselectstart;
document.onselectstart = function () { return false; };
}
if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
savedhandlers.ondrag = document.ondrag;
document.ondrag = function () { return false; };
}
setSelectionPos(selection.first, e);
selection.active = true;
// this is a bit silly, but we have to use a closure to be
// able to whack the same handler again
mouseUpHandler = function (e) { onMouseUp(e); };
$(document).one("mouseup", mouseUpHandler);
}
function onMouseUp(e) {
mouseUpHandler = null;
// revert drag stuff for old-school browsers
if (document.onselectstart !== undefined)
document.onselectstart = savedhandlers.onselectstart;
if (document.ondrag !== undefined)
document.ondrag = savedhandlers.ondrag;
// no more dragging
selection.active = false;
updateSelection(e);
if (selectionIsSane())
triggerSelectedEvent();
else {
// this counts as a clear
plot.getPlaceholder().trigger("plotunselected", [ ]);
plot.getPlaceholder().trigger("plotselecting", [ null ]);
}
return false;
}
function getSelection() {
if (!selectionIsSane())
return null;
if (!selection.show) return null;
var r = {}, c1 = selection.first, c2 = selection.second;
$.each(plot.getAxes(), function (name, axis) {
if (axis.used) {
var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
}
});
return r;
}
function triggerSelectedEvent() {
var r = getSelection();
plot.getPlaceholder().trigger("plotselected", [ r ]);
// backwards-compat stuff, to be removed in future
if (r.xaxis && r.yaxis)
plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
}
function clamp(min, value, max) {
return value < min ? min: (value > max ? max: value);
}
function setSelectionPos(pos, e) {
var o = plot.getOptions();
var offset = plot.getPlaceholder().offset();
var plotOffset = plot.getPlotOffset();
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
if (o.selection.mode == "y")
pos.x = pos == selection.first ? 0 : plot.width();
if (o.selection.mode == "x")
pos.y = pos == selection.first ? 0 : plot.height();
}
function updateSelection(pos) {
if (pos.pageX == null)
return;
setSelectionPos(selection.second, pos);
if (selectionIsSane()) {
selection.show = true;
plot.triggerRedrawOverlay();
}
else
clearSelection(true);
}
function clearSelection(preventEvent) {
if (selection.show) {
selection.show = false;
plot.triggerRedrawOverlay();
if (!preventEvent)
plot.getPlaceholder().trigger("plotunselected", [ ]);
}
}
// function taken from markings support in Flot
function extractRange(ranges, coord) {
var axis, from, to, key, axes = plot.getAxes();
for (var k in axes) {
axis = axes[k];
if (axis.direction == coord) {
key = coord + axis.n + "axis";
if (!ranges[key] && axis.n == 1)
key = coord + "axis"; // support x1axis as xaxis
if (ranges[key]) {
from = ranges[key].from;
to = ranges[key].to;
break;
}
}
}
// backwards-compat stuff - to be removed in future
if (!ranges[key]) {
axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
from = ranges[coord + "1"];
to = ranges[coord + "2"];
}
// auto-reverse as an added bonus
if (from != null && to != null && from > to) {
var tmp = from;
from = to;
to = tmp;
}
return { from: from, to: to, axis: axis };
}
function setSelection(ranges, preventEvent) {
var axis, range, o = plot.getOptions();
if (o.selection.mode == "y") {
selection.first.x = 0;
selection.second.x = plot.width();
}
else {
range = extractRange(ranges, "x");
selection.first.x = range.axis.p2c(range.from);
selection.second.x = range.axis.p2c(range.to);
}
if (o.selection.mode == "x") {
selection.first.y = 0;
selection.second.y = plot.height();
}
else {
range = extractRange(ranges, "y");
selection.first.y = range.axis.p2c(range.from);
selection.second.y = range.axis.p2c(range.to);
}
selection.show = true;
plot.triggerRedrawOverlay();
if (!preventEvent && selectionIsSane())
triggerSelectedEvent();
}
function selectionIsSane() {
var minSize = plot.getOptions().selection.minSize;
return Math.abs(selection.second.x - selection.first.x) >= minSize &&
Math.abs(selection.second.y - selection.first.y) >= minSize;
}
plot.clearSelection = clearSelection;
plot.setSelection = setSelection;
plot.getSelection = getSelection;
plot.hooks.bindEvents.push(function(plot, eventHolder) {
var o = plot.getOptions();
if (o.selection.mode != null) {
eventHolder.mousemove(onMouseMove);
eventHolder.mousedown(onMouseDown);
}
});
plot.hooks.drawOverlay.push(function (plot, ctx) {
// draw selection
if (selection.show && selectionIsSane()) {
var plotOffset = plot.getPlotOffset();
var o = plot.getOptions();
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
var c = $.color.parse(o.selection.color);
ctx.strokeStyle = c.scale('a', 0.8).toString();
ctx.lineWidth = 1;
ctx.lineJoin = o.selection.shape;
ctx.fillStyle = c.scale('a', 0.4).toString();
var x = Math.min(selection.first.x, selection.second.x) + 0.5,
y = Math.min(selection.first.y, selection.second.y) + 0.5,
w = Math.abs(selection.second.x - selection.first.x) - 1,
h = Math.abs(selection.second.y - selection.first.y) - 1;
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.restore();
}
});
plot.hooks.shutdown.push(function (plot, eventHolder) {
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mousedown", onMouseDown);
if (mouseUpHandler)
$(document).unbind("mouseup", mouseUpHandler);
});
}
$.plot.plugins.push({
init: init,
options: {
selection: {
mode: null, // one of null, "x", "y" or "xy"
color: "#e8cfac",
shape: "round", // one of "round", "miter", or "bevel"
minSize: 5 // minimum number of pixels
}
},
name: 'selection',
version: '1.1'
});
})(jQuery);

View file

@ -0,0 +1,188 @@
/* Flot plugin for stacking data sets rather than overlaying them.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin assumes the data is sorted on x (or y if stacking horizontally).
For line charts, it is assumed that if a line has an undefined gap (from a
null point), then the line above it should have the same gap - insert zeros
instead of "null" if you want another behaviour. This also holds for the start
and end of the chart. Note that stacking a mix of positive and negative values
in most instances doesn't make sense (so it looks weird).
Two or more series are stacked when their "stack" attribute is set to the same
key (which can be any number or string or just "true"). To specify the default
stack, you can set the stack option like this:
series: {
stack: null/false, true, or a key (number/string)
}
You can also specify it for a single series, like this:
$.plot( $("#placeholder"), [{
data: [ ... ],
stack: true
}])
The stacking order is determined by the order of the data series in the array
(later series end up on top of the previous).
Internally, the plugin modifies the datapoints in each series, adding an
offset to the y value. For line series, extra data points are inserted through
interpolation. If there's a second y value, it's also adjusted (e.g for bar
charts or filled areas).
*/
(function ($) {
var options = {
series: { stack: null } // or number/string
};
function init(plot) {
function findMatchingSeries(s, allseries) {
var res = null;
for (var i = 0; i < allseries.length; ++i) {
if (s == allseries[i])
break;
if (allseries[i].stack == s.stack)
res = allseries[i];
}
return res;
}
function stackData(plot, s, datapoints) {
if (s.stack == null || s.stack === false)
return;
var other = findMatchingSeries(s, plot.getData());
if (!other)
return;
var ps = datapoints.pointsize,
points = datapoints.points,
otherps = other.datapoints.pointsize,
otherpoints = other.datapoints.points,
newpoints = [],
px, py, intery, qx, qy, bottom,
withlines = s.lines.show,
horizontal = s.bars.horizontal,
withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
withsteps = withlines && s.lines.steps,
fromgap = true,
keyOffset = horizontal ? 1 : 0,
accumulateOffset = horizontal ? 0 : 1,
i = 0, j = 0, l, m;
while (true) {
if (i >= points.length)
break;
l = newpoints.length;
if (points[i] == null) {
// copy gaps
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else if (j >= otherpoints.length) {
// for lines, we can't use the rest of the points
if (!withlines) {
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
}
i += ps;
}
else if (otherpoints[j] == null) {
// oops, got a gap
for (m = 0; m < ps; ++m)
newpoints.push(null);
fromgap = true;
j += otherps;
}
else {
// cases where we actually got two points
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
if (px == qx) {
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
newpoints[l + accumulateOffset] += qy;
bottom = qy;
i += ps;
j += otherps;
}
else if (px > qx) {
// we got past point below, might need to
// insert interpolated extra point
if (withlines && i > 0 && points[i - ps] != null) {
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
newpoints.push(qx);
newpoints.push(intery + qy);
for (m = 2; m < ps; ++m)
newpoints.push(points[i + m]);
bottom = qy;
}
j += otherps;
}
else { // px < qx
if (fromgap && withlines) {
// if we come from a gap, we just skip this point
i += ps;
continue;
}
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
// we might be able to interpolate a point below,
// this can give us a better y
if (withlines && j > 0 && otherpoints[j - otherps] != null)
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
newpoints[l + accumulateOffset] += bottom;
i += ps;
}
fromgap = false;
if (l != newpoints.length && withbottom)
newpoints[l + 2] += bottom;
}
// maintain the line steps invariant
if (withsteps && l != newpoints.length && l > 0
&& newpoints[l] != null
&& newpoints[l] != newpoints[l - ps]
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
for (m = 0; m < ps; ++m)
newpoints[l + ps + m] = newpoints[l + m];
newpoints[l + 1] = newpoints[l - ps + 1];
}
}
datapoints.points = newpoints;
}
plot.hooks.processDatapoints.push(stackData);
}
$.plot.plugins.push({
init: init,
options: options,
name: 'stack',
version: '1.2'
});
})(jQuery);

View file

@ -0,0 +1,71 @@
/* Flot plugin that adds some extra symbols for plotting points.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
The symbols are accessed as strings through the standard symbol options:
series: {
points: {
symbol: "square" // or "diamond", "triangle", "cross"
}
}
*/
(function ($) {
function processRawData(plot, series, datapoints) {
// we normalize the area of each symbol so it is approximately the
// same as a circle of the given radius
var handlers = {
square: function (ctx, x, y, radius, shadow) {
// pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
var size = radius * Math.sqrt(Math.PI) / 2;
ctx.rect(x - size, y - size, size + size, size + size);
},
diamond: function (ctx, x, y, radius, shadow) {
// pi * r^2 = 2s^2 => s = r * sqrt(pi/2)
var size = radius * Math.sqrt(Math.PI / 2);
ctx.moveTo(x - size, y);
ctx.lineTo(x, y - size);
ctx.lineTo(x + size, y);
ctx.lineTo(x, y + size);
ctx.lineTo(x - size, y);
},
triangle: function (ctx, x, y, radius, shadow) {
// pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3))
var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3));
var height = size * Math.sin(Math.PI / 3);
ctx.moveTo(x - size/2, y + height/2);
ctx.lineTo(x + size/2, y + height/2);
if (!shadow) {
ctx.lineTo(x, y - height/2);
ctx.lineTo(x - size/2, y + height/2);
}
},
cross: function (ctx, x, y, radius, shadow) {
// pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
var size = radius * Math.sqrt(Math.PI) / 2;
ctx.moveTo(x - size, y - size);
ctx.lineTo(x + size, y + size);
ctx.moveTo(x - size, y + size);
ctx.lineTo(x + size, y - size);
}
};
var s = series.points.symbol;
if (handlers[s])
series.points.symbol = handlers[s];
}
function init(plot) {
plot.hooks.processDatapoints.push(processRawData);
}
$.plot.plugins.push({
init: init,
name: 'symbols',
version: '1.0'
});
})(jQuery);

View file

@ -0,0 +1,432 @@
/* Pretty handling of time axes.
Copyright (c) 2007-2014 IOLA and Ole Laursen.
Licensed under the MIT license.
Set axis.mode to "time" to enable. See the section "Time series data" in
API.txt for details.
*/
(function($) {
var options = {
xaxis: {
timezone: null, // "browser" for local to the client or timezone for timezone-js
timeformat: null, // format string to use
twelveHourClock: false, // 12 or 24 time in time mode
monthNames: null // list of names of months
}
};
// round to nearby lower multiple of base
function floorInBase(n, base) {
return base * Math.floor(n / base);
}
// Returns a string with the date d formatted according to fmt.
// A subset of the Open Group's strftime format is supported.
function formatDate(d, fmt, monthNames, dayNames) {
if (typeof d.strftime == "function") {
return d.strftime(fmt);
}
var leftPad = function(n, pad) {
n = "" + n;
pad = "" + (pad == null ? "0" : pad);
return n.length == 1 ? pad + n : n;
};
var r = [];
var escape = false;
var hours = d.getHours();
var isAM = hours < 12;
if (monthNames == null) {
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
}
if (dayNames == null) {
dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
}
var hours12;
if (hours > 12) {
hours12 = hours - 12;
} else if (hours == 0) {
hours12 = 12;
} else {
hours12 = hours;
}
for (var i = 0; i < fmt.length; ++i) {
var c = fmt.charAt(i);
if (escape) {
switch (c) {
case 'a': c = "" + dayNames[d.getDay()]; break;
case 'b': c = "" + monthNames[d.getMonth()]; break;
case 'd': c = leftPad(d.getDate()); break;
case 'e': c = leftPad(d.getDate(), " "); break;
case 'h': // For back-compat with 0.7; remove in 1.0
case 'H': c = leftPad(hours); break;
case 'I': c = leftPad(hours12); break;
case 'l': c = leftPad(hours12, " "); break;
case 'm': c = leftPad(d.getMonth() + 1); break;
case 'M': c = leftPad(d.getMinutes()); break;
// quarters not in Open Group's strftime specification
case 'q':
c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
case 'S': c = leftPad(d.getSeconds()); break;
case 'y': c = leftPad(d.getFullYear() % 100); break;
case 'Y': c = "" + d.getFullYear(); break;
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
case 'w': c = "" + d.getDay(); break;
}
r.push(c);
escape = false;
} else {
if (c == "%") {
escape = true;
} else {
r.push(c);
}
}
}
return r.join("");
}
// To have a consistent view of time-based data independent of which time
// zone the client happens to be in we need a date-like object independent
// of time zones. This is done through a wrapper that only calls the UTC
// versions of the accessor methods.
function makeUtcWrapper(d) {
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
sourceObj[sourceMethod] = function() {
return targetObj[targetMethod].apply(targetObj, arguments);
};
};
var utc = {
date: d
};
// support strftime, if found
if (d.strftime != undefined) {
addProxyMethod(utc, "strftime", d, "strftime");
}
addProxyMethod(utc, "getTime", d, "getTime");
addProxyMethod(utc, "setTime", d, "setTime");
var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
for (var p = 0; p < props.length; p++) {
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
}
return utc;
};
// select time zone strategy. This returns a date-like object tied to the
// desired timezone
function dateGenerator(ts, opts) {
if (opts.timezone == "browser") {
return new Date(ts);
} else if (!opts.timezone || opts.timezone == "utc") {
return makeUtcWrapper(new Date(ts));
} else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
var d = new timezoneJS.Date();
// timezone-js is fickle, so be sure to set the time zone before
// setting the time.
d.setTimezone(opts.timezone);
d.setTime(ts);
return d;
} else {
return makeUtcWrapper(new Date(ts));
}
}
// map of app. size of time units in milliseconds
var timeUnitSize = {
"second": 1000,
"minute": 60 * 1000,
"hour": 60 * 60 * 1000,
"day": 24 * 60 * 60 * 1000,
"month": 30 * 24 * 60 * 60 * 1000,
"quarter": 3 * 30 * 24 * 60 * 60 * 1000,
"year": 365.2425 * 24 * 60 * 60 * 1000
};
// the allowed tick sizes, after 1 year we use
// an integer algorithm
var baseSpec = [
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
[30, "second"],
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
[30, "minute"],
[1, "hour"], [2, "hour"], [4, "hour"],
[8, "hour"], [12, "hour"],
[1, "day"], [2, "day"], [3, "day"],
[0.25, "month"], [0.5, "month"], [1, "month"],
[2, "month"]
];
// we don't know which variant(s) we'll need yet, but generating both is
// cheap
var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
[1, "year"]]);
var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
[1, "year"]]);
function init(plot) {
plot.hooks.processOptions.push(function (plot, options) {
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options;
if (opts.mode == "time") {
axis.tickGenerator = function(axis) {
var ticks = [];
var d = dateGenerator(axis.min, opts);
var minSize = 0;
// make quarter use a possibility if quarters are
// mentioned in either of these options
var spec = (opts.tickSize && opts.tickSize[1] ===
"quarter") ||
(opts.minTickSize && opts.minTickSize[1] ===
"quarter") ? specQuarters : specMonths;
if (opts.minTickSize != null) {
if (typeof opts.tickSize == "number") {
minSize = opts.tickSize;
} else {
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
}
}
for (var i = 0; i < spec.length - 1; ++i) {
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
break;
}
}
var size = spec[i][0];
var unit = spec[i][1];
// special-case the possibility of several years
if (unit == "year") {
// if given a minTickSize in years, just use it,
// ensuring that it's an integer
if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
size = Math.floor(opts.minTickSize[0]);
} else {
var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
var norm = (axis.delta / timeUnitSize.year) / magn;
if (norm < 1.5) {
size = 1;
} else if (norm < 3) {
size = 2;
} else if (norm < 7.5) {
size = 5;
} else {
size = 10;
}
size *= magn;
}
// minimum size for years is 1
if (size < 1) {
size = 1;
}
}
axis.tickSize = opts.tickSize || [size, unit];
var tickSize = axis.tickSize[0];
unit = axis.tickSize[1];
var step = tickSize * timeUnitSize[unit];
if (unit == "second") {
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
} else if (unit == "minute") {
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
} else if (unit == "hour") {
d.setHours(floorInBase(d.getHours(), tickSize));
} else if (unit == "month") {
d.setMonth(floorInBase(d.getMonth(), tickSize));
} else if (unit == "quarter") {
d.setMonth(3 * floorInBase(d.getMonth() / 3,
tickSize));
} else if (unit == "year") {
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
}
// reset smaller components
d.setMilliseconds(0);
if (step >= timeUnitSize.minute) {
d.setSeconds(0);
}
if (step >= timeUnitSize.hour) {
d.setMinutes(0);
}
if (step >= timeUnitSize.day) {
d.setHours(0);
}
if (step >= timeUnitSize.day * 4) {
d.setDate(1);
}
if (step >= timeUnitSize.month * 2) {
d.setMonth(floorInBase(d.getMonth(), 3));
}
if (step >= timeUnitSize.quarter * 2) {
d.setMonth(floorInBase(d.getMonth(), 6));
}
if (step >= timeUnitSize.year) {
d.setMonth(0);
}
var carry = 0;
var v = Number.NaN;
var prev;
do {
prev = v;
v = d.getTime();
ticks.push(v);
if (unit == "month" || unit == "quarter") {
if (tickSize < 1) {
// a bit complicated - we'll divide the
// month/quarter up but we need to take
// care of fractions so we don't end up in
// the middle of a day
d.setDate(1);
var start = d.getTime();
d.setMonth(d.getMonth() +
(unit == "quarter" ? 3 : 1));
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
carry = d.getHours();
d.setHours(0);
} else {
d.setMonth(d.getMonth() +
tickSize * (unit == "quarter" ? 3 : 1));
}
} else if (unit == "year") {
d.setFullYear(d.getFullYear() + tickSize);
} else {
d.setTime(v + step);
}
} while (v < axis.max && v != prev);
return ticks;
};
axis.tickFormatter = function (v, axis) {
var d = dateGenerator(v, axis.options);
// first check global format
if (opts.timeformat != null) {
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
}
// possibly use quarters if quarters are mentioned in
// any of these places
var useQuarters = (axis.options.tickSize &&
axis.options.tickSize[1] == "quarter") ||
(axis.options.minTickSize &&
axis.options.minTickSize[1] == "quarter");
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
var span = axis.max - axis.min;
var suffix = (opts.twelveHourClock) ? " %p" : "";
var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
var fmt;
if (t < timeUnitSize.minute) {
fmt = hourCode + ":%M:%S" + suffix;
} else if (t < timeUnitSize.day) {
if (span < 2 * timeUnitSize.day) {
fmt = hourCode + ":%M" + suffix;
} else {
fmt = "%b %d " + hourCode + ":%M" + suffix;
}
} else if (t < timeUnitSize.month) {
fmt = "%b %d";
} else if ((useQuarters && t < timeUnitSize.quarter) ||
(!useQuarters && t < timeUnitSize.year)) {
if (span < timeUnitSize.year) {
fmt = "%b";
} else {
fmt = "%b %Y";
}
} else if (useQuarters && t < timeUnitSize.year) {
if (span < timeUnitSize.year) {
fmt = "Q%q";
} else {
fmt = "Q%q %Y";
}
} else {
fmt = "%Y";
}
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
return rt;
};
}
});
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'time',
version: '1.0'
});
// Time-axis support used to be in Flot core, which exposed the
// formatDate function on the plot object. Various plugins depend
// on the function, so we need to re-expose it here.
$.plot.formatDate = formatDate;
$.plot.dateGenerator = dateGenerator;
})(jQuery);

View file

@ -17,14 +17,16 @@
* under the License.
*/
import 'ngreact';
import { schema, TypeOf } from '@kbn/config-schema';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/timelion', ['react']);
export const configSchema = {
schema: schema.object({
graphiteUrls: schema.maybe(schema.arrayOf(schema.string())),
enabled: schema.boolean({ defaultValue: true }),
ui: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
}),
};
import { TimelionHelpTabs } from './timelionhelp_tabs';
module.directive('timelionHelpTabs', function (reactDirective) {
return reactDirective(wrapInI18nContext(TimelionHelpTabs), undefined, { restrict: 'E' });
});
export type TimelionConfigType = TypeOf<typeof configSchema.schema>;

View file

@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server';
import { TimelionPlugin } from './plugin';
import { configSchema, TimelionConfigType } from './config';
export const plugin = (context: PluginInitializerContext) => new TimelionPlugin(context);
export const config: PluginConfigDescriptor<TimelionConfigType> = {
schema: configSchema.schema,
};
export const plugin = (context: PluginInitializerContext<TimelionConfigType>) =>
new TimelionPlugin(context);

View file

@ -16,12 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { TimelionConfigType } from './config';
export class TimelionPlugin implements Plugin {
constructor(context: PluginInitializerContext) {}
constructor(context: PluginInitializerContext<TimelionConfigType>) {}
setup(core: CoreSetup) {
public setup(core: CoreSetup) {
core.capabilities.registerProvider(() => ({
timelion: {
save: true,
},
}));
core.savedObjects.registerType({
name: 'timelion-sheet',
hidden: false,
@ -46,6 +55,42 @@ export class TimelionPlugin implements Plugin {
},
},
});
core.uiSettings.register({
'timelion:showTutorial': {
name: i18n.translate('timelion.uiSettings.showTutorialLabel', {
defaultMessage: 'Show tutorial',
}),
value: false,
description: i18n.translate('timelion.uiSettings.showTutorialDescription', {
defaultMessage: 'Should I show the tutorial by default when entering the timelion app?',
}),
category: ['timelion'],
schema: schema.boolean(),
},
'timelion:default_columns': {
name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', {
defaultMessage: 'Default columns',
}),
value: 2,
description: i18n.translate('timelion.uiSettings.defaultColumnsDescription', {
defaultMessage: 'Number of columns on a timelion sheet by default',
}),
category: ['timelion'],
schema: schema.number(),
},
'timelion:default_rows': {
name: i18n.translate('timelion.uiSettings.defaultRowsLabel', {
defaultMessage: 'Default rows',
}),
value: 2,
description: i18n.translate('timelion.uiSettings.defaultRowsDescription', {
defaultMessage: 'Number of rows on a timelion sheet by default',
}),
category: ['timelion'],
schema: schema.number(),
},
});
}
start() {}
stop() {}

View file

@ -25,5 +25,10 @@ export function plugin(initializerContext: PluginInitializerContext) {
}
export { getTimezone } from './helpers/get_timezone';
export { tickFormatters } from './helpers/tick_formatters';
export { xaxisFormatterProvider } from './helpers/xaxis_formatter';
export { generateTicksProvider } from './helpers/tick_generator';
export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib';
export { VisTypeTimelionPluginStart } from './plugin';

View file

@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import { first } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import { TypeOf, schema } from '@kbn/config-schema';
import { RecursiveReadonly } from '@kbn/utility-types';
import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server';
@ -31,6 +31,10 @@ import { validateEsRoute } from './routes/validate_es';
import { runRoute } from './routes/run';
import { ConfigManager } from './lib/config_manager';
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
defaultMessage: 'experimental',
});
/**
* Describes public Timelion plugin contract returned at the `setup` stage.
*/
@ -82,6 +86,97 @@ export class Plugin {
runRoute(router, deps);
validateEsRoute(router);
core.uiSettings.register({
'timelion:es.timefield': {
name: i18n.translate('timelion.uiSettings.timeFieldLabel', {
defaultMessage: 'Time field',
}),
value: '@timestamp',
description: i18n.translate('timelion.uiSettings.timeFieldDescription', {
defaultMessage: 'Default field containing a timestamp when using {esParam}',
values: { esParam: '.es()' },
}),
category: ['timelion'],
schema: schema.string(),
},
'timelion:es.default_index': {
name: i18n.translate('timelion.uiSettings.defaultIndexLabel', {
defaultMessage: 'Default index',
}),
value: '_all',
description: i18n.translate('timelion.uiSettings.defaultIndexDescription', {
defaultMessage: 'Default elasticsearch index to search with {esParam}',
values: { esParam: '.es()' },
}),
category: ['timelion'],
schema: schema.string(),
},
'timelion:target_buckets': {
name: i18n.translate('timelion.uiSettings.targetBucketsLabel', {
defaultMessage: 'Target buckets',
}),
value: 200,
description: i18n.translate('timelion.uiSettings.targetBucketsDescription', {
defaultMessage: 'The number of buckets to shoot for when using auto intervals',
}),
category: ['timelion'],
schema: schema.number(),
},
'timelion:max_buckets': {
name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', {
defaultMessage: 'Maximum buckets',
}),
value: 2000,
description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', {
defaultMessage: 'The maximum number of buckets a single datasource can return',
}),
category: ['timelion'],
schema: schema.number(),
},
'timelion:min_interval': {
name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', {
defaultMessage: 'Minimum interval',
}),
value: '1ms',
description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', {
defaultMessage: 'The smallest interval that will be calculated when using "auto"',
description:
'"auto" is a technical value in that context, that should not be translated.',
}),
category: ['timelion'],
schema: schema.string(),
},
'timelion:graphite.url': {
name: i18n.translate('timelion.uiSettings.graphiteURLLabel', {
defaultMessage: 'Graphite URL',
description:
'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
}),
value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null,
description: i18n.translate('timelion.uiSettings.graphiteURLDescription', {
defaultMessage:
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
type: 'select',
options: config.graphiteUrls || [],
category: ['timelion'],
schema: schema.nullable(schema.string()),
},
'timelion:quandl.key': {
name: i18n.translate('timelion.uiSettings.quandlKeyLabel', {
defaultMessage: 'Quandl key',
}),
value: 'someKeyHere',
description: i18n.translate('timelion.uiSettings.quandlKeyDescription', {
defaultMessage: '{experimentalLabel} Your API key from www.quandl.com',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
category: ['timelion'],
schema: schema.string(),
},
});
return deepFreeze({ uiEnabled: config.ui.enabled });
}

View file

@ -0,0 +1,121 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const keyMap: { [key: number]: string } = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
19: 'pause',
20: 'capsLock',
27: 'escape',
32: 'space',
33: 'pageUp',
34: 'pageDown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'insert',
46: 'delete',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
65: 'a',
66: 'b',
67: 'c',
68: 'd',
69: 'e',
70: 'f',
71: 'g',
72: 'h',
73: 'i',
74: 'j',
75: 'k',
76: 'l',
77: 'm',
78: 'n',
79: 'o',
80: 'p',
81: 'q',
82: 'r',
83: 's',
84: 't',
85: 'u',
86: 'v',
87: 'w',
88: 'x',
89: 'y',
90: 'z',
91: 'leftWindowKey',
92: 'rightWindowKey',
93: 'selectKey',
96: '0',
97: '1',
98: '2',
99: '3',
100: '4',
101: '5',
102: '6',
103: '7',
104: '8',
105: '9',
106: 'multiply',
107: 'add',
109: 'subtract',
110: 'period',
111: 'divide',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
144: 'numLock',
145: 'scrollLock',
186: 'semiColon',
187: 'equalSign',
188: 'comma',
189: 'dash',
190: 'period',
191: 'forwardSlash',
192: 'graveAccent',
219: 'openBracket',
220: 'backSlash',
221: 'closeBracket',
222: 'singleQuote',
224: 'meta',
};

View file

@ -20,7 +20,7 @@
import $ from 'jquery';
import _ from 'lodash';
import Bluebird from 'bluebird';
import { keyMap } from 'ui/directives/key_map';
import { keyMap } from './key_map';
const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1));
/**