diff --git a/.eslintignore b/.eslintignore
index 4b5e781c2697..d983c4bedfaa 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -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/*
diff --git a/.i18nrc.json b/.i18nrc.json
index 9af7f17067b8..e8431fdb3f0e 100644
--- a/.i18nrc.json
+++ b/.i18nrc.json
@@ -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",
diff --git a/.sass-lint.yml b/.sass-lint.yml
index 055929006259..19d9b10f909b 100644
--- a/.sass-lint.yml
+++ b/.sass-lint.yml
@@ -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'
diff --git a/src/legacy/core_plugins/timelion/index.ts b/src/legacy/core_plugins/timelion/index.ts
deleted file mode 100644
index 9c8ab156d1a7..000000000000
--- a/src/legacy/core_plugins/timelion/index.ts
+++ /dev/null
@@ -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 URL of your graphite host',
- values: { experimentalLabel: `[${experimentalLabel}]` },
- }),
- 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: `[${experimentalLabel}]` },
- }),
- category: ['timelion'],
- },
- },
- },
- });
-
-// eslint-disable-next-line import/no-default-export
-export default timelionPluginInitializer;
diff --git a/src/legacy/core_plugins/timelion/package.json b/src/legacy/core_plugins/timelion/package.json
deleted file mode 100644
index 8b138e3b76d1..000000000000
--- a/src/legacy/core_plugins/timelion/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "author": "Rashid Khan ",
- "name": "timelion",
- "version": "kibana"
-}
diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js
deleted file mode 100644
index e6bbceb1ae58..000000000000
--- a/src/legacy/core_plugins/timelion/public/app.js
+++ /dev/null
@@ -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();
-});
diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/cells.js b/src/legacy/core_plugins/timelion/public/directives/cells/cells.js
deleted file mode 100644
index a9121c13b159..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/cells/cells.js
+++ /dev/null
@@ -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);
- };
- },
- };
-});
diff --git a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js b/src/legacy/core_plugins/timelion/public/directives/fixed_element.js
deleted file mode 100644
index 0e18240fc690..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js
+++ /dev/null
@@ -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');
- }
- });
- },
- };
-});
diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
deleted file mode 100644
index ae042310fd46..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
+++ /dev/null
@@ -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');
- }
- });
- }
- },
- };
- });
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
deleted file mode 100644
index 8b4c28a50b73..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ /dev/null
@@ -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 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();
- },
- };
-}
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js b/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js
deleted file mode 100644
index a1b920f30e80..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js
+++ /dev/null
@@ -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();
- },
- };
-});
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js b/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js
deleted file mode 100644
index 25f3df13153b..000000000000
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js
+++ /dev/null
@@ -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();
- },
- };
-});
diff --git a/src/legacy/core_plugins/timelion/public/header.svg b/src/legacy/core_plugins/timelion/public/header.svg
deleted file mode 100644
index 56f2f0dc51a6..000000000000
--- a/src/legacy/core_plugins/timelion/public/header.svg
+++ /dev/null
@@ -1,227 +0,0 @@
-
-
diff --git a/src/legacy/core_plugins/timelion/public/icon.svg b/src/legacy/core_plugins/timelion/public/icon.svg
deleted file mode 100644
index ba9a704b3ade..000000000000
--- a/src/legacy/core_plugins/timelion/public/icon.svg
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
diff --git a/src/legacy/core_plugins/timelion/public/legacy.ts b/src/legacy/core_plugins/timelion/public/legacy.ts
deleted file mode 100644
index 7980291e2d46..000000000000
--- a/src/legacy/core_plugins/timelion/public/legacy.ts
+++ /dev/null
@@ -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 = {
- // 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);
diff --git a/src/legacy/core_plugins/timelion/public/logo.png b/src/legacy/core_plugins/timelion/public/logo.png
deleted file mode 100644
index 7a62253697a0..000000000000
Binary files a/src/legacy/core_plugins/timelion/public/logo.png and /dev/null differ
diff --git a/src/legacy/core_plugins/timelion/public/plugin.ts b/src/legacy/core_plugins/timelion/public/plugin.ts
deleted file mode 100644
index 1f837303a2b3..000000000000
--- a/src/legacy/core_plugins/timelion/public/plugin.ts
+++ /dev/null
@@ -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;
-}
-
-/** @internal */
-export interface TimelionPluginSetupDependencies {
- // Temporary solution
- __LEGACY: LegacyDependenciesPlugin;
-}
-
-/** @internal */
-export class TimelionPlugin implements Plugin, void> {
- initializerContext: PluginInitializerContext;
-
- constructor(initializerContext: PluginInitializerContext) {
- this.initializerContext = initializerContext;
- }
-
- public async setup(core: CoreSetup, { __LEGACY }: TimelionPluginSetupDependencies) {
- const timelionPanels: Map = 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 {}
-}
diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts b/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts
deleted file mode 100644
index 1fb29de83d3d..000000000000
--- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.ts
+++ /dev/null
@@ -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);
diff --git a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts b/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts
deleted file mode 100644
index 8122259f1c99..000000000000
--- a/src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts
+++ /dev/null
@@ -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): 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);
-});
diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js
index cde123e6c1d8..b6c705e81450 100644
--- a/src/legacy/ui/public/state_management/__tests__/state.js
+++ b/src/legacy/ui/public/state_management/__tests__/state.js
@@ -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;
diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts
index 4f7a4ff7f196..9140de316605 100644
--- a/src/plugins/saved_objects/public/index.ts
+++ b/src/plugins/saved_objects/public/index.ts
@@ -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();
diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json
index 55e492e8f23c..d8c709d867a3 100644
--- a/src/plugins/timelion/kibana.json
+++ b/src/plugins/timelion/kibana.json
@@ -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"
+ ]
}
diff --git a/src/legacy/core_plugins/timelion/public/_app.scss b/src/plugins/timelion/public/_app.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/_app.scss
rename to src/plugins/timelion/public/_app.scss
diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js
new file mode 100644
index 000000000000..0294e71084f9
--- /dev/null
+++ b/src/plugins/timelion/public/app.js
@@ -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('/');
+ });
+}
diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts
new file mode 100644
index 000000000000..a398106d56f5
--- /dev/null
+++ b/src/plugins/timelion/public/application.ts
@@ -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;
+}
+
+export interface TimelionVisualizationDependencies {
+ uiSettings: IUiSettingsClient;
+ timelionPanels: Map;
+ 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) => `
+
+
`;
+
+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);
+}
diff --git a/src/legacy/core_plugins/timelion/public/breadcrumbs.js b/src/plugins/timelion/public/breadcrumbs.js
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/breadcrumbs.js
rename to src/plugins/timelion/public/breadcrumbs.js
diff --git a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js b/src/plugins/timelion/public/components/timelionhelp_tabs.js
similarity index 95%
rename from src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js
rename to src/plugins/timelion/public/components/timelionhelp_tabs.js
index 639bd7d65a19..7939afce412e 100644
--- a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs.js
+++ b/src/plugins/timelion/public/components/timelionhelp_tabs.js
@@ -54,6 +54,6 @@ export function TimelionHelpTabs(props) {
}
TimelionHelpTabs.propTypes = {
- activeTab: PropTypes.string.isRequired,
- activateTab: PropTypes.func.isRequired,
+ activeTab: PropTypes.string,
+ activateTab: PropTypes.func,
};
diff --git a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js b/src/plugins/timelion/public/components/timelionhelp_tabs_directive.js
similarity index 56%
rename from src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js
rename to src/plugins/timelion/public/components/timelionhelp_tabs_directive.js
index db234043bbf1..67e0d595314f 100644
--- a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js
+++ b/src/plugins/timelion/public/components/timelionhelp_tabs_directive.js
@@ -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 (
+
+
+
+ );
+ },
+ [['activeTab'], ['activateTab', { watchDepth: 'reference' }]],
+ {
+ restrict: 'E',
+ scope: {
+ activeTab: '=',
+ activateTab: '=',
+ },
+ }
+ );
+ });
+}
diff --git a/src/legacy/core_plugins/timelion/public/directives/_index.scss b/src/plugins/timelion/public/directives/_index.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/_index.scss
rename to src/plugins/timelion/public/directives/_index.scss
diff --git a/src/legacy/core_plugins/timelion/public/directives/_timelion_expression_input.scss b/src/plugins/timelion/public/directives/_timelion_expression_input.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/_timelion_expression_input.scss
rename to src/plugins/timelion/public/directives/_timelion_expression_input.scss
diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/_cells.scss b/src/plugins/timelion/public/directives/cells/_cells.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/cells/_cells.scss
rename to src/plugins/timelion/public/directives/cells/_cells.scss
diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/_index.scss b/src/plugins/timelion/public/directives/cells/_index.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/cells/_index.scss
rename to src/plugins/timelion/public/directives/cells/_index.scss
diff --git a/src/legacy/core_plugins/timelion/public/directives/cells/cells.html b/src/plugins/timelion/public/directives/cells/cells.html
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/cells/cells.html
rename to src/plugins/timelion/public/directives/cells/cells.html
diff --git a/src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts b/src/plugins/timelion/public/directives/cells/cells.js
similarity index 50%
rename from src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts
rename to src/plugins/timelion/public/directives/cells/cells.js
index f6c329d417f2..36a1e80dd470 100644
--- a/src/legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts
+++ b/src/plugins/timelion/public/directives/cells/cells.js
@@ -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;
-}
-
-export class LegacyDependenciesPlugin
- implements Plugin, void> {
- public async setup(core: CoreSetup, timelionPanels: Map) {
- initTimelionLegacyModule(timelionPanels);
+import html from './cells.html';
- 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);
+ };
+ },
+ };
+ });
}
diff --git a/src/plugins/timelion/public/directives/cells/collection.ts b/src/plugins/timelion/public/directives/cells/collection.ts
new file mode 100644
index 000000000000..b882a2bbe6e5
--- /dev/null
+++ b/src/plugins/timelion/public/directives/cells/collection.ts
@@ -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
+): 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;
+}
diff --git a/src/legacy/core_plugins/timelion/public/directives/chart/chart.js b/src/plugins/timelion/public/directives/chart/chart.js
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/directives/chart/chart.js
rename to src/plugins/timelion/public/directives/chart/chart.js
diff --git a/src/plugins/timelion/public/directives/fixed_element.js b/src/plugins/timelion/public/directives/fixed_element.js
new file mode 100644
index 000000000000..f57c391e7fcd
--- /dev/null
+++ b/src/plugins/timelion/public/directives/fixed_element.js
@@ -0,0 +1,50 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+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');
+ }
+ });
+ },
+ };
+ });
+}
diff --git a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html
similarity index 85%
rename from src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html
rename to src/plugins/timelion/public/directives/fullscreen/fullscreen.html
index 325c7eabb2b0..194596ba79d0 100644
--- a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.html
+++ b/src/plugins/timelion/public/directives/fullscreen/fullscreen.html
@@ -1,5 +1,5 @@
-
+
diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/index.scss
rename to src/plugins/timelion/public/index.scss
diff --git a/src/legacy/core_plugins/timelion/public/index.ts b/src/plugins/timelion/public/index.ts
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/index.ts
rename to src/plugins/timelion/public/index.ts
diff --git a/src/legacy/core_plugins/timelion/public/lib/observe_resize.js b/src/plugins/timelion/public/lib/observe_resize.js
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/lib/observe_resize.js
rename to src/plugins/timelion/public/lib/observe_resize.js
diff --git a/src/legacy/core_plugins/timelion/public/panels/panel.ts b/src/plugins/timelion/public/panels/panel.ts
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/panels/panel.ts
rename to src/plugins/timelion/public/panels/panel.ts
diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts
similarity index 93%
rename from src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
rename to src/plugins/timelion/public/panels/timechart/schema.ts
index 087e16692532..b56d8a66110c 100644
--- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
+++ b/src/plugins/timelion/public/panels/timechart/schema.ts
@@ -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(),
diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts b/src/plugins/timelion/public/panels/timechart/timechart.ts
similarity index 94%
rename from src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts
rename to src/plugins/timelion/public/panels/timechart/timechart.ts
index 4173bfeb331e..525a994e3121 100644
--- a/src/legacy/core_plugins/timelion/public/panels/timechart/timechart.ts
+++ b/src/plugins/timelion/public/panels/timechart/timechart.ts
@@ -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
diff --git a/src/legacy/core_plugins/timelion/public/partials/load_sheet.html b/src/plugins/timelion/public/partials/load_sheet.html
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/partials/load_sheet.html
rename to src/plugins/timelion/public/partials/load_sheet.html
diff --git a/src/legacy/core_plugins/timelion/public/partials/save_sheet.html b/src/plugins/timelion/public/partials/save_sheet.html
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/partials/save_sheet.html
rename to src/plugins/timelion/public/partials/save_sheet.html
diff --git a/src/legacy/core_plugins/timelion/public/partials/sheet_options.html b/src/plugins/timelion/public/partials/sheet_options.html
similarity index 100%
rename from src/legacy/core_plugins/timelion/public/partials/sheet_options.html
rename to src/plugins/timelion/public/partials/sheet_options.html
diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts
new file mode 100644
index 000000000000..a92ced20cb6d
--- /dev/null
+++ b/src/plugins/timelion/public/plugin.ts
@@ -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 {
+ initializerContext: PluginInitializerContext;
+ private appStateUpdater = new BehaviorSubject(() => ({}));
+ 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 = 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();
+ }
+ }
+}
diff --git a/src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts
similarity index 95%
rename from src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts
rename to src/plugins/timelion/public/services/_saved_sheet.ts
index 4e5aa8d445e7..0958cce86012 100644
--- a/src/legacy/core_plugins/timelion/public/services/_saved_sheet.ts
+++ b/src/plugins/timelion/public/services/_saved_sheet.ts
@@ -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(
diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts
new file mode 100644
index 000000000000..a3e7f66d9ee4
--- /dev/null
+++ b/src/plugins/timelion/public/services/saved_sheets.ts
@@ -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;
+}
diff --git a/src/plugins/timelion/public/timelion_app_state.ts b/src/plugins/timelion/public/timelion_app_state.ts
new file mode 100644
index 000000000000..43382adbf8f8
--- /dev/null
+++ b/src/plugins/timelion/public/timelion_app_state.ts
@@ -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(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(
+ 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 };
+}
diff --git a/src/plugins/timelion/public/types.ts b/src/plugins/timelion/public/types.ts
new file mode 100644
index 000000000000..700485064e41
--- /dev/null
+++ b/src/plugins/timelion/public/types.ts
@@ -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
+ ) => (prop: T, value: TimelionAppState[T]) => TimelionAppState;
+ updateState: (
+ state: TimelionAppState
+ ) => (newValues: Partial) => TimelionAppState;
+}
diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js b/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js
new file mode 100644
index 000000000000..cda8038953c7
--- /dev/null
+++ b/src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js
@@ -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 = $('
');
+ 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);
diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js b/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js
new file mode 100644
index 000000000000..5111695e3d12
--- /dev/null
+++ b/src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js
@@ -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);
diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.js b/src/plugins/timelion/public/webpackShims/jquery.flot.js
new file mode 100644
index 000000000000..5d613037cf23
--- /dev/null
+++ b/src/plugins/timelion/public/webpackShims/jquery.flot.js
@@ -0,0 +1,3168 @@
+/* JavaScript plotting library for jQuery, version 0.8.3.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+*/
+
+// first an inline dependency, jquery.colorhelpers.js, we inline it here
+// for convenience
+
+/* Plugin for jQuery for working with colors.
+ *
+ * Version 1.1.
+ *
+ * Inspiration from jQuery color animation plugin by John Resig.
+ *
+ * Released under the MIT license by Ole Laursen, October 2009.
+ *
+ * Examples:
+ *
+ * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
+ * var c = $.color.extract($("#mydiv"), 'background-color');
+ * console.log(c.r, c.g, c.b, c.a);
+ * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
+ *
+ * Note that .scale() and .add() return the same modified object
+ * instead of making a new one.
+ *
+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
+ * produce a color rather than just crashing.
+ */
+(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);
+
+// the actual Flot code
+(function($) {
+
+ // Cache the prototype hasOwnProperty for faster access
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+ // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM
+ // operation produces the same effect as detach, i.e. removing the element
+ // without touching its jQuery data.
+
+ // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+.
+
+ if (!$.fn.detach) {
+ $.fn.detach = function() {
+ return this.each(function() {
+ if (this.parentNode) {
+ this.parentNode.removeChild( this );
+ }
+ });
+ };
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // The Canvas object is a wrapper around an HTML5