From b8aaf9c9cc51966371b7a873f496e7f212cb21ed Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Thu, 11 Jun 2020 16:29:56 +0300 Subject: [PATCH 01/14] Migrate timelion to the NP. --- .i18nrc.json | 2 +- .sass-lint.yml | 2 +- src/legacy/core_plugins/timelion/index.ts | 189 ------ src/legacy/core_plugins/timelion/package.json | 5 - .../core_plugins/timelion/public/app.js | 517 --------------- .../timelion/public/directives/cells/cells.js | 53 -- .../public/directives/fixed_element.js | 49 -- .../public/directives/saved_object_finder.js | 315 --------- .../directives/timelion_expression_input.js | 281 -------- .../public/directives/timelion_grid.js | 67 -- .../directives/timelion_help/timelion_help.js | 168 ----- .../core_plugins/timelion/public/header.svg | 227 ------- .../core_plugins/timelion/public/icon.svg | 97 --- .../core_plugins/timelion/public/legacy.ts | 35 - .../core_plugins/timelion/public/logo.png | Bin 14638 -> 0 bytes .../core_plugins/timelion/public/plugin.ts | 65 -- .../timelion/public/services/saved_sheets.ts | 52 -- .../public/shim/timelion_legacy_module.ts | 55 -- src/plugins/timelion/kibana.json | 9 +- .../timelion/public/_app.scss | 0 src/plugins/timelion/public/app.js | 617 ++++++++++++++++++ src/plugins/timelion/public/application.ts | 154 +++++ .../timelion/public/breadcrumbs.js | 0 .../public/components/timelionhelp_tabs.js | 4 +- .../timelionhelp_tabs_directive.js} | 42 +- .../timelion/public/directives/_index.scss | 0 .../_timelion_expression_input.scss | 0 .../public/directives/cells/_cells.scss | 0 .../public/directives/cells/_index.scss | 0 .../public/directives/cells/cells.html | 0 .../public/directives/cells/cells.js} | 49 +- .../public/directives/cells/collection.ts | 76 +++ .../timelion/public/directives/chart/chart.js | 0 .../public/directives/fixed_element.js | 50 ++ .../directives/fullscreen/fullscreen.html | 2 +- .../directives/fullscreen/fullscreen.js} | 26 +- .../timelion/public/directives/input_focus.js | 35 + .../timelion}/public/directives/key_map.ts | 0 .../directives/saved_object_finder.html | 0 .../public/directives/saved_object_finder.js | 314 +++++++++ .../saved_object_save_as_checkbox.html | 0 .../saved_object_save_as_checkbox.js | 23 +- .../directives/timelion_expression_input.html | 0 .../directives/timelion_expression_input.js | 282 ++++++++ .../timelion_expression_input_helpers.js | 0 .../timelion_expression_suggestions.js | 0 .../_index.scss | 0 .../_timelion_expression_suggestions.scss | 0 .../timelion_expression_suggestions.html | 0 .../timelion_expression_suggestions.js | 0 .../public/directives/timelion_grid.js | 68 ++ .../directives/timelion_help/_index.scss | 0 .../timelion_help/_timelion_help.scss | 0 .../timelion_help/timelion_help.html | 4 +- .../directives/timelion_help/timelion_help.js | 166 +++++ .../directives/timelion_interval/_index.scss | 0 .../timelion_interval/_timelion_interval.scss | 0 .../timelion_interval/timelion_interval.html | 0 .../timelion_interval/timelion_interval.js | 11 +- .../public/directives/timelion_load_sheet.js} | 12 +- .../directives/timelion_options_sheet.js} | 13 +- .../public/directives/timelion_save_sheet.js} | 20 +- .../timelion/public/index.html | 6 +- .../timelion/public/index.scss | 3 - .../timelion/public/index.ts | 0 .../timelion/public/lib/observe_resize.js | 0 .../timelion/public/panels/panel.ts | 0 .../public/panels/timechart/schema.ts | 26 +- .../public/panels/timechart/timechart.ts | 2 +- .../timelion/public/partials/load_sheet.html | 0 .../timelion/public/partials/save_sheet.html | 0 .../public/partials/sheet_options.html | 0 src/plugins/timelion/public/plugin.ts | 122 ++++ .../timelion/public/services/_saved_sheet.ts | 5 +- .../timelion/public/services/saved_sheets.ts | 50 ++ .../timelion/public/timelion_app_state.ts | 67 ++ .../timelion/public/types.ts} | 25 +- .../timelion/server/config.ts} | 20 +- src/plugins/timelion/server/index.ts | 10 +- src/plugins/timelion/server/plugin.ts | 148 ++++- src/test_utils/public/key_map.ts | 121 ++++ src/test_utils/public/simulate_keys.js | 2 +- 82 files changed, 2453 insertions(+), 2310 deletions(-) delete mode 100644 src/legacy/core_plugins/timelion/index.ts delete mode 100644 src/legacy/core_plugins/timelion/package.json delete mode 100644 src/legacy/core_plugins/timelion/public/app.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/cells/cells.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/fixed_element.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_grid.js delete mode 100644 src/legacy/core_plugins/timelion/public/directives/timelion_help/timelion_help.js delete mode 100644 src/legacy/core_plugins/timelion/public/header.svg delete mode 100644 src/legacy/core_plugins/timelion/public/icon.svg delete mode 100644 src/legacy/core_plugins/timelion/public/legacy.ts delete mode 100644 src/legacy/core_plugins/timelion/public/logo.png delete mode 100644 src/legacy/core_plugins/timelion/public/plugin.ts delete mode 100644 src/legacy/core_plugins/timelion/public/services/saved_sheets.ts delete mode 100644 src/legacy/core_plugins/timelion/public/shim/timelion_legacy_module.ts rename src/{legacy/core_plugins => plugins}/timelion/public/_app.scss (100%) create mode 100644 src/plugins/timelion/public/app.js create mode 100644 src/plugins/timelion/public/application.ts rename src/{legacy/core_plugins => plugins}/timelion/public/breadcrumbs.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/components/timelionhelp_tabs.js (95%) rename src/{legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js => plugins/timelion/public/components/timelionhelp_tabs_directive.js} (56%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/_timelion_expression_input.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/_cells.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/cells/cells.html (100%) rename src/{legacy/core_plugins/timelion/public/shim/legacy_dependencies_plugin.ts => plugins/timelion/public/directives/cells/cells.js} (50%) create mode 100644 src/plugins/timelion/public/directives/cells/collection.ts rename src/{legacy/core_plugins => plugins}/timelion/public/directives/chart/chart.js (100%) create mode 100644 src/plugins/timelion/public/directives/fixed_element.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/fullscreen/fullscreen.html (85%) rename src/{legacy/core_plugins/timelion/public/directives/timelion_options_sheet.js => plugins/timelion/public/directives/fullscreen/fullscreen.js} (69%) create mode 100644 src/plugins/timelion/public/directives/input_focus.js rename src/{legacy/ui => plugins/timelion}/public/directives/key_map.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_finder.html (100%) create mode 100644 src/plugins/timelion/public/directives/saved_object_finder.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_save_as_checkbox.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/saved_object_save_as_checkbox.js (75%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_input.html (100%) create mode 100644 src/plugins/timelion/public/directives/timelion_expression_input.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_input_helpers.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/__tests__/timelion_expression_suggestions.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/_timelion_expression_suggestions.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/timelion_expression_suggestions.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_expression_suggestions/timelion_expression_suggestions.js (100%) create mode 100644 src/plugins/timelion/public/directives/timelion_grid.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/_timelion_help.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_help/timelion_help.html (99%) create mode 100644 src/plugins/timelion/public/directives/timelion_help/timelion_help.js rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/_index.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/_timelion_interval.scss (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/timelion_interval.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/directives/timelion_interval/timelion_interval.js (88%) rename src/{legacy/core_plugins/timelion/public/shim/index.ts => plugins/timelion/public/directives/timelion_load_sheet.js} (76%) rename src/{legacy/core_plugins/timelion/public/services/saved_sheet_register.ts => plugins/timelion/public/directives/timelion_options_sheet.js} (76%) rename src/{legacy/core_plugins/timelion/public/directives/timelion_load_sheet.js => plugins/timelion/public/directives/timelion_save_sheet.js} (74%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.html (94%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.scss (62%) rename src/{legacy/core_plugins => plugins}/timelion/public/index.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/lib/observe_resize.js (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/panel.ts (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/timechart/schema.ts (94%) rename src/{legacy/core_plugins => plugins}/timelion/public/panels/timechart/timechart.ts (94%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/load_sheet.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/save_sheet.html (100%) rename src/{legacy/core_plugins => plugins}/timelion/public/partials/sheet_options.html (100%) create mode 100644 src/plugins/timelion/public/plugin.ts rename src/{legacy/core_plugins => plugins}/timelion/public/services/_saved_sheet.ts (95%) create mode 100644 src/plugins/timelion/public/services/saved_sheets.ts create mode 100644 src/plugins/timelion/public/timelion_app_state.ts rename src/{legacy/core_plugins/timelion/public/directives/timelion_save_sheet.js => plugins/timelion/public/types.ts} (63%) rename src/{legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js => plugins/timelion/server/config.ts} (67%) create mode 100644 src/test_utils/public/key_map.ts diff --git a/.i18nrc.json b/.i18nrc.json index 9af7f17067b8e..e8431fdb3f0e1 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 eb43af293c670..a3d94e0727d76 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 9c8ab156d1a79..0000000000000 --- 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 8b138e3b76d1a..0000000000000 --- 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 b5501982cec09..0000000000000 --- a/src/legacy/core_plugins/timelion/public/app.js +++ /dev/null @@ -1,517 +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 { 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 = require('ui/modules').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: _.extend( - { - 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 104af3b1043d6..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/cells/cells.js +++ /dev/null @@ -1,53 +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'; - -require('angular-sortable-view'); -require('plugins/timelion/directives/chart/chart'); -require('plugins/timelion/directives/timelion_grid'); - -const app = require('ui/modules').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 e3a8b2184bb20..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/fixed_element.js +++ /dev/null @@ -1,49 +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'; - -const app = require('ui/modules').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 879fab206b99d..0000000000000 --- 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(_.capitalize).join(' '); - }; - - //key handler for the filter text box - self.filterKeyDown = function ($event) { - switch (keyMap[$event.keyCode]) { - case 'enter': - if (self.hitCount !== 1) return; - - const hit = self.hits[0]; - if (!hit) return; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - } - }; - - //key handler for the list items - self.hitKeyDown = function ($event, page, paginate) { - switch (keyMap[$event.keyCode]) { - case 'tab': - if (!self.selector.enabled) break; - - self.selector.index = -1; - self.selector.enabled = false; - - //if the user types shift-tab return to the textbox - //if the user types tab, set the focus to the currently selected hit. - if ($event.shiftKey) { - $input.focus(); - } else { - $list.find('li.active a').focus(); - } - - $event.preventDefault(); - break; - case 'down': - if (!self.selector.enabled) break; - - if (self.selector.index + 1 < page.length) { - self.selector.index += 1; - } - $event.preventDefault(); - break; - case 'up': - if (!self.selector.enabled) break; - - if (self.selector.index > 0) { - self.selector.index -= 1; - } - $event.preventDefault(); - break; - case 'right': - if (!self.selector.enabled) break; - - if (page.number < page.count) { - paginate.goToPage(page.number + 1); - self.selector.index = 0; - selectTopHit(); - } - $event.preventDefault(); - break; - case 'left': - if (!self.selector.enabled) break; - - if (page.number > 1) { - paginate.goToPage(page.number - 1); - self.selector.index = 0; - selectTopHit(); - } - $event.preventDefault(); - break; - case 'escape': - if (!self.selector.enabled) break; - - $input.focus(); - $event.preventDefault(); - break; - case 'enter': - if (!self.selector.enabled) break; - - const hitIndex = (page.number - 1) * paginate.perPage + self.selector.index; - const hit = self.hits[hitIndex]; - if (!hit) break; - - self.onChoose(hit, $event); - $event.preventDefault(); - break; - case 'shift': - break; - default: - $input.focus(); - break; - } - }; - - self.hitBlur = function () { - self.selector.index = -1; - self.selector.enabled = false; - }; - - self.manageObjects = function (type) { - $location.url('/management/kibana/objects?_a=' + rison.encode({ tab: type })); - }; - - self.hitCountNoun = function () { - return (self.hitCount === 1 ? self.properties.noun : self.properties.nouns).toLowerCase(); - }; - - function selectTopHit() { - setTimeout(function () { - //triggering a focus event kicks off a new angular digest cycle. - $list.find('a:first').focus(); - }, 0); - } - - function filterResults() { - if (!self.service) return; - if (!self.properties) return; - - // track the filter that we use for this search, - // but ensure that we don't search for the same - // thing twice. This is called from multiple places - // and needs to be smart about when it actually searches - const filter = currentFilter; - if (prevSearch === filter) return; - - prevSearch = filter; - - const isLabsEnabled = 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 f3fd2fde8f2c5..0000000000000 --- 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 { comboBoxKeyCodes } 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: _.indexBy(resp.data, 'name'), - list: resp.data, - }); - }); - } - - function setCaretOffset(caretOffset) { - // Wait for Angular to update the input with the new expression and *then* we can set - // the caret position. - $timeout(() => { - expressionInput.focus(); - expressionInput[0].selectionStart = expressionInput[0].selectionEnd = caretOffset; - scope.$apply(); - }, 0); - } - - function insertSuggestionIntoExpression(suggestionIndex) { - if (scope.suggestions.isEmpty()) { - return; - } - - const { min, max } = suggestibleFunctionLocation; - let insertedValue; - let insertPositionMinOffset = 0; - - switch (scope.suggestions.type) { - case SUGGESTION_TYPE.FUNCTIONS: { - // Position the caret inside of the function parentheses. - insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`; - - // min advanced one to not replace function '.' - insertPositionMinOffset = 1; - break; - } - case SUGGESTION_TYPE.ARGUMENTS: { - // Position the caret after the '=' - insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`; - break; - } - case SUGGESTION_TYPE.ARGUMENT_VALUE: { - // Position the caret after the argument value - insertedValue = `${scope.suggestions.list[suggestionIndex].name}`; - break; - } - } - - const updatedExpression = insertAtLocation( - insertedValue, - scope.sheet, - min + insertPositionMinOffset, - max - ); - scope.sheet = updatedExpression; - - const newCaretOffset = min + insertedValue.length; - setCaretOffset(newCaretOffset); - } - - function scrollToSuggestionAt(index) { - // We don't cache these because the list changes based on user input. - const suggestionsList = $('[data-suggestions-list]'); - const suggestionListItem = $('[data-suggestion-list-item]')[index]; - // Scroll to the position of the item relative to the list, not to the window. - suggestionsList.scrollTop(suggestionListItem.offsetTop - suggestionsList[0].offsetTop); - } - - function getCursorPosition() { - if (expressionInput.length) { - return expressionInput[0].selectionStart; - } - return null; - } - - async function getSuggestions() { - const suggestions = await suggest( - scope.sheet, - functionReference.list, - Parser, - getCursorPosition(), - argValueSuggestions - ); - - // We're using ES6 Promises, not $q, so we have to wrap this in $apply. - scope.$apply(() => { - if (suggestions) { - scope.suggestions.setList(suggestions.list, suggestions.type); - scope.suggestions.show(); - suggestibleFunctionLocation = suggestions.location; - $timeout(() => { - const suggestionsList = $('[data-suggestions-list]'); - suggestionsList.scrollTop(0); - }, 0); - return; - } - - suggestibleFunctionLocation = undefined; - scope.suggestions.reset(); - }); - } - - function isNavigationalKey(keyCode) { - const keyCodes = _.values(comboBoxKeyCodes); - return keyCodes.includes(keyCode); - } - - scope.onFocusInput = () => { - // Wait for the caret position of the input to update and then we can get suggestions - // (which depends on the caret position). - $timeout(getSuggestions, 0); - }; - - scope.onBlurInput = () => { - scope.suggestions.hide(); - }; - - scope.onKeyDownInput = (e) => { - // If we've pressed any non-navigational keys, then the user has typed something and we - // can exit early without doing any navigation. The keyup handler will pull up suggestions. - if (!isNavigationalKey(e.keyCode)) { - return; - } - - switch (e.keyCode) { - case comboBoxKeyCodes.UP: - if (scope.suggestions.isVisible) { - // Up and down keys navigate through suggestions. - e.preventDefault(); - scope.suggestions.stepForward(); - scrollToSuggestionAt(scope.suggestions.index); - } - break; - - case comboBoxKeyCodes.DOWN: - if (scope.suggestions.isVisible) { - // Up and down keys navigate through suggestions. - e.preventDefault(); - scope.suggestions.stepBackward(); - scrollToSuggestionAt(scope.suggestions.index); - } - break; - - case comboBoxKeyCodes.TAB: - // If there are no suggestions or none is selected, the user tabs to the next input. - if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) { - // Before letting the tab be handled to focus the next element - // we need to hide the suggestions, otherwise it will focus these - // instead of the time interval select. - scope.suggestions.hide(); - return; - } - - // If we have suggestions, complete the selected one. - e.preventDefault(); - insertSuggestionIntoExpression(scope.suggestions.index); - break; - - case comboBoxKeyCodes.ENTER: - if (e.metaKey || e.ctrlKey) { - // Re-render the chart when the user hits CMD+ENTER. - e.preventDefault(); - scope.updateChart(); - } else if (!scope.suggestions.isEmpty()) { - // If the suggestions are open, complete the expression with the suggestion. - e.preventDefault(); - insertSuggestionIntoExpression(scope.suggestions.index); - } - break; - - case comboBoxKeyCodes.ESCAPE: - e.preventDefault(); - scope.suggestions.hide(); - break; - } - }; - - scope.onKeyUpInput = (e) => { - // If the user isn't navigating, then we should update the suggestions based on their input. - if (!isNavigationalKey(e.keyCode)) { - getSuggestions(); - } - }; - - scope.onClickExpression = () => { - getSuggestions(); - }; - - scope.onClickSuggestion = (index) => { - insertSuggestionIntoExpression(index); - }; - - scope.getActiveSuggestionId = () => { - if (scope.suggestions.isVisible && scope.suggestions.index > -1) { - return `timelionSuggestion${scope.suggestions.index}`; - } - return ''; - }; - - init(); - }, - }; -} 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 256c35331d016..0000000000000 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_grid.js +++ /dev/null @@ -1,67 +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'; - -const app = require('ui/modules').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 25f3df13153ba..0000000000000 --- 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 56f2f0dc51a6e..0000000000000 --- a/src/legacy/core_plugins/timelion/public/header.svg +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - image/svg+xml - - Kibana-Full-Logo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Kibana-Full-Logo - - - - - - - - - - - - - - - - - - - - - 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 ba9a704b3ade2..0000000000000 --- a/src/legacy/core_plugins/timelion/public/icon.svg +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - 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 acb95e80fe18c..0000000000000 --- 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 } 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(); 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 7a62253697a062d70fe8e8cccaad83638851af5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14638 zcmX9_1z3~)*M3Gfn;?yJN(_c{J3>+#q-%h*AV^O|7$G6uA>Ab)NGJjV14N`-q@|># zzUTdayRMCG*TZ(s`JHp0`^2Ii>!^{FFpvNMK>kP_sSf}U0r0sJ5drw^ zfc4z^WxhP8Zzof5T~Qph`(2sQm|h{jNsA{OM=-fL0Y|V>DC+rLC(4H1uLkcP?8%Mp zQw}t{X*T?kblIHRpSoZBO?o5OXYaeuXut@ftfGRSP?7Z4kzYW-1sCyP#0BM-E#El1 zbzI_TYx9o46O;HIbcB>gD*$VF|NSu*U-=tOY8_gVra>)#~kDjbS$&P`@;C1&4K)D;>R0O(1;(1W&c2GVdl6*Vy zV89c9OdQ8O<((^6Y`n>&?S8#d{?rbD z7-6gw838O`QK{GMkNvXc=kwPy@I}9X@4i3fROo3;K?zt?FPOAAjXfdf-0D&gRsQP{GhO)$RASi6N0zUU9_Z|WwAxu}Q< zcAMrhZ=NTS3?Y^{Lr?WY{CrXK^2H8#I0VwKq_1w^!8gz=W`b68WqzPjx!Xh(kz7*D zb<7)iOvo>>^37(%+th{}fu+rkzB5kGYDRp3-!UXAs`YI^oP%d5x~b{>%a$|rRYGnJ zyPTy;Xw6+kB<4eNyVA!URAm_rCC*A@s52Hd07|Ry<6~Pe6o47 zVzQ&9202(SD1nf0BjXZ!1MYPjUp?+0@h|H%!lB{wOr+e|$#3i5BMLC`a!xoF_sDn| z@1yRM68t>(Wyr z=MHyh!5OyGv+I|n>CP-}Oi4?l=D45Y7l}U!?+N}~mdv3%C z7Ccr*S&@uQ5*Cf_kuVZPa`@>LBtW3782D2M0tSi;D2AlEWF2_LMr@guqs%UZ#B)_8Fx9}5 zk+vJAW`}=YCodRgd{qd2=2PVyeK(jIrfx=$Vsar-WVloDsJnL_=GB(ou@Wh#L%85XbI104Bj#b_3Bwbhraf2@ZEnxz<4OdhsB*z~ zQ$9x;fH(RXr;JR{(jmim!wlX13~S}D02N1!=wriliag<1?YXR9|E`KPZqgHZb7csV zG*MsNK5$pNHa|&Jn2o*~;}BUrHt5#3TWZdk{wFNq=Q&T7@hw>%Le{-cx#|-}7nvmu z_X(6Q(Pnu0Pf%NLMUx^W*$933c6v3gynFVekCQa)TFQzZ)`7R|itDo9dATrR5L_Bf$xIk4IAfE`Aja2j|7eme#UeB_d)!X%&MHdPD!t90k5l+lQc`@PHlL#8^Ds<+s&8)B#8VX`BO?T23Sz2; zbd5VLhDNR-ea&7qK(hWx=8Pz#{BT-{x-C%hkB}ceVq;56O1xZLwkS|H378usva7bd zJnDA#9?tq`p64T7-L6-d8oTu)X=!QhzSNzzMX3^P>=gxK>eJuB`(X;ktBH?8DOhQN z2E;HO)dxw>BZBtu6ete9y&79PD|nflNm`ms^CG|u-PY3Wb$V$71^J(m>djKrM0 z$d9ynkx`8(G=WRDin7i8F&~{o2uUU*#cE!dRis=NdweICAUOw5>_Z);dst&qEfgaB zk;=CvNY@l$n7ItY**$dagTEdpgua^*;T0rW8DcnDE#gX|uhHg%OxvCn3;%+0u0z>U z!-vv@S;{N6EhB7$85R9VBB^mqiEIjN=^`sDD_OQTH_zV;t!eNvsXl*xVubxf*1Z)n zxfh}b$%ZVGvH#OEGMX?4jFZ#S!V)yB%a@;h#L~p9sj_{|!MYKCf2e-Oo`xAwL+0Ol z8m8Bf;Ngt6Thx5jh*`zH!Y9>XE9AlklLJ2QD!74Oq#@Zd3gy8kC>Ww|G(l(;eAs*a z@IkI?ZAUXJHOYbwvIW8mytA^W0tjJJn9Y$NKYqML`dZuDukKG)n@c!Mwf=HA30Aiv z1EQ7F;qvV}ZmJlxjc9*kk!x5w4X$R9Y9m`H4vl!So<=-)IQ9-YeamyqmbKU1vM z#Kgps0ktCnvGn$5xREDG`i$d;ClV&c#zH#A^$iWro`SZ?%eXiLbXbtqcsBCrGti=W z*7bHOE3C7dawRcU#+6x^+)Zj@Tn8cal4ToJSURT=I_U}0=6soAxMR+_)#~xS@z7b_q zX>1lqYiweYc1KT;T{G!@L`Lmh+0E4-Jj4#o6oOEsa4Dgf`-MGb7=pl6Emc~NRm;ENtt`vXKF(-e7BA4-_wo<~ms$F8%> zzM>jD8?MoF$WAT(eVaVLekSIj+U(f(pK)=D4u0vm3-0qZk?q=S;jFRD6T5EOA|*^b zMcr*6Bt-*NxLy~JTZXYM|E_N&_S*M5V@LKYDl0u#F?K$1M5{wAi%}{I7@h zjkH0OA8amjd!|2hZ7p#(F=Ta2H?%{7D8yI4C;Ewr2aSFItzxf)Ou5E#sG8b{ z_;;9e8ScS{@pTkmu08wMK;8q&lWjjetP)}S>DRN5KL-Z~vobDhnc7`#9oI9#SG7g@ z7i($O##f=YKA6qrxVxm+p|?-chEjrEoiX5{T=T=^VqOa0>N}Dlx*A^`{>msf^pnn! ziZ1*_Y@?{`fu2v!%ezemQ`CXaTEl0uFF35CbG+X)cMM9$e=Aa|ikH1E=&Wdr6+>{x z4(#ezA#iuKqY4XHn3?5SzI@KjT6^;`gLiO;F}1{}YUOduy*MOO&yQDoN8E3^2##xg z^9;?*%)}F3ss)r57XBK4d5?O+Ob&PXH#OyjKpRK1H4$4;vOUIG2yMp4ba^+Xw;=+f z|JH1Y%!V1U&sIo-5@Idn!|=9TUan+CZLWd(8eGn)%-7qi-PNZxzD&PB0BS%PQjDg% z9QZ{jygd+HIq@%gXzd9IayWda5;oa78%(HBSh@RwI&cEW6E5RF{)VNBa<(Jsr=g2D z?3*yLmOBFF2LVQnFNf-cQJP=^j*GF ze1Ir@;z<1&4RnUyFu7B7)Q#PjPO+|sQf_|g%NILikx*YNO7jX_8UIz?(NAa*18&(*CJn@4$>7FZD)^S2?NF}7r~lkuc;a+K?-zk-0yC{EAk>>7^cr}fEyOD`@i!fsAmx)wGDygpiFxVX4XA;~XGxLkN5F}Xe; zEu21D)U?_%fFCLZ@iI|Z9gEt0WI4rR)$Eo*<0M-#@w9`k`&zMUr07V1CI4YW-51dwlzaOqfpT_-Cv&;lYx+P%KYn#XmZ9c-H;Qk_AY9SDGPF zcWu<>KxSs9Jc{jTec0gW=xBqOt#cF`r6ntUQ9izPrKzp0t$f?MnDF&#WOQaGF{M2@ zHV#c;8E@K1MGO$6zbtu84y9k1B{PDhOLEuhnx=X2;(^EyvYQrn9#Nl$!?jvrEtkl8 ziO+GsV(`BJF=ppY4+Xj)aB4z~q+r?2j43qr~&(7l{CB9-v0 zyG6z2o15Pu2US5D@U3tW*Vfyc%OmB2$IlfeFK`AO!>u%_ z)o~7HyvwjQW#Kb?9bG7k*PT%`tWsQDeCv6#J>hvK7#_;=1s@-3($5o zHRa`x1`KR=XFrgGFRifH>*0T^BwQ#V+;@GJPjV6yy_H05CpOR%l~b@WdO)0~+^=GM z>yzk&I9r<$14xpgTI^MTO=&an*5o&3cA4?$Z0tGJlL@ykDNCI&Co4agPS>$=@QIAX zXSb-Xv%(!|kjNY~H?dR4vP4|}+Q~WpVY0FMvliPkv2ji1saRebP9z3Phd_aglw?SEKuwfGbatjo$iy@zH2KFTqg~aQ&VYx)(1*C2oO4@9z1wp+1GAj z9dd%6=w;jFsN`+3L8sP6Ngm->LgtR5OPh)k2Yz zqwVqUL$7Qf?!T%kSkk*m(Z37~tUvummA)_Tz@5o(IsZMecei0iAin|!i@t{nKRMqI z!kN>-(5H@Ww39`pq_-vi{l{{(O!(TbpI2De({Y_KaW?tt@bBNh3S`Z~Kv`MY3!uCG zQN{e7%l^TX`?`<>=SWS_qrzg%G;Z=)YfH=Pw^S;o5CE~1Ty$Ols(O3lJUcox)K2~g z^+12{p1gB(p(NyZ!6OFBTIjkId^D`&Cu> zCQztsO(wt(lc|(#xaP_^3QG&6%ikA*H?Rv6T5&VHjRDls?Hj#hu_En(Y~RX_FsbPtU%< z+f~aXlzNtbqK-Ize0$%=g%0HTp5L!|Up+okI{W45pclxJM4$ruHX>?&H7zjhMD3e~ zaOAmbinZv+#tDZbc%0I?)P8 zKcFVbp*cEBdl$PkHSn*zs_N5&ySBYplY5l5_<%Add;@oRjn?YL%E9Af`z~4wK8ROSt$H%DBAe*ff(IEDs~whR1%3@bOa$ozi!QQ^zsm zCl!Ywq}XkKpC=J4Py(^J)4S<5bOW?<75sMoGL1>wb(l??fPjEuy|*kbZF&@NUsb4wPyohiS+@vv!&n!@si0IVM6IN#k2$r zXFf2Ge5%z}(^bJc_#P9c`){J)og&JgI^wsuezp6;{b|0qWH%I*U4yAtpCj$ce(jA& zx#JZUq5E)g^Jg$EmB@ey)y|X(jzv>*B%FIJC#sOQ5E1P%*%k6AJt-;a?bo(9%u#Ns z1P_14@fWOcBPeHHGr9}U==+gD>$8+OgVbfWlcwE5j*!&GdVO5{P~{g94IJ#V2}Li0 zn?^+*ZF9~oFv4A@YTSYE3Hg#M&*{mmJX_3`)60|M+B&vMlb8Qg3ccO=q=h%#K>LjmRzY+4`TjHD${=1l#J1hA$7w(?864x9hE{ZJVPVx<9u=w z6}>wV3T%Y}K;4sSdUdayDBisb6CAoB0gWASWKOS zuR`FzN9kZ(3^~2~gHCluVF)}L6|m^d&CLV0`T`fqU6iBaT1)x` z@o^X9i{36xQ~PQIC|+)w#S4h`I42wczms&57wvuAUcS58iv$)5In_9MLg6Q zQVZPl1)LjvT(8^lXj=T_&%HzbrUzJn-H3#m_Q!yCpn^S$Qr(x^n48&$=2pZ@a54RO*)ZA4Feq46DsUQSSOHW}0wnw{z-d^e$rmZm zXC^l5+_2$OwmQS&^W*sO)X;q^?(8}t@BGg()pPCcHARVmec4p_94IV#rHwwC=1_BgG5hgJ?xvu2a#OGci&IoL zUxI@! zBlA7ZT_!7t=)Mt*PEM4{*ylIbhO7Io##3q2|C(?Uk-1;gJg+{+*WsXT)Cjj1Z;2{d zUXTTevxIQBo1Wz+^*M0JRFO~5rx6SEG(%QDJ9 z$=EaN3tZn2uaDLW0Y|mCdsM)@$QXm_?_+8fJexmdTCqaghZHh4!w;+o1GxxHo84v# zzz$R`;wVe~Ui~vMm>_PQK<2%UyNO7mM4c#O5DBllAbA6~-e8MEzM`tiLIZjKvRpbN zBad2H7OW3H;CCw_V|8PoQI#qBHmayl+reZ$7M6vwB!64FzF*EiemV%WLZ^TS zM~=zXr9!i#GC6~TvBR0R zu;r^CKXJ1>F0pSk zvBNI>SDt2k49eNU9(Ae6^bFA2y8?59MgFGkV?8}@>o7J)J1B-TGo5gG2vbz8VPtyk z2MXRI@T6%oo#C*`vb|Y}$onqZ@N6*#NM?Pe)0og!H_KQ2Y1{eLL5_M=+4ya=It4D? zLum?$Udm>3!}Q4y$a#<*FPxMMlb^ZC2e!{%&-oWyNc`UKChXd8UZyunzBgrBI?Sl7 z={n`^Mv-|ebnRh58xP{2Q$_#`eT&JuRk0s<$JN62?quEc&B5w>Qs?Q@r;}>r z3Gq>tWC%|a>a|}cRsmcE_LsCUg6r0!qnEE2TWMA*_`#4p`@6MY+llY#`ZP_7TJql? z4^bt_Rr!3 zIx^Ym65-I&uHzq?){+w0H)XT=U2mOnQtE{2U3Fb8T;~(<>Ayv1;^SR~UL6i>EW7V1 z*ltg|BwbdIvrz3`JHCtI-BJkL)wN{ARfclE9o<8Gv;$-mJRicR`S8qS?B2!qO+I4S zF8uWAiUs?}W<4??Q}jhTTTxY&+g|0{RjlZtE)PcJnLqr15(3_QzBWvamI4cN5FN=5N&b% zLNL(H`m}QYzxoc}>k_Qu!a#>%Oyo@Ik*-)pf)*t}$+v(#ZxU2z7G^|g+k23%`%)Qm zxd)D~6pAt%{+Q|;7_8zSyzY&my`6=}76UYF-a!8gG0~&<;9w+eRN?k&`9YsGG}U$5 zK!mY{Ju}|sR!JCx))Q%WUa5O|9T`|bG?(?{VD^32=L{2zSdg%ObvK8u#9q*T@td@8 zn_E`u+3S!zzK{>6Dmu|Ox*PCb&F*)&ce?1@qNn=$WFN8pH&sdIEY#z42xtH7T-@f@LZ?8UUPX^8#XDF6sDvhh##y5^~D3@i~8`$jJ=Q#uu zp@m%ky2Qvh^f)mD04sY=M$#m`MxlG(ZagN!mxlgrM@m_3^4aV6uhyRfwM+g>AxTBa zCaSBi=O9|j{(d`9bPgcK9;i!n_;0~~se=kg6)^i@EkSU{g7#~A8~o@q=ZXG$ahlaE z&hmO~*!7M-9?r|5s7)MzhvS``%xQo$o((Bl7qcn+@#Dv^x7_*X3J=!^R#T#-wn^Z^ z`0NN64qTMK>GY55e)O&o(PM2AWj0*!WGN?aU@{*9vrkh<6D0UUoma6isdS%Eu%gHq zTRCCdq-32QgP|SyWzXB&937ZP)-jZ}*BhKVsZS?5chReJryCaIwa*kQ2ca!|{ z5L|?Cap6DRJ1b=4PUL?bX7N$!M#yTeU$$@)-k+;ey6#N0c}r9ShS2epby$dVM_|`- za$4E_sPny%uA^twyjb1-RCjI5w(7&xhD3=kuNH#l#^!%igMp|el=0@*xK3XgaX+_R za_t9l^IN0Hw=B1nkq?`D3vK>}!|I+;yUqUyw}G>NZDn5uyn`Ny`I(bmE@-&hjm2N*o z82Q_LBbJ7cs1*-=hmkpOK3%J*^!Z#+X=%pegggrNS6v+QDMM_V`@)lOE(o{gX&Fwz~&-~swVB!7PdI^2q6;KeZ!AIF(2vD3xe-}!BDeIg-H7eT*H z+DAzE@$_nJPzKw|6fQLU9PZq0XJjkle_Qp2)xj}S&oF@?se+YSJbwpf>%Dm-m(cc7 zFNueY|6_Pd@m)`aOVLcq2RIl3nHZ9B5$|B}Xp(VT{q_?}viy%yXy*37$bpp(k^!Go z&rQE`oxZwm22{E*>pn|zP^Va29%p$Gt|U3I)rJK5n!Gigiwk$U-JN<8rElNhO0e+M z^x%j{zx`g1QIzSA@q`o^_F@N`#Z=?}T1<=PN7BI}sukQxuR!J$M8ppqT=Y!WSKb9V ze?7g|7rcKTr?aK?&n&rKU6M;(9XQ?^ zC&i+UiZe@k7v$(Qz*{tVj~9RRUkgjig#@)-Ur8pXM|{uY`;WH9n!)~vKo-=ls>H>U zvUfV--AtpC?$q;;hW^z@bb5Zgym{|Oa96!?{CPOJS%v$f6FCrplG^*z3$NBwZA@~e zpJzjHGB}9(pELGD&iU_|KkVybwG$MliNYz zhw9Q@1cYTX`mp*1iuAgGhL$#7Btxt6f%eYvY?lbbyWio|rplP{_gl3$y2Ju8z53l) z2|#ikM^od%9563V?jJOL0fSvY$TVzbcKv8!yU*GeMv2(mFj%;b5P(JX1kGs{Y;5>YP61p#UF5)A@6-Y zIlDepHLsu;!~4Ba@_B=j4eI`Wf$I=E_bbQB*un z5Tc@e_^&_9%{9A|o(c_(j0lxgR?f{`wlI*4H^ zuJK2Rtl^pG&!4ydifuoVXZ|zcR01vh<|e*hUR|y2Kp+C?4s*?%TuWW^lH`u;S(((% z5 zF5vXfjQzucgeTV4OEQ_c=?JHhb1%tHORI-u#g#_1uo$mZ?l6Y{VC=iz2M=A{CWhA0|nU;8RGyo z+(!y66UED-cpu&PKwCyE==9GPI2rwl-9j2FB1hX6_E_?#aJRiA*AYkWF%H(b8pyNO zg}r`&sC}XHPB%Fa49vcP#()}&&+VR;0xuu}+=czc$^5Wl*GxaBM~?<1`PMWsc(hpj zgP@q+zq_-uvvms|w_NPTKM8@~8ho|$t={%|yvcs;A@pb&NRwzIwXy|Iu`el`{5$>5 zox>M<4Rc`l1>-E^pgO6$XOLn9u&ELpl-2LU7xI{9BeG>J;6rAK<&KwR=vQouT5TFz zcxz2f&7QSi`?VVqr5OomK@QL*E-74GT&w99f~ZqmI`U;taE4~8uP>^maY3j(uBsNs z+ZENPb%sMkAE5^xQ`dw?OBYc-exfts7GS8Nx9&b&XB#t&YEQwWa=bHk!$EsGXP&Co z?i@;^FMv~oPdT*kZHBed*SaQXH^?lL`d5i(c25aVp~;w361M-m`m)T#TnK1U8MSm; zuMLpRC8vm7g`iGdGn=ufMoVR+?wv7tfJfv=29yj>ad!9A8n+dLnN626&`Z8g*3(Me zJqm{>&^j3YdK080RK zclY${lD&U_#}#{JiKMZf89Ta+=wA(}Kl73NnJkk)sxq&t<4A#HQ@i+tclAM@jG39M()#;s>(=Soe~C@p2gjt} zu^Sr6u_%Jd@OcmRiyZl6mS__kkiixzE4JTcmfVF%Dk4pAS$p82gv%oF{RUb{gNzG} z_Ju{B=K_c&O{D+ROg|b}&S$JY02R;LB>*-=kCL6OU5k^uqNR@cQJwimY%wnT#O(i) zN?Z&CX@(vFs7U34S{bIe0ie^i-jv%pQ#=i`$5SZ(h?0q--x2VzS*Hhuz^#kUU^#ms z%f{Z8i0QO%oasa&brC)vN?dO70Cs5v6NPn&%M)VQbvPzhsYU*v5*r*-;xYvu`2F;Y zC+z%j&9~qLx!##k0jJJlys1%(JBhsetw_Jkd=nRCDPR0Y*RPydG;8U9Rg*^j6)`PBZIJ2ZWnj|mJHIwEt z7M;NYIVb^*_+{n55U>WXpWMP)y!=LLThnWoL9xuxz99d@qh%D=D{{c0!6j2X2eaq6 zUJ9^+mcqBGEpcfqJiD+oGV+Yw+S33wTr=0BxqA*kaPh{+$4|aw;UUZ>yo|9Glpt;ZAg!0-#nX8#4fVkPLrg8u^UBf>t} z_C$mLoUo7)Yyb!hR1mD=Iv7GLcwR8HJ2?1oA-17H)Yi@I`oj?}nW&g%qd_d=8|cw{ zcQFppAr8Q1-AV4=Xy0^1bW9Am+jEC+EfoB3vNy_UxYK=DTE6HXc=;T3WS!0}bF|NW zeP#axvTZp!ln{QM9?_vAcrCT^IV9Vzapo_Gonn9x`#&ng(3dY?Dn?c@o7clx68nT` zEEeko(cozG?-G8T1f+sKd85?>$Sbpg}0JR*E9m3LnjV*5TnVG$u6>aQ? z$Hu(h*u(cK8Ac7oR&N}>5K7YbsGr-7!FkzD2{;h3i3-qY;sV8;oi|ch!^VTyu1PUL zTd&S?gVHB}g&SR84^S6ywC;zJe~XbFa}7!BS>YvpqrAG{(PBB?^bq?g0VCU1GwLJB zq=TA-XbFR+0KX)c{EhMw_}&pHt2;KhBw19hWN67bbFy5lGaGXc(!DcTJ)UvFkF+QU z@+Wuw+QI$PNrX@EZMyx}*K<${JttY$h5XSwv6axb_4vqeaAlF=MKjaq$=zYOU;RAn zbU*5y!er)8BJ20jElv$4zohNv9R?mmoBSNG{^-{`${QwjyrDUrBG0isIvE5w(5bS) z-T>It2G`xT+>A?J!;Fir!o~(yp!JVmkh`(*q|xr~t_l*5Z*4w(yQ^--jT?X}AA9Sy zm#kJv)3}W+chP&8?iXmob;I2P#w8w+3wf7cm{_(@BQGoj+82iE}XLQ z&{}wEIW;h!b7C^Rd&&SHz?2IxLd~A8G>*!<3J3``XAQ42W?WFP2M&6)EWLMmGVkld zoy13phAR77(#H3X2jqqR@shk@EB3uJb}Pvghg(r$b%!D{CS9t1>iQ$4;x~^LVO7o# z3t7k_5ug2{s`?E}-tf)82fOpO{*p|avU!^y;LdOY-G^w>gffH`;laNjGu!yU(nz-v zK<&lpY?UQ}SEM)@ z0KV1MIkmyGKTEe`yulX8(ZHCBGv}7?JKv&85HDkLqmh#iANr$`OaAL$SF8@drV{tvRn7c z+S*#M>s;Y7`xZn~d%$74?S`v;A(RIV!q-hMh}yjG{<Vl6oA!oy z>d0UGR%ZW~QR1QtqJHgmn>=y%REch$Rl+8nik=^?dc07*%;=VHQK}v|Fm>OO^#|es zxclO#kFL7&s77r9FGtX|%kRZ+)Y!n!`3vJlLC_IsocYwf>t_NIoFnKMZHR=P)5*z6 zB+$ek{o}bO=!7ibeFmgvRw{bpfwH;gV%m&D*cC6yU^a9$aOxW;!bEXvL6m4_VG)d% zcxkob*X}tfw!48$#wxnR*wV$N9|eB*XyFNlUA3FlD~0*@64<|l_EDpa+56a8p+D<` zbj{${zbGYU-`U^MT6bz8y+TmaA^IM73yqaH1Qi`jLUxQgiLGC zhEfG1cl|CoxRbU5B6|+u=nQa;gYBzJM}-DpFpN5S4Qj@PYR1iPYHHH{Y;zzFguUhW zW-Lm+3xsA>E&a!7hf(HANL`QvKLx1LFCvk?^00v7oQaHpE?`9*~fjslDIbC1IU94DuG8}zs}=?O&_~>JkmV3ONlt@NBYtt6z=4>oH$z( z9cuEe^@D*+2-|(9f1{{z;n8$00{R*lD!H=3=LggP{)*Q*!pO)Sd5q~YR_NR#Cz+yvDohC$y-)*UZ5}lk`Lu>1>jA0Zh5!(%Sv8?d3%Vy4420as# zjm8-_N}Ly;SG=y65>5Y3K_nlHiHOXPVp&urp34(@(Za6znqSrIM1s7gwxi=Z4CI-e zn;}nO$`(Y+MyQ*p1rLez02Xvs`bEd)MGH>r^orNlOi^`|6Wky#WTSLSlIeb)Q>?>FOnqf*&ZSj2XsW$pL$rnG8LsMt7KUFqjmAGA=BD zQm9_df(OgYB{IqxA05d=5vihKhD7T0z>Ui)GJOZG_fZ#4)8w0uH6PC(u~u2Aef0-^ z1K-#3h>*udB4Rkly!8J#QWz*yPVM4Y*xR&2EVYzcD5lfL4vx68Ub?XM67HrSd2STlAgV`3m$9EH8e@9Zz2fS!k z%%&Urzf=$&vXY-!`$ke`mUIkbr_}kFF?i?g3%h;_%pybfV;N#Ti9K^ z1br*v@b3@{%(-W8-d5Wpw4+bKVO&>c9?-h$iV{u@dla(Mh>&yc=kAkmpRe# ztkI+8k^&$ic|GRhWGEfNpgnbY=cvgCV2X+g^LMn9O|SpSL3+uB)!8I z-9W0B(j66ENpn*ny*e;R{!IL(kc!x#i!cRFuu1ycM=6ZQx0VH5!!^&bba9h##sB;h zDW)^tw13s!FN%ftM;A9vkkb8CLZe^@;WJO?cmJJFfBnru#7B6*h)TD;*{=i9XRl@b o*KZ4#OrvYF^YDSA^IHhsSEhqJl#(j=+eYA#iVm_y+2+;%0W@q08~^|S 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 8b021cda4bfb0..0000000000000 --- a/src/legacy/core_plugins/timelion/public/plugin.ts +++ /dev/null @@ -1,65 +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 } from 'kibana/public'; -import { getTimeChart } from './panels/timechart/timechart'; -import { Panel } from './panels/panel'; -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; - -/** @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() {} - - 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 1fb29de83d3d7..0000000000000 --- 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 8122259f1c991..0000000000000 --- 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/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index 55e492e8f23cd..ed9aa41e835fd 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -1,8 +1,7 @@ { "id": "timelion", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": "timelion", - "ui": false, - "server": true + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["visualizations", "data", "navigation", "visTypeTimelion"] } 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 0000000000000..fe067e1ca3b1b --- /dev/null +++ b/src/plugins/timelion/public/app.js @@ -0,0 +1,617 @@ +/* + * 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 { useTimelionAppState } from './timelion_app_state'; + +import rootTemplate from './index.html'; + +document.title = 'Timelion - Kibana'; + +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(); + + // 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, + }; + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + const { stateContainer, stopStateSync } = useTimelionAppState({ + stateDefaults: getStateDefaults(), + kbnUrlStateStorage, + }); + + $scope.state = stateContainer.getState(); + $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); + + 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('/'); + }, + 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) => { + $scope.state = state; + $scope.opts.state = _.cloneDeep(state); + $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); + }); + + 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) { + stateContainer.transitions.set('rows', newRow); + }); + + $scope.$watch('opts.state.columns', function (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); + }; + + $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(); + const newSheet = _.clone(state.sheet); + newSheet[state.selected] = newExpression; + stateContainer.transitions.set('sheet', newSheet); + }); + + $scope.toggle = function (property) { + $scope[property] = !$scope[property]; + }; + + $scope.changeInterval = function (interval) { + stateContainer.transitions.set('interval', interval); + }; + + $scope.newSheet = function () { + history.push('/'); + }; + + $scope.removeSheet = function (removedIndex) { + const state = stateContainer.getState(); + const newSheet = state.sheet.filter((el, index) => index !== removedIndex); + stateContainer.transitions.set('sheet', newSheet); + }; + + $scope.newCell = function () { + const state = stateContainer.getState(); + const newSheet = [...state.sheet, defaultExpression]; + stateContainer.transitions.updateState({ sheet: newSheet, selected: newSheet.length - 1 }); + $scope.safeSearch(); + }; + + $scope.setActiveCell = function (cell) { + stateContainer.transitions.set('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: _.extend( + { + 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; + _.each(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}`); + } + } + }); + } + + function saveExpression(title) { + savedVisualizations.get({ type: 'timelion' }).then(function (savedExpression) { + const state = stateContainer.getState(); + savedExpression.visState.params = { + expression: state.sheet[state.selected], + interval: state.interval, + }; + savedExpression.title = title; + savedExpression.visState.title = title; + savedExpression.save().then(function (id) { + if (id) { + deps.core.notifications.toasts.addSuccess( + i18n.translate('timelion.saveExpression.successNotificationText', { + defaultMessage: `Saved expression '{title}'`, + values: { title: savedExpression.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 0000000000000..53dc912ebdd89 --- /dev/null +++ b/src/plugins/timelion/public/application.ts @@ -0,0 +1,154 @@ +/* + * 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, + () => deps.mountParams.history + ); + 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 639bd7d65a19e..7939afce412e1 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 5c4bd72ceb708..67e0d595314f6 100644 --- a/src/legacy/core_plugins/timelion/public/directives/fullscreen/fullscreen.js +++ b/src/plugins/timelion/public/components/timelionhelp_tabs_directive.js @@ -17,23 +17,27 @@ * under the License. */ -require('angular-sortable-view'); -require('plugins/timelion/directives/chart/chart'); -require('plugins/timelion/directives/timelion_grid'); +import React from 'react'; +import { TimelionHelpTabs } from './timelionhelp_tabs'; -const app = require('ui/modules').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 f6c329d417f2b..cca9a577f4045 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) { + $scope.onSelect(indexTo); + move($scope.sheet, indexFrom, 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 0000000000000..45e5a0704c37b --- /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 && _.callback(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 0000000000000..f57c391e7fcda --- /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 325c7eabb2b03..194596ba79d0e 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 62% rename from src/legacy/core_plugins/timelion/public/index.scss rename to src/plugins/timelion/public/index.scss index ebf000d160b54..cf2a7859a505d 100644 --- a/src/legacy/core_plugins/timelion/public/index.scss +++ b/src/plugins/timelion/public/index.scss @@ -1,6 +1,3 @@ -// Should import both the EUI constants and any Kibana ones that are considered global -@import 'src/legacy/ui/public/styles/styling_constants'; - /* Timelion plugin styles */ // Prefix all styles with "tim" to avoid conflicts. 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 94% rename from src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts rename to src/plugins/timelion/public/panels/timechart/schema.ts index b1999eb4b483c..cf53ee2431b15 100644 --- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -18,30 +18,36 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../../plugins/vis_type_timelion/public/flot'; +import '../../../../vis_type_timelion/public/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'; +} from '../../../../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'; +import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters'; +import { TimelionVisualizationDependencies } from '../../application'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { xaxisFormatterProvider } from '../../../../../../plugins/vis_type_timelion/public/helpers/xaxis_formatter'; +import { xaxisFormatterProvider } from '../../../../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'; +import { generateTicksProvider } from '../../../../vis_type_timelion/public/helpers/tick_generator'; 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 +205,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 +305,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 4173bfeb331e2..525a994e3121d 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 0000000000000..b2c19e5b4f3b0 --- /dev/null +++ b/src/plugins/timelion/public/plugin.ts @@ -0,0 +1,122 @@ +/* + * 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, + Plugin, + PluginInitializerContext, + DEFAULT_APP_CATEGORIES, + AppMountParameters, + AppUpdater, +} from '../../../core/public'; +import { Panel } from './panels/panel'; +import { initAngularBootstrap } from '../../kibana_legacy/public'; +import { createKbnUrlTracker } from '../../kibana_utils/public'; +import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; +import { VisualizationsStart } from '../../visualizations/public'; +import { VisTypeTimelionPluginStart } from '../../vis_type_timelion/public'; + +export interface TimelionPluginDependencies { + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; + visualizations: VisualizationsStart; + visTypeTimelion: VisTypeTimelionPluginStart; +} + +/** @internal */ +export class TimelionPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking: (() => void) | undefined = undefined; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async 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), + })) + ), + }, + ], + }); + + 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(); + + appMounted(); + + 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 () => { + unmount(); + appUnMounted(); + }; + }, + }); + } + + public start() {} + + 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 4e5aa8d445e7d..0958cce860126 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 0000000000000..a3e7f66d9ee47 --- /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 0000000000000..934ca8d0ce61f --- /dev/null +++ b/src/plugins/timelion/public/timelion_app_state.ts @@ -0,0 +1,67 @@ +/* + * 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 useTimelionAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { + /* + 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, stateDefaults, { replace: true }); + + const stateContainer = createStateContainer( + stateDefaults, + { + 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/legacy/core_plugins/timelion/public/directives/timelion_save_sheet.js b/src/plugins/timelion/public/types.ts similarity index 63% rename from src/legacy/core_plugins/timelion/public/directives/timelion_save_sheet.js rename to src/plugins/timelion/public/types.ts index 6dd44a10dc48c..700485064e41b 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_save_sheet.js +++ b/src/plugins/timelion/public/types.ts @@ -17,14 +17,19 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import saveTemplate from 'plugins/timelion/partials/save_sheet.html'; -const app = uiModules.get('apps/timelion', []); +export interface TimelionAppState { + sheet: string[]; + selected: number; + columns: number; + rows: number; + interval: string; +} -app.directive('timelionSave', function () { - return { - replace: true, - restrict: 'E', - template: saveTemplate, - }; -}); +export interface TimelionAppStateTransitions { + set: ( + state: TimelionAppState + ) => (prop: T, value: TimelionAppState[T]) => TimelionAppState; + updateState: ( + state: TimelionAppState + ) => (newValues: Partial) => TimelionAppState; +} diff --git a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js b/src/plugins/timelion/server/config.ts similarity index 67% rename from src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js rename to src/plugins/timelion/server/config.ts index 7e77027f750c6..16e559761e9ad 100644 --- a/src/legacy/core_plugins/timelion/public/components/timelionhelp_tabs_directive.js +++ b/src/plugins/timelion/server/config.ts @@ -17,14 +17,16 @@ * under the License. */ -import 'ngreact'; +import { schema, TypeOf } from '@kbn/config-schema'; -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/timelion', ['react']); +export const configSchema = { + schema: schema.object({ + graphiteUrls: schema.maybe(schema.arrayOf(schema.string())), + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }), +}; -import { TimelionHelpTabs } from './timelionhelp_tabs'; - -module.directive('timelionHelpTabs', function (reactDirective) { - return reactDirective(wrapInI18nContext(TimelionHelpTabs), undefined, { restrict: 'E' }); -}); +export type TimelionConfigType = TypeOf; diff --git a/src/plugins/timelion/server/index.ts b/src/plugins/timelion/server/index.ts index 5bb0c9e2567e0..28c5709d89132 100644 --- a/src/plugins/timelion/server/index.ts +++ b/src/plugins/timelion/server/index.ts @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { TimelionPlugin } from './plugin'; +import { configSchema, TimelionConfigType } from './config'; -export const plugin = (context: PluginInitializerContext) => new TimelionPlugin(context); +export const config: PluginConfigDescriptor = { + schema: configSchema.schema, +}; + +export const plugin = (context: PluginInitializerContext) => + new TimelionPlugin(context); diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 015f0c573e531..97461cb9f4d95 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -16,12 +16,32 @@ * specific language governing permissions and limitations * under the License. */ + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { TimelionConfigType } from './config'; + +const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { + defaultMessage: 'experimental', +}); export class TimelionPlugin implements Plugin { - constructor(context: PluginInitializerContext) {} + private readonly config$: Observable; - setup(core: CoreSetup) { + constructor(context: PluginInitializerContext) { + this.config$ = context.config.create(); + } + + async setup(core: CoreSetup) { + const config = await this.config$.pipe(first()).toPromise(); + core.capabilities.registerProvider(() => ({ + timelion: { + save: true, + }, + })); core.savedObjects.registerType({ name: 'timelion-sheet', hidden: false, @@ -46,6 +66,130 @@ export class TimelionPlugin implements Plugin { }, }, }); + + core.uiSettings.register({ + 'timelion:showTutorial': { + name: i18n.translate('timelion.uiSettings.showTutorialLabel', { + defaultMessage: 'Show tutorial', + }), + value: false, + description: i18n.translate('timelion.uiSettings.showTutorialDescription', { + defaultMessage: 'Should I show the tutorial by default when entering the timelion app?', + }), + category: ['timelion'], + schema: schema.boolean(), + }, + 'timelion:es.timefield': { + name: i18n.translate('timelion.uiSettings.timeFieldLabel', { + defaultMessage: 'Time field', + }), + value: '@timestamp', + description: i18n.translate('timelion.uiSettings.timeFieldDescription', { + defaultMessage: 'Default field containing a timestamp when using {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:es.default_index': { + name: i18n.translate('timelion.uiSettings.defaultIndexLabel', { + defaultMessage: 'Default index', + }), + value: '_all', + description: i18n.translate('timelion.uiSettings.defaultIndexDescription', { + defaultMessage: 'Default elasticsearch index to search with {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:target_buckets': { + name: i18n.translate('timelion.uiSettings.targetBucketsLabel', { + defaultMessage: 'Target buckets', + }), + value: 200, + description: i18n.translate('timelion.uiSettings.targetBucketsDescription', { + defaultMessage: 'The number of buckets to shoot for when using auto intervals', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:max_buckets': { + name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', { + defaultMessage: 'Maximum buckets', + }), + value: 2000, + description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', { + defaultMessage: 'The maximum number of buckets a single datasource can return', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:default_columns': { + name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', { + defaultMessage: 'Default columns', + }), + value: 2, + description: i18n.translate('timelion.uiSettings.defaultColumnsDescription', { + defaultMessage: 'Number of columns on a timelion sheet by default', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:default_rows': { + name: i18n.translate('timelion.uiSettings.defaultRowsLabel', { + defaultMessage: 'Default rows', + }), + value: 2, + description: i18n.translate('timelion.uiSettings.defaultRowsDescription', { + defaultMessage: 'Number of rows on a timelion sheet by default', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:min_interval': { + name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', { + defaultMessage: 'Minimum interval', + }), + value: '1ms', + description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', { + defaultMessage: 'The smallest interval that will be calculated when using "auto"', + description: + '"auto" is a technical value in that context, that should not be translated.', + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:graphite.url': { + name: i18n.translate('timelion.uiSettings.graphiteURLLabel', { + defaultMessage: 'Graphite URL', + description: + 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', + }), + value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null, + description: i18n.translate('timelion.uiSettings.graphiteURLDescription', { + defaultMessage: + '{experimentalLabel} The URL of your graphite host', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + type: 'select', + options: config.graphiteUrls, + category: ['timelion'], + schema: schema.nullable(schema.string()), + }, + 'timelion:quandl.key': { + name: i18n.translate('timelion.uiSettings.quandlKeyLabel', { + defaultMessage: 'Quandl key', + }), + value: 'someKeyHere', + description: i18n.translate('timelion.uiSettings.quandlKeyDescription', { + defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + category: ['timelion'], + schema: schema.string(), + }, + }); } start() {} stop() {} diff --git a/src/test_utils/public/key_map.ts b/src/test_utils/public/key_map.ts new file mode 100644 index 0000000000000..aac3c6b2db3e0 --- /dev/null +++ b/src/test_utils/public/key_map.ts @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const keyMap: { [key: number]: string } = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause', + 20: 'capsLock', + 27: 'escape', + 32: 'space', + 33: 'pageUp', + 34: 'pageDown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 65: 'a', + 66: 'b', + 67: 'c', + 68: 'd', + 69: 'e', + 70: 'f', + 71: 'g', + 72: 'h', + 73: 'i', + 74: 'j', + 75: 'k', + 76: 'l', + 77: 'm', + 78: 'n', + 79: 'o', + 80: 'p', + 81: 'q', + 82: 'r', + 83: 's', + 84: 't', + 85: 'u', + 86: 'v', + 87: 'w', + 88: 'x', + 89: 'y', + 90: 'z', + 91: 'leftWindowKey', + 92: 'rightWindowKey', + 93: 'selectKey', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: 'multiply', + 107: 'add', + 109: 'subtract', + 110: 'period', + 111: 'divide', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numLock', + 145: 'scrollLock', + 186: 'semiColon', + 187: 'equalSign', + 188: 'comma', + 189: 'dash', + 190: 'period', + 191: 'forwardSlash', + 192: 'graveAccent', + 219: 'openBracket', + 220: 'backSlash', + 221: 'closeBracket', + 222: 'singleQuote', + 224: 'meta', +}; diff --git a/src/test_utils/public/simulate_keys.js b/src/test_utils/public/simulate_keys.js index 56596508a2181..460a75486169a 100644 --- a/src/test_utils/public/simulate_keys.js +++ b/src/test_utils/public/simulate_keys.js @@ -20,7 +20,7 @@ import $ from 'jquery'; import _ from 'lodash'; import Bluebird from 'bluebird'; -import { keyMap } from 'ui/directives/key_map'; +import { keyMap } from './key_map'; const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); /** From 0762ac28c968d7d8c955e4d2f042abf7ea0447b2 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 16 Jun 2020 13:51:45 +0300 Subject: [PATCH 02/14] fixed ci --- src/plugins/saved_objects/public/index.ts | 1 + .../timelion/public/directives/saved_object_finder.js | 2 +- src/plugins/timelion/public/panels/timechart/schema.ts | 6 +----- src/plugins/vis_type_timelion/public/index.ts | 2 ++ 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 4f7a4ff7f196f..9140de316605c 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/public/directives/saved_object_finder.js b/src/plugins/timelion/public/directives/saved_object_finder.js index 96af0a37c13f5..37a8c11dd22de 100644 --- a/src/plugins/timelion/public/directives/saved_object_finder.js +++ b/src/plugins/timelion/public/directives/saved_object_finder.js @@ -25,7 +25,7 @@ import { PaginateControlsDirectiveProvider, PaginateDirectiveProvider, } from '../../../kibana_legacy/public'; -import { PER_PAGE_SETTING } from '../../../saved_objects/common'; +import { PER_PAGE_SETTING } from '../../../saved_objects/public'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; export function initSavedObjectFinderDirective(app, savedSheetLoader, uiSettings) { diff --git a/src/plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts index cf53ee2431b15..c008c807af502 100644 --- a/src/plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -24,11 +24,7 @@ import $ from 'jquery'; import moment from 'moment-timezone'; // @ts-ignore import observeResize from '../../lib/observe_resize'; -import { - calculateInterval, - DEFAULT_TIME_FORMAT, - // @ts-ignore -} from '../../../../vis_type_timelion/common/lib'; +import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../../vis_type_timelion/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters'; import { TimelionVisualizationDependencies } from '../../application'; diff --git a/src/plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts index 0aa5f3a810033..d768f59c4e6f7 100644 --- a/src/plugins/vis_type_timelion/public/index.ts +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -26,4 +26,6 @@ export function plugin(initializerContext: PluginInitializerContext) { export { getTimezone } from './helpers/get_timezone'; +export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib'; + export { VisTypeTimelionPluginStart } from './plugin'; From 3ddf24638bf7adb0a262503aaa6bd7145b2cb1a0 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 17 Jun 2020 17:49:06 +0300 Subject: [PATCH 03/14] Fixed paths --- .eslintignore | 1 + src/plugins/timelion/public/flot.js | 26 + .../public/panels/timechart/schema.ts | 17 +- .../webpackShims/jquery.flot.axislabels.js | 462 +++ .../webpackShims/jquery.flot.crosshair.js | 176 + .../public/webpackShims/jquery.flot.js | 3168 +++++++++++++++++ .../webpackShims/jquery.flot.selection.js | 360 ++ .../public/webpackShims/jquery.flot.stack.js | 188 + .../public/webpackShims/jquery.flot.symbol.js | 71 + .../public/webpackShims/jquery.flot.time.js | 432 +++ src/plugins/vis_type_timelion/public/index.ts | 3 + 11 files changed, 4895 insertions(+), 9 deletions(-) create mode 100644 src/plugins/timelion/public/flot.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.axislabels.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.crosshair.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.selection.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.stack.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js create mode 100644 src/plugins/timelion/public/webpackShims/jquery.flot.time.js diff --git a/.eslintignore b/.eslintignore index fbdd70703f3c4..d81de5b2db60d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,6 +25,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/src/plugins/timelion/public/flot.js b/src/plugins/timelion/public/flot.js new file mode 100644 index 0000000000000..1ccb40c93a3d6 --- /dev/null +++ b/src/plugins/timelion/public/flot.js @@ -0,0 +1,26 @@ +/* + * 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 './webpackShims/jquery.flot'; +import './webpackShims/jquery.flot.time'; +import './webpackShims/jquery.flot.symbol'; +import './webpackShims/jquery.flot.crosshair'; +import './webpackShims/jquery.flot.selection'; +import './webpackShims/jquery.flot.stack'; +import './webpackShims/jquery.flot.axislabels'; diff --git a/src/plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts index c008c807af502..73a007260f498 100644 --- a/src/plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -17,21 +17,20 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../vis_type_timelion/public/flot'; +import '../../flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; // @ts-ignore import observeResize from '../../lib/observe_resize'; -import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../../vis_type_timelion/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters'; +import { + calculateInterval, + DEFAULT_TIME_FORMAT, + tickFormatters, + xaxisFormatterProvider, + generateTicksProvider, +} from '../../../../vis_type_timelion/public'; import { TimelionVisualizationDependencies } from '../../application'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { xaxisFormatterProvider } from '../../../../vis_type_timelion/public/helpers/xaxis_formatter'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { generateTicksProvider } from '../../../../vis_type_timelion/public/helpers/tick_generator'; const DEBOUNCE_DELAY = 50; 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 0000000000000..cda8038953c76 --- /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.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(elem); + // store height and width of label itself, for use in draw() + this.labelWidth = elem.outerWidth(true); + this.labelHeight = elem.outerHeight(true); + elem.remove(); + + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelWidth + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + HtmlAxisLabel.prototype.cleanup = function() { + if (this.elem) { + this.elem.remove(); + } + }; + + HtmlAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); + this.elem = $('
' + + this.opts.axisLabel + '
'); + this.plot.getPlaceholder().append(this.elem); + if (this.position == 'top') { + this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + + 'px'); + this.elem.css('top', box.top + 'px'); + } else if (this.position == 'bottom') { + this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + + 'px'); + this.elem.css('top', box.top + box.height - this.labelHeight + + 'px'); + } else if (this.position == 'left') { + this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + + 'px'); + this.elem.css('left', box.left + 'px'); + } else if (this.position == 'right') { + this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + + 'px'); + this.elem.css('left', box.left + box.width - this.labelWidth + + 'px'); + } + }; + + + CssTransformAxisLabel.prototype = new HtmlAxisLabel(); + CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; + function CssTransformAxisLabel(axisName, position, padding, plot, opts) { + HtmlAxisLabel.prototype.constructor.call(this, axisName, position, + padding, plot, opts); + } + + CssTransformAxisLabel.prototype.calculateSize = function() { + HtmlAxisLabel.prototype.calculateSize.call(this); + this.width = this.height = 0; + if (this.position == 'left' || this.position == 'right') { + this.width = this.labelHeight + this.padding; + } else { + this.height = this.labelHeight + this.padding; + } + }; + + CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { + var stransforms = { + '-moz-transform': '', + '-webkit-transform': '', + '-o-transform': '', + '-ms-transform': '' + }; + if (x != 0 || y != 0) { + var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; + stransforms['-moz-transform'] += stdTranslate; + stransforms['-webkit-transform'] += stdTranslate; + stransforms['-o-transform'] += stdTranslate; + stransforms['-ms-transform'] += stdTranslate; + } + if (degrees != 0) { + var rotation = degrees / 90; + var stdRotate = ' rotate(' + degrees + 'deg)'; + stransforms['-moz-transform'] += stdRotate; + stransforms['-webkit-transform'] += stdRotate; + stransforms['-o-transform'] += stdRotate; + stransforms['-ms-transform'] += stdRotate; + } + var s = 'top: 0; left: 0; '; + for (var prop in stransforms) { + if (stransforms[prop]) { + s += prop + ':' + stransforms[prop] + ';'; + } + } + s += ';'; + return s; + }; + + CssTransformAxisLabel.prototype.calculateOffsets = function(box) { + var offsets = { x: 0, y: 0, degrees: 0 }; + if (this.position == 'bottom') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top + box.height - this.labelHeight; + } else if (this.position == 'top') { + offsets.x = box.left + box.width/2 - this.labelWidth/2; + offsets.y = box.top; + } else if (this.position == 'left') { + offsets.degrees = -90; + offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } else if (this.position == 'right') { + offsets.degrees = 90; + offsets.x = box.left + box.width - this.labelWidth/2 + - this.labelHeight/2; + offsets.y = box.height/2 + box.top; + } + offsets.x = Math.round(offsets.x); + offsets.y = Math.round(offsets.y); + + return offsets; + }; + + CssTransformAxisLabel.prototype.draw = function(box) { + this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); + var offsets = this.calculateOffsets(box); + this.elem = $('
' + this.opts.axisLabel + '
'); + 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 0000000000000..5111695e3d12c --- /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 0000000000000..5d613037cf234 --- /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 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of columns in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js b/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js new file mode 100644 index 0000000000000..c8707b30f4e6f --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.selection.js @@ -0,0 +1,360 @@ +/* Flot plugin for selecting regions of a plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + +selection: { + mode: null or "x" or "y" or "xy", + color: color, + shape: "round" or "miter" or "bevel", + minSize: number of pixels +} + +Selection support is enabled by setting the mode to one of "x", "y" or "xy". +In "x" mode, the user will only be able to specify the x range, similarly for +"y" mode. For "xy", the selection becomes a rectangle where both ranges can be +specified. "color" is color of the selection (if you need to change the color +later on, you can get to it with plot.getOptions().selection.color). "shape" +is the shape of the corners of the selection. + +"minSize" is the minimum size a selection can be in pixels. This value can +be customized to determine the smallest size a selection can be and still +have the selection rectangle be displayed. When customizing this value, the +fact that it refers to pixels, not axis units must be taken into account. +Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 +minute, setting "minSize" to 1 will not make the minimum selection size 1 +minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent +"plotunselected" events from being fired when the user clicks the mouse without +dragging. + +When selection support is enabled, a "plotselected" event will be emitted on +the DOM element you passed into the plot function. The event handler gets a +parameter with the ranges selected on the axes, like this: + + placeholder.bind( "plotselected", function( event, ranges ) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished making the +selection. A "plotselecting" event is fired during the process with the same +parameters as the "plotselected" event, in case you want to know what's +happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user clicks the +mouse to remove the selection. As stated above, setting "minSize" to 0 will +destroy this behavior. + +The plugin also adds the following methods to the plot object: + +- setSelection( ranges, preventEvent ) + + Set the selection rectangle. The passed in ranges is on the same form as + returned in the "plotselected" event. If the selection mode is "x", you + should put in either an xaxis range, if the mode is "y" you need to put in + an yaxis range and both xaxis and yaxis if the selection mode is "xy", like + this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If you don't + want that to happen, e.g. if you're inside a "plotselected" handler, pass + true as the second parameter. If you are using multiple axes, you can + specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of + xaxis, the plugin picks the first one it sees. + +- clearSelection( preventEvent ) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the "plotselected" + event. If there's currently no selection, the function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, + active: false + }; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + var mouseUpHandler = null; + + function onMouseMove(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + + // this is a bit silly, but we have to use a closure to be + // able to whack the same handler again + mouseUpHandler = function (e) { onMouseUp(e); }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp(e) { + mouseUpHandler = null; + + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = savedhandlers.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = savedhandlers.ondrag; + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) + triggerSelectedEvent(); + else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) + return null; + + if (!selection.show) return null; + + var r = {}, c1 = selection.first, c2 = selection.second; + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + function setSelectionPos(pos, e) { + var o = plot.getOptions(); + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (o.selection.mode == "y") + pos.x = pos == selection.first ? 0 : plot.width(); + + if (o.selection.mode == "x") + pos.y = pos == selection.first ? 0 : plot.height(); + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } + else + clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + plot.triggerRedrawOverlay(); + if (!preventEvent) + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var axis, range, o = plot.getOptions(); + + if (o.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (o.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = plot.getOptions().selection.minSize; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + eventHolder.mousemove(onMouseMove); + eventHolder.mousedown(onMouseDown); + } + }); + + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + + ctx.strokeStyle = c.scale('a', 0.8).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = o.selection.shape; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x) + 0.5, + y = Math.min(selection.first.y, selection.second.y) + 0.5, + w = Math.abs(selection.second.x - selection.first.x) - 1, + h = Math.abs(selection.second.y - selection.first.y) - 1; + + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mousedown", onMouseDown); + + if (mouseUpHandler) + $(document).unbind("mouseup", mouseUpHandler); + }); + + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac", + shape: "round", // one of "round", "miter", or "bevel" + minSize: 5 // minimum number of pixels + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js b/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js new file mode 100644 index 0000000000000..0d91c0f3c0160 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlaying them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js b/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js new file mode 100644 index 0000000000000..79f634971b6fa --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.symbol.js @@ -0,0 +1,71 @@ +/* Flot plugin that adds some extra symbols for plotting points. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The symbols are accessed as strings through the standard symbol options: + + series: { + points: { + symbol: "square" // or "diamond", "triangle", "cross" + } + } + +*/ + +(function ($) { + function processRawData(plot, series, datapoints) { + // we normalize the area of each symbol so it is approximately the + // same as a circle of the given radius + + var handlers = { + square: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + diamond: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + }, + triangle: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) + var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); + var height = size * Math.sin(Math.PI / 3); + ctx.moveTo(x - size/2, y + height/2); + ctx.lineTo(x + size/2, y + height/2); + if (!shadow) { + ctx.lineTo(x, y - height/2); + ctx.lineTo(x - size/2, y + height/2); + } + }, + cross: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); + } + }; + + var s = series.points.symbol; + if (handlers[s]) + series.points.symbol = handlers[s]; + } + + function init(plot) { + plot.hooks.processDatapoints.push(processRawData); + } + + $.plot.plugins.push({ + init: init, + name: 'symbols', + version: '1.0' + }); +})(jQuery); diff --git a/src/plugins/timelion/public/webpackShims/jquery.flot.time.js b/src/plugins/timelion/public/webpackShims/jquery.flot.time.js new file mode 100644 index 0000000000000..34c1d121259a2 --- /dev/null +++ b/src/plugins/timelion/public/webpackShims/jquery.flot.time.js @@ -0,0 +1,432 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + } + + if (dayNames == null) { + dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + $.plot.dateGenerator = dateGenerator; + +})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts index d768f59c4e6f7..abfe345d8c672 100644 --- a/src/plugins/vis_type_timelion/public/index.ts +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -25,6 +25,9 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { getTimezone } from './helpers/get_timezone'; +export { tickFormatters } from './helpers/tick_formatters'; +export { xaxisFormatterProvider } from './helpers/xaxis_formatter'; +export { generateTicksProvider } from './helpers/tick_generator'; export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib'; From 2c07e0e0573bb9db20dfb807370ca325ef6890ed Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 17 Jun 2020 18:13:02 +0300 Subject: [PATCH 04/14] fixed UI settings --- src/plugins/timelion/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 97461cb9f4d95..72daf5b96d2b8 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -173,7 +173,7 @@ export class TimelionPlugin implements Plugin { values: { experimentalLabel: `[${experimentalLabel}]` }, }), type: 'select', - options: config.graphiteUrls, + options: config.graphiteUrls || [], category: ['timelion'], schema: schema.nullable(schema.string()), }, From 80746ab8836ddad9a34d2728c9a65e7f3ae961d9 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 19 Jun 2020 17:28:07 +0300 Subject: [PATCH 05/14] Fixed ci --- src/plugins/kibana_legacy/public/index.scss | 3 +++ src/plugins/kibana_legacy/public/index.ts | 2 ++ test/functional/apps/bundles/index.js | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/plugins/kibana_legacy/public/index.scss diff --git a/src/plugins/kibana_legacy/public/index.scss b/src/plugins/kibana_legacy/public/index.scss new file mode 100644 index 0000000000000..0992ec0df5610 --- /dev/null +++ b/src/plugins/kibana_legacy/public/index.scss @@ -0,0 +1,3 @@ +// Importing from css was intentional because font-awesome.scss requries "file-loader" for loading fonts. +// As we temporary using font-awesome icons in the new platform and we don't want change webpack config for tempoprary library. +@import "~font-awesome/css/font-awesome.min.css"; \ No newline at end of file diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 75e81b0505747..de20b4fd2e2e1 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index ead6412564751..d0a411cb76fcf 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -63,7 +63,8 @@ export default function ({ getService }) { .expect(200) .expect('Content-Encoding', 'gzip')); - it('returns gzip files when no brotli version exists', () => + // commons.style.css file is no longer generated since we don't have any common styles. + it.skip('returns gzip files when no brotli version exists', () => supertest .get(`/${buildNum}/bundles/commons.style.css`) // legacy optimizer does not create brotli outputs .set('Accept-Encoding', 'gzip, br') From 2eb9702e8fb81dcf3ff74a285a9ea662c6c46b3f Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 22 Jun 2020 20:07:48 +0300 Subject: [PATCH 06/14] fix CI --- src/optimize/base_optimizer.js | 1 - src/plugins/kibana_legacy/public/index.scss | 3 --- src/plugins/kibana_legacy/public/index.ts | 2 -- test/functional/apps/bundles/index.js | 3 +-- 4 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 src/plugins/kibana_legacy/public/index.scss diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 12131b89e03c1..cf943456526d0 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -270,7 +270,6 @@ export default class BaseOptimizer { name: 'commons', chunks: (chunk) => chunk.canBeInitial() && chunk.name !== 'light_theme' && chunk.name !== 'dark_theme', - minChunks: 2, reuseExistingChunk: true, }, light_theme: { diff --git a/src/plugins/kibana_legacy/public/index.scss b/src/plugins/kibana_legacy/public/index.scss deleted file mode 100644 index 0992ec0df5610..0000000000000 --- a/src/plugins/kibana_legacy/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Importing from css was intentional because font-awesome.scss requries "file-loader" for loading fonts. -// As we temporary using font-awesome icons in the new platform and we don't want change webpack config for tempoprary library. -@import "~font-awesome/css/font-awesome.min.css"; \ No newline at end of file diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index de20b4fd2e2e1..75e81b0505747 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import './index.scss'; - import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index d0a411cb76fcf..ead6412564751 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -63,8 +63,7 @@ export default function ({ getService }) { .expect(200) .expect('Content-Encoding', 'gzip')); - // commons.style.css file is no longer generated since we don't have any common styles. - it.skip('returns gzip files when no brotli version exists', () => + it('returns gzip files when no brotli version exists', () => supertest .get(`/${buildNum}/bundles/commons.style.css`) // legacy optimizer does not create brotli outputs .set('Accept-Encoding', 'gzip, br') From 3d96386b7088129a9fc1406d919bb6673c663271 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 26 Jun 2020 10:51:30 +0300 Subject: [PATCH 07/14] Fixed some comments --- src/plugins/timelion/kibana.json | 2 +- src/plugins/timelion/public/app.js | 46 +++++++++++++++------------ src/plugins/timelion/public/plugin.ts | 7 ++-- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index ed9aa41e835fd..1072a1edd5bf1 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["visualizations", "data", "navigation", "visTypeTimelion"] + "requiredPlugins": ["visualizations", "data", "navigation", "visTypeTimelion", "kibanaLegacy"] } diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index fe067e1ca3b1b..42bf2a4a8bac0 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -146,7 +146,7 @@ export function initTimelionApp(app, deps) { kbnUrlStateStorage, }); - $scope.state = stateContainer.getState(); + $scope.state = _.cloneDeep(stateContainer.getState()); $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); const savedVisualizations = deps.plugins.visualizations.savedVisualizationsLoader; @@ -348,8 +348,8 @@ export function initTimelionApp(app, deps) { }; const unsubscribeStateUpdates = stateContainer.subscribe((state) => { - $scope.state = state; - $scope.opts.state = _.cloneDeep(state); + $scope.state = _.cloneDeep(state); + $scope.opts.state = $scope.state; $scope.expression = _.clone($scope.state.sheet[$scope.state.selected]); }); @@ -407,6 +407,7 @@ export function initTimelionApp(app, deps) { ...dateRange, }; timefilter.setTime(dateRange); + if (!$scope.running) $scope.search(); }; $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { @@ -456,6 +457,7 @@ export function initTimelionApp(app, deps) { const state = stateContainer.getState(); const newSheet = state.sheet.filter((el, index) => index !== removedIndex); stateContainer.transitions.set('sheet', newSheet); + $scope.search(); }; $scope.newCell = function () { @@ -546,25 +548,27 @@ export function initTimelionApp(app, deps) { } function saveExpression(title) { - savedVisualizations.get({ type: 'timelion' }).then(function (savedExpression) { - const state = stateContainer.getState(); - savedExpression.visState.params = { - expression: state.sheet[state.selected], - interval: state.interval, - }; - savedExpression.title = title; - savedExpression.visState.title = title; - savedExpression.save().then(function (id) { - if (id) { - deps.core.notifications.toasts.addSuccess( - i18n.translate('timelion.saveExpression.successNotificationText', { - defaultMessage: `Saved expression '{title}'`, - values: { title: savedExpression.title }, - }) - ); - } + savedVisualizations + .get({ type: 'timelion', searchSource: true }) + .then(function (savedExpression) { + const state = stateContainer.getState(); + savedExpression.visState.params = { + expression: state.sheet[state.selected], + interval: state.interval, + }; + savedExpression.title = title; + savedExpression.visState.title = title; + savedExpression.save().then(function (id) { + if (id) { + deps.core.notifications.toasts.addSuccess( + i18n.translate('timelion.saveExpression.successNotificationText', { + defaultMessage: `Saved expression '{title}'`, + values: { title: savedExpression.title }, + }) + ); + } + }); }); - }); } init(); diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index b2c19e5b4f3b0..7a9db819d3271 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -21,6 +21,7 @@ import { BehaviorSubject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; import { CoreSetup, + CoreStart, Plugin, PluginInitializerContext, DEFAULT_APP_CATEGORIES, @@ -28,7 +29,7 @@ import { AppUpdater, } from '../../../core/public'; import { Panel } from './panels/panel'; -import { initAngularBootstrap } from '../../kibana_legacy/public'; +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'; @@ -112,7 +113,9 @@ export class TimelionPlugin implements Plugin, void> { }); } - public start() {} + public start(core: CoreStart, { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart }) { + kibanaLegacy.loadFontAwesome(); + } public stop(): void { if (this.stopUrlTracking) { From 09c3cd446f9365b05a28e078f8377353fdcec2fa Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 30 Jun 2020 11:31:59 +0300 Subject: [PATCH 08/14] Fixed browser tests --- src/legacy/ui/public/state_management/__tests__/state.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js index cde123e6c1d85..b6c705e814509 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; From a3832b6146231636f3d352e97fa503d06b2a29dd Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 1 Jul 2020 15:04:25 +0300 Subject: [PATCH 09/14] fixed state --- src/plugins/timelion/public/app.js | 47 ++++++++++++++++++++++---- src/plugins/timelion/public/index.html | 4 +-- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 42bf2a4a8bac0..71e93463ef648 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -148,6 +148,7 @@ export function initTimelionApp(app, deps) { $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); @@ -348,9 +349,14 @@ export function initTimelionApp(app, deps) { }; const unsubscribeStateUpdates = stateContainer.subscribe((state) => { - $scope.state = _.cloneDeep(state); - $scope.opts.state = $scope.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); @@ -438,7 +444,17 @@ export function initTimelionApp(app, deps) { const state = stateContainer.getState(); const newSheet = _.clone(state.sheet); newSheet[state.selected] = newExpression; - stateContainer.transitions.set('sheet', newSheet); + 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) { @@ -446,7 +462,22 @@ export function initTimelionApp(app, deps) { }; $scope.changeInterval = function (interval) { - stateContainer.transitions.set('interval', 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 () { @@ -456,15 +487,17 @@ export function initTimelionApp(app, deps) { $scope.removeSheet = function (removedIndex) { const state = stateContainer.getState(); const newSheet = state.sheet.filter((el, index) => index !== removedIndex); - stateContainer.transitions.set('sheet', newSheet); - $scope.search(); + $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.safeSearch(); }; $scope.setActiveCell = function (cell) { diff --git a/src/plugins/timelion/public/index.html b/src/plugins/timelion/public/index.html index 4266f12ee723d..54efae7f81ba7 100644 --- a/src/plugins/timelion/public/index.html +++ b/src/plugins/timelion/public/index.html @@ -39,14 +39,14 @@
From e2e7d150f82570d9c8ba93450b4a6a2b0a4cad18 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 6 Jul 2020 18:06:21 +0300 Subject: [PATCH 10/14] Fixed comments --- src/plugins/timelion/public/app.js | 52 +++++---- .../timelion/public/directives/cells/cells.js | 2 +- .../public/directives/cells/collection.ts | 2 +- .../public/directives/saved_object_finder.js | 6 +- .../directives/timelion_expression_input.js | 2 +- .../timelion/public/timelion_app_state.ts | 12 +- src/plugins/timelion/server/plugin.ts | 103 +----------------- .../vis_type_timelion/server/plugin.ts | 97 ++++++++++++++++- 8 files changed, 143 insertions(+), 133 deletions(-) diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 71e93463ef648..07ab6058563ce 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -49,12 +49,10 @@ import { TimelionInterval } from './directives/timelion_interval/timelion_interv import { timelionExpInput } from './directives/timelion_expression_input'; import { TimelionExpressionSuggestions } from './directives/timelion_expression_suggestions/timelion_expression_suggestions'; import { initSavedSheetService } from './services/saved_sheets'; -import { useTimelionAppState } from './timelion_app_state'; +import { initTimelionAppState } from './timelion_app_state'; import rootTemplate from './index.html'; -document.title = 'Timelion - Kibana'; - export function initTimelionApp(app, deps) { app.run(registerListenEventListener); @@ -122,6 +120,8 @@ export function initTimelionApp(app, deps) { 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, @@ -140,8 +140,7 @@ export function initTimelionApp(app, deps) { }; } - // eslint-disable-next-line react-hooks/rules-of-hooks - const { stateContainer, stopStateSync } = useTimelionAppState({ + const { stateContainer, stopStateSync } = initTimelionAppState({ stateDefaults: getStateDefaults(), kbnUrlStateStorage, }); @@ -375,11 +374,17 @@ export function initTimelionApp(app, deps) { }; $scope.$watch('opts.state.rows', function (newRow) { - stateContainer.transitions.set('rows', newRow); + const state = stateContainer.getState(); + if (state.rows !== newRow) { + stateContainer.transitions.set('rows', newRow); + } }); $scope.$watch('opts.state.columns', function (newColumn) { - stateContainer.transitions.set('columns', newColumn); + const state = stateContainer.getState(); + if (state.columns !== newColumn) { + stateContainer.transitions.set('columns', newColumn); + } }); $scope.menus = { @@ -442,18 +447,18 @@ export function initTimelionApp(app, deps) { $scope.$watch('expression', function (newExpression) { const state = stateContainer.getState(); - const newSheet = _.clone(state.sheet); - newSheet[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, - }); + 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, + }); + } } }); @@ -501,7 +506,10 @@ export function initTimelionApp(app, deps) { }; $scope.setActiveCell = function (cell) { - stateContainer.transitions.set('selected', cell); + const state = stateContainer.getState(); + if (state.selected !== cell) { + stateContainer.transitions.updateState({ sheet: $scope.state.sheet, selected: cell }); + } }; $scope.search = function () { @@ -514,7 +522,7 @@ export function initTimelionApp(app, deps) { const httpResult = $http .post('../api/timelion/run', { sheet: state.sheet, - time: _.extend( + time: _.assignIn( { from: timeRangeBounds.min, to: timeRangeBounds.max, @@ -534,7 +542,7 @@ export function initTimelionApp(app, deps) { .then(function (resp) { $scope.stats = resp.stats; $scope.sheet = resp.sheet; - _.each(resp.sheet, function (cell) { + _.forEach(resp.sheet, function (cell) { if (cell.exception && cell.plot !== state.selected) { stateContainer.transitions.set('selected', cell.plot); } diff --git a/src/plugins/timelion/public/directives/cells/cells.js b/src/plugins/timelion/public/directives/cells/cells.js index cca9a577f4045..36a1e80dd470e 100644 --- a/src/plugins/timelion/public/directives/cells/cells.js +++ b/src/plugins/timelion/public/directives/cells/cells.js @@ -43,8 +43,8 @@ export function initCellsDirective(app) { }; $scope.dropCell = function (item, partFrom, partTo, indexFrom, indexTo) { - $scope.onSelect(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 index 45e5a0704c37b..b882a2bbe6e5b 100644 --- a/src/plugins/timelion/public/directives/cells/collection.ts +++ b/src/plugins/timelion/public/directives/cells/collection.ts @@ -50,7 +50,7 @@ export function move( } below = !!below; - qualifier = qualifier && _.callback(qualifier); + qualifier = qualifier && _.iteratee(qualifier); const above = !below; const finder = below ? _.findIndex : _.findLastIndex; diff --git a/src/plugins/timelion/public/directives/saved_object_finder.js b/src/plugins/timelion/public/directives/saved_object_finder.js index 37a8c11dd22de..88f1b78cd3da5 100644 --- a/src/plugins/timelion/public/directives/saved_object_finder.js +++ b/src/plugins/timelion/public/directives/saved_object_finder.js @@ -97,8 +97,8 @@ export function initSavedObjectFinderDirective(app, savedSheetLoader, uiSettings self.sortHits = function (hits) { self.isAscending = !self.isAscending; self.hits = self.isAscending - ? _.sortBy(hits, 'title') - : _.sortBy(hits, 'title').reverse(); + ? _.sortBy(hits, ['title']) + : _.sortBy(hits, ['title']).reverse(); }; /** @@ -304,7 +304,7 @@ export function initSavedObjectFinderDirective(app, savedSheetLoader, uiSettings // as we can't really cancel requests if (currentFilter === filter) { self.hitCount = hits.total; - self.hits = _.sortBy(hits.hits, 'title'); + self.hits = _.sortBy(hits.hits, ['title']); } }); } diff --git a/src/plugins/timelion/public/directives/timelion_expression_input.js b/src/plugins/timelion/public/directives/timelion_expression_input.js index ef81959e42fd2..286c0839d26ee 100644 --- a/src/plugins/timelion/public/directives/timelion_expression_input.js +++ b/src/plugins/timelion/public/directives/timelion_expression_input.js @@ -78,7 +78,7 @@ export function timelionExpInput(deps) { function init() { $http.get('../api/timelion/functions').then(function (resp) { Object.assign(functionReference, { - byName: _.indexBy(resp.data, 'name'), + byName: _.keyBy(resp.data, 'name'), list: resp.data, }); }); diff --git a/src/plugins/timelion/public/timelion_app_state.ts b/src/plugins/timelion/public/timelion_app_state.ts index 934ca8d0ce61f..43382adbf8f80 100644 --- a/src/plugins/timelion/public/timelion_app_state.ts +++ b/src/plugins/timelion/public/timelion_app_state.ts @@ -28,7 +28,13 @@ interface Arguments { stateDefaults: TimelionAppState; } -export function useTimelionAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { +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, @@ -36,10 +42,10 @@ export function useTimelionAppState({ stateDefaults, kbnUrlStateStorage }: Argum 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, stateDefaults, { replace: true }); + kbnUrlStateStorage.set(STATE_STORAGE_KEY, initialState, { replace: true }); const stateContainer = createStateContainer( - stateDefaults, + initialState, { set: (state) => (prop, value) => ({ ...state, [prop]: value }), updateState: (state) => (newValues) => ({ ...state, ...newValues }), diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index 72daf5b96d2b8..3e4cd5467dd44 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -17,26 +17,15 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { TimelionConfigType } from './config'; -const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { - defaultMessage: 'experimental', -}); - export class TimelionPlugin implements Plugin { - private readonly config$: Observable; - - constructor(context: PluginInitializerContext) { - this.config$ = context.config.create(); - } + constructor(context: PluginInitializerContext) {} - async setup(core: CoreSetup) { - const config = await this.config$.pipe(first()).toPromise(); + public setup(core: CoreSetup) { core.capabilities.registerProvider(() => ({ timelion: { save: true, @@ -79,52 +68,6 @@ export class TimelionPlugin implements Plugin { category: ['timelion'], schema: schema.boolean(), }, - 'timelion:es.timefield': { - name: i18n.translate('timelion.uiSettings.timeFieldLabel', { - defaultMessage: 'Time field', - }), - value: '@timestamp', - description: i18n.translate('timelion.uiSettings.timeFieldDescription', { - defaultMessage: 'Default field containing a timestamp when using {esParam}', - values: { esParam: '.es()' }, - }), - category: ['timelion'], - schema: schema.string(), - }, - 'timelion:es.default_index': { - name: i18n.translate('timelion.uiSettings.defaultIndexLabel', { - defaultMessage: 'Default index', - }), - value: '_all', - description: i18n.translate('timelion.uiSettings.defaultIndexDescription', { - defaultMessage: 'Default elasticsearch index to search with {esParam}', - values: { esParam: '.es()' }, - }), - category: ['timelion'], - schema: schema.string(), - }, - 'timelion:target_buckets': { - name: i18n.translate('timelion.uiSettings.targetBucketsLabel', { - defaultMessage: 'Target buckets', - }), - value: 200, - description: i18n.translate('timelion.uiSettings.targetBucketsDescription', { - defaultMessage: 'The number of buckets to shoot for when using auto intervals', - }), - category: ['timelion'], - schema: schema.number(), - }, - 'timelion:max_buckets': { - name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', { - defaultMessage: 'Maximum buckets', - }), - value: 2000, - description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', { - defaultMessage: 'The maximum number of buckets a single datasource can return', - }), - category: ['timelion'], - schema: schema.number(), - }, 'timelion:default_columns': { name: i18n.translate('timelion.uiSettings.defaultColumnsLabel', { defaultMessage: 'Default columns', @@ -147,48 +90,6 @@ export class TimelionPlugin implements Plugin { category: ['timelion'], schema: schema.number(), }, - 'timelion:min_interval': { - name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', { - defaultMessage: 'Minimum interval', - }), - value: '1ms', - description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', { - defaultMessage: 'The smallest interval that will be calculated when using "auto"', - description: - '"auto" is a technical value in that context, that should not be translated.', - }), - category: ['timelion'], - schema: schema.string(), - }, - 'timelion:graphite.url': { - name: i18n.translate('timelion.uiSettings.graphiteURLLabel', { - defaultMessage: 'Graphite URL', - description: - 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', - }), - value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null, - description: i18n.translate('timelion.uiSettings.graphiteURLDescription', { - defaultMessage: - '{experimentalLabel} The URL of your graphite host', - values: { experimentalLabel: `[${experimentalLabel}]` }, - }), - type: 'select', - options: config.graphiteUrls || [], - category: ['timelion'], - schema: schema.nullable(schema.string()), - }, - 'timelion:quandl.key': { - name: i18n.translate('timelion.uiSettings.quandlKeyLabel', { - defaultMessage: 'Quandl key', - }), - value: 'someKeyHere', - description: i18n.translate('timelion.uiSettings.quandlKeyDescription', { - defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', - values: { experimentalLabel: `[${experimentalLabel}]` }, - }), - category: ['timelion'], - schema: schema.string(), - }, }); } start() {} diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 605c6be0a85df..5e6557e305692 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { TypeOf } from '@kbn/config-schema'; +import { TypeOf, schema } from '@kbn/config-schema'; import { RecursiveReadonly } from '@kbn/utility-types'; import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; @@ -31,6 +31,10 @@ import { validateEsRoute } from './routes/validate_es'; import { runRoute } from './routes/run'; import { ConfigManager } from './lib/config_manager'; +const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', { + defaultMessage: 'experimental', +}); + /** * Describes public Timelion plugin contract returned at the `setup` stage. */ @@ -82,6 +86,97 @@ export class Plugin { runRoute(router, deps); validateEsRoute(router); + core.uiSettings.register({ + 'timelion:es.timefield': { + name: i18n.translate('timelion.uiSettings.timeFieldLabel', { + defaultMessage: 'Time field', + }), + value: '@timestamp', + description: i18n.translate('timelion.uiSettings.timeFieldDescription', { + defaultMessage: 'Default field containing a timestamp when using {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:es.default_index': { + name: i18n.translate('timelion.uiSettings.defaultIndexLabel', { + defaultMessage: 'Default index', + }), + value: '_all', + description: i18n.translate('timelion.uiSettings.defaultIndexDescription', { + defaultMessage: 'Default elasticsearch index to search with {esParam}', + values: { esParam: '.es()' }, + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:target_buckets': { + name: i18n.translate('timelion.uiSettings.targetBucketsLabel', { + defaultMessage: 'Target buckets', + }), + value: 200, + description: i18n.translate('timelion.uiSettings.targetBucketsDescription', { + defaultMessage: 'The number of buckets to shoot for when using auto intervals', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:max_buckets': { + name: i18n.translate('timelion.uiSettings.maximumBucketsLabel', { + defaultMessage: 'Maximum buckets', + }), + value: 2000, + description: i18n.translate('timelion.uiSettings.maximumBucketsDescription', { + defaultMessage: 'The maximum number of buckets a single datasource can return', + }), + category: ['timelion'], + schema: schema.number(), + }, + 'timelion:min_interval': { + name: i18n.translate('timelion.uiSettings.minimumIntervalLabel', { + defaultMessage: 'Minimum interval', + }), + value: '1ms', + description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', { + defaultMessage: 'The smallest interval that will be calculated when using "auto"', + description: + '"auto" is a technical value in that context, that should not be translated.', + }), + category: ['timelion'], + schema: schema.string(), + }, + 'timelion:graphite.url': { + name: i18n.translate('timelion.uiSettings.graphiteURLLabel', { + defaultMessage: 'Graphite URL', + description: + 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite', + }), + value: config.graphiteUrls && config.graphiteUrls.length ? config.graphiteUrls[0] : null, + description: i18n.translate('timelion.uiSettings.graphiteURLDescription', { + defaultMessage: + '{experimentalLabel} The URL of your graphite host', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + type: 'select', + options: config.graphiteUrls || [], + category: ['timelion'], + schema: schema.nullable(schema.string()), + }, + 'timelion:quandl.key': { + name: i18n.translate('timelion.uiSettings.quandlKeyLabel', { + defaultMessage: 'Quandl key', + }), + value: 'someKeyHere', + description: i18n.translate('timelion.uiSettings.quandlKeyDescription', { + defaultMessage: '{experimentalLabel} Your API key from www.quandl.com', + values: { experimentalLabel: `[${experimentalLabel}]` }, + }), + category: ['timelion'], + schema: schema.string(), + }, + }); + return deepFreeze({ uiEnabled: config.ui.enabled }); } From 1932a682f6954a2ee127a7e2e37c8c6867a208c2 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Wed, 8 Jul 2020 11:02:34 +0300 Subject: [PATCH 11/14] Fixed save expression --- src/plugins/timelion/public/app.js | 42 ++++++++++++++---------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 07ab6058563ce..7cdced0a99f9f 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -588,28 +588,26 @@ export function initTimelionApp(app, deps) { }); } - function saveExpression(title) { - savedVisualizations - .get({ type: 'timelion', searchSource: true }) - .then(function (savedExpression) { - const state = stateContainer.getState(); - savedExpression.visState.params = { - expression: state.sheet[state.selected], - interval: state.interval, - }; - savedExpression.title = title; - savedExpression.visState.title = title; - savedExpression.save().then(function (id) { - if (id) { - deps.core.notifications.toasts.addSuccess( - i18n.translate('timelion.saveExpression.successNotificationText', { - defaultMessage: `Saved expression '{title}'`, - values: { title: savedExpression.title }, - }) - ); - } - }); - }); + 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(); From 3ea1bc7872b47cbfe23216774af5edb1d2b7d1ef Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Fri, 10 Jul 2020 15:22:02 +0300 Subject: [PATCH 12/14] Fixed navigation --- src/plugins/timelion/public/application.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts index 53dc912ebdd89..a398106d56f58 100644 --- a/src/plugins/timelion/public/application.ts +++ b/src/plugins/timelion/public/application.ts @@ -71,8 +71,7 @@ export const renderApp = (deps: RenderDeps) => { configureAppAngularModule( angularModuleInstance, { core: deps.core, env: deps.pluginInitializerContext.env }, - true, - () => deps.mountParams.history + true ); initTimelionApp(angularModuleInstance, deps); } From 1fd161a6f0aa95de195e35bbd56b972464400fa8 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 13 Jul 2020 14:41:19 +0300 Subject: [PATCH 13/14] fix CI --- src/plugins/timelion/kibana.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index 1072a1edd5bf1..d8c709d867a3f 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -3,5 +3,17 @@ "version": "kibana", "ui": true, "server": true, - "requiredPlugins": ["visualizations", "data", "navigation", "visTypeTimelion", "kibanaLegacy"] + "requiredBundles": [ + "kibanaLegacy", + "kibanaUtils", + "savedObjects", + "visTypeTimelion" + ], + "requiredPlugins": [ + "visualizations", + "data", + "navigation", + "visTypeTimelion", + "kibanaLegacy" + ] } From ed3703eb9f1aefa53b3f4fe779e30eded68fa982 Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Tue, 14 Jul 2020 16:35:47 +0300 Subject: [PATCH 14/14] Fixed some problem --- src/plugins/timelion/public/app.js | 1 + src/plugins/timelion/public/plugin.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/plugins/timelion/public/app.js b/src/plugins/timelion/public/app.js index 7cdced0a99f9f..0294e71084f98 100644 --- a/src/plugins/timelion/public/app.js +++ b/src/plugins/timelion/public/app.js @@ -175,6 +175,7 @@ export function initTimelionApp(app, deps) { }), run: function () { history.push('/'); + $route.reload(); }, testId: 'timelionNewButton', }; diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 7a9db819d3271..a92ced20cb6d1 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -27,6 +27,7 @@ import { DEFAULT_APP_CATEGORIES, AppMountParameters, AppUpdater, + ScopedHistory, } from '../../../core/public'; import { Panel } from './panels/panel'; import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public'; @@ -44,16 +45,17 @@ export interface TimelionPluginDependencies { } /** @internal */ -export class TimelionPlugin implements Plugin, void> { +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 async setup(core: CoreSetup, { data }: { data: DataPublicPluginSetup }) { + public setup(core: CoreSetup, { data }: { data: DataPublicPluginSetup }) { const timelionPanels: Map = new Map(); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ @@ -76,6 +78,7 @@ export class TimelionPlugin implements Plugin, void> { ), }, ], + getHistory: () => this.currentHistory!, }); this.stopUrlTracking = () => { @@ -93,9 +96,14 @@ export class TimelionPlugin implements Plugin, void> { 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({ @@ -106,6 +114,7 @@ export class TimelionPlugin implements Plugin, void> { plugins: pluginsStart as TimelionPluginDependencies, }); return () => { + unlistenParentHistory(); unmount(); appUnMounted(); };