mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
parent
b590bc2df9
commit
9b0c006e16
95 changed files with 7419 additions and 2331 deletions
|
@ -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/*
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"author": "Rashid Khan <rashid@elastic.co>",
|
||||
"name": "timelion",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 {}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
661
src/plugins/timelion/public/app.js
Normal file
661
src/plugins/timelion/public/app.js
Normal 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('/');
|
||||
});
|
||||
}
|
153
src/plugins/timelion/public/application.ts
Normal file
153
src/plugins/timelion/public/application.ts
Normal 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);
|
||||
}
|
|
@ -54,6 +54,6 @@ export function TimelionHelpTabs(props) {
|
|||
}
|
||||
|
||||
TimelionHelpTabs.propTypes = {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
activateTab: PropTypes.func.isRequired,
|
||||
activeTab: PropTypes.string,
|
||||
activateTab: PropTypes.func,
|
||||
};
|
|
@ -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: '=',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
76
src/plugins/timelion/public/directives/cells/collection.ts
Normal file
76
src/plugins/timelion/public/directives/cells/collection.ts
Normal 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;
|
||||
}
|
50
src/plugins/timelion/public/directives/fixed_element.js
Normal file
50
src/plugins/timelion/public/directives/fixed_element.js
Normal 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');
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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"
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
35
src/plugins/timelion/public/directives/input_focus.js
Normal file
35
src/plugins/timelion/public/directives/input_focus.js
Normal 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();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
314
src/plugins/timelion/public/directives/saved_object_finder.js
Normal file
314
src/plugins/timelion/public/directives/saved_object_finder.js
Normal 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']);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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: '=',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
68
src/plugins/timelion/public/directives/timelion_grid.js
Normal file
68
src/plugins/timelion/public/directives/timelion_grid.js
Normal 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();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -612,8 +612,8 @@
|
|||
></h2>
|
||||
|
||||
<timelion-help-tabs
|
||||
activateTab="activateTab"
|
||||
activeTab="activeTab"
|
||||
activate-tab="activateTab"
|
||||
active-tab="activeTab"
|
||||
>
|
||||
</timelion-help-tabs>
|
||||
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
|
@ -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(),
|
|
@ -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
|
134
src/plugins/timelion/public/plugin.ts
Normal file
134
src/plugins/timelion/public/plugin.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
50
src/plugins/timelion/public/services/saved_sheets.ts
Normal file
50
src/plugins/timelion/public/services/saved_sheets.ts
Normal 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;
|
||||
}
|
73
src/plugins/timelion/public/timelion_app_state.ts
Normal file
73
src/plugins/timelion/public/timelion_app_state.ts
Normal 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 };
|
||||
}
|
35
src/plugins/timelion/public/types.ts
Normal file
35
src/plugins/timelion/public/types.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
3168
src/plugins/timelion/public/webpackShims/jquery.flot.js
Normal file
3168
src/plugins/timelion/public/webpackShims/jquery.flot.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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);
|
188
src/plugins/timelion/public/webpackShims/jquery.flot.stack.js
Normal file
188
src/plugins/timelion/public/webpackShims/jquery.flot.stack.js
Normal 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);
|
|
@ -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);
|
432
src/plugins/timelion/public/webpackShims/jquery.flot.time.js
Normal file
432
src/plugins/timelion/public/webpackShims/jquery.flot.time.js
Normal 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);
|
|
@ -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>;
|
|
@ -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);
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
121
src/test_utils/public/key_map.ts
Normal file
121
src/test_utils/public/key_map.ts
Normal 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',
|
||||
};
|
|
@ -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));
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue