From 156467e53591962762b33919c10f6b900220d10e Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 14 Jun 2019 12:51:11 -0400 Subject: [PATCH] Typescript (most of) the rest of dashboard code (#38546) * Typescript the rest of dashboard code, including the main dashboard_app entry file * Update jest snapshot after unused compressed parameter * fix: accidental logic change prevented clone modal from closing * Update based on review feedback --- .../kibana/public/dashboard/dashboard_app.js | 540 ------------ .../kibana/public/dashboard/dashboard_app.tsx | 771 ++++++++++++++++++ .../dashboard/dashboard_state_manager.ts | 12 +- .../public/dashboard/lib/filter_utils.ts | 14 +- .../saved_dashboard/saved_dashboard.d.ts | 8 +- .../__snapshots__/save_modal.test.js.snap | 1 - .../top_nav/{add_panel.js => add_panel.tsx} | 49 +- .../{clone_modal.js => clone_modal.tsx} | 72 +- ...op_nav_config.js => get_top_nav_config.ts} | 50 +- .../top_nav/{options.js => options.tsx} | 48 +- .../top_nav/{save_modal.js => save_modal.tsx} | 112 ++- .../{show_add_panel.js => show_add_panel.tsx} | 9 +- ...ow_clone_modal.js => show_clone_modal.tsx} | 29 +- ...ns_popover.js => show_options_popover.tsx} | 18 +- .../kibana/public/dashboard/types.ts | 9 +- src/legacy/ui/public/chrome/index.d.ts | 2 +- .../ui/public/share/{index.js => index.ts} | 0 .../ui/public/share/share_action_registry.ts | 3 +- .../ui/public/timefilter/timefilter.d.ts | 21 +- src/legacy/ui/public/url/kbn_url.d.ts | 23 + 20 files changed, 1063 insertions(+), 728 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js create mode 100644 src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{add_panel.js => add_panel.tsx} (71%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{clone_modal.js => clone_modal.tsx} (78%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{get_top_nav_config.js => get_top_nav_config.ts} (85%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{options.js => options.tsx} (77%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{save_modal.js => save_modal.tsx} (54%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{show_add_panel.js => show_add_panel.tsx} (88%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{show_clone_modal.js => show_clone_modal.tsx} (69%) rename src/legacy/core_plugins/kibana/public/dashboard/top_nav/{show_options_popover.js => show_options_popover.tsx} (83%) rename src/legacy/ui/public/share/{index.js => index.ts} (100%) create mode 100644 src/legacy/ui/public/url/kbn_url.d.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js deleted file mode 100644 index 1e8a6ecb3bf5..000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js +++ /dev/null @@ -1,540 +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 { i18n } from '@kbn/i18n'; -import React from 'react'; -import angular from 'angular'; -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { wrapInI18nContext } from 'ui/i18n'; -import { toastNotifications } from 'ui/notify'; - -import { panelActionsStore } from './store/panel_actions_store'; - -import { getDashboardTitle } from './dashboard_strings'; -import { DashboardViewMode } from './dashboard_view_mode'; -import { TopNavIds } from './top_nav/top_nav_ids'; -import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { DocTitleProvider } from 'ui/doc_title'; -import { getTopNavConfig } from './top_nav/get_top_nav_config'; -import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -import { DashboardStateManager } from './dashboard_state_manager'; -import { saveDashboard } from './lib'; -import { showCloneModal } from './top_nav/show_clone_modal'; -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { DashboardSaveModal } from './top_nav/save_modal'; -import { showAddPanel } from './top_nav/show_add_panel'; -import { showOptionsPopover } from './top_nav/show_options_popover'; -import { showNewVisModal } from '../visualize/wizard'; -import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; -import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; -import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; -import { FilterManagerProvider } from 'ui/filter_manager'; -import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; -import { ContextMenuActionsRegistryProvider } from 'ui/embeddable'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { timefilter } from 'ui/timefilter'; -import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; - -import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; - - -const app = uiModules.get('app/dashboard', [ - 'elasticsearch', - 'ngRoute', - 'react', - 'kibana/courier', - 'kibana/config', -]); - -app.directive('dashboardViewportProvider', function (reactDirective) { - return reactDirective(wrapInI18nContext(DashboardViewportProvider)); -}); - -app.directive('dashboardApp', function ($injector) { - const courier = $injector.get('courier'); - const AppState = $injector.get('AppState'); - const kbnUrl = $injector.get('kbnUrl'); - const confirmModal = $injector.get('confirmModal'); - const config = $injector.get('config'); - const Private = $injector.get('Private'); - const indexPatterns = $injector.get('indexPatterns'); - - return { - restrict: 'E', - controllerAs: 'dashboardApp', - controller: function ( - $scope, - $rootScope, - $route, - $routeParams, - getAppState, - dashboardConfig, - localStorage - ) { - const filterManager = Private(FilterManagerProvider); - const queryFilter = Private(FilterBarQueryFilterProvider); - const docTitle = Private(DocTitleProvider); - const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider); - const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); - const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); - - panelActionsStore.initializeFromRegistry(panelActionsRegistry); - - const visTypes = Private(VisTypesRegistryProvider); - $scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType]; - - const dash = $scope.dash = $route.current.locals.dash; - if (dash.id) { - docTitle.change(dash.title); - } - - const dashboardStateManager = new DashboardStateManager({ - savedDashboard: dash, - AppStateClass: AppState, - hideWriteControls: dashboardConfig.getHideWriteControls(), - addFilter: ({ field, value, operator, index }) => { - filterActions.addFilter(field, value, operator, index, dashboardStateManager.getAppState(), filterManager); - } - }); - - $scope.getDashboardState = () => dashboardStateManager; - $scope.appState = dashboardStateManager.getAppState(); - - // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during - // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - } - - const updateState = () => { - // Following the "best practice" of always have a '.' in your ng-models – - // https://github.com/angular/angular.js/wiki/Understanding-Scopes - $scope.model = { - query: dashboardStateManager.getQuery(), - filters: queryFilter.getFilters(), - timeRestore: dashboardStateManager.getTimeRestore(), - title: dashboardStateManager.getTitle(), - description: dashboardStateManager.getDescription(), - timeRange: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }; - $scope.panels = dashboardStateManager.getPanels(); - $scope.screenTitle = dashboardStateManager.getTitle(); - - const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns(); - if (panelIndexPatterns && panelIndexPatterns.length > 0) { - $scope.indexPatterns = panelIndexPatterns; - } - else { - indexPatterns.getDefault().then((defaultIndexPattern) => { - $scope.$evalAsync(() => { - $scope.indexPatterns = [defaultIndexPattern]; - }); - }); - } - }; - - // Part of the exposed plugin API - do not remove without careful consideration. - this.appStatus = { - dirty: !dash.id - }; - - dashboardStateManager.registerChangeListener(status => { - this.appStatus.dirty = status.dirty || !dash.id; - updateState(); - }); - - dashboardStateManager.applyFilters( - dashboardStateManager.getQuery() || { - query: '', - language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') - }, - queryFilter.getFilters() - ); - - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - updateState(); - - $scope.refresh = () => { - $rootScope.$broadcast('fetch'); - courier.fetch(); - }; - dashboardStateManager.handleTimeChange(timefilter.getTime()); - dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); - - $scope.expandedPanel = null; - $scope.dashboardViewMode = dashboardStateManager.getViewMode(); - - $scope.landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; - $scope.hasExpandedPanel = () => $scope.expandedPanel !== null; - $scope.getDashTitle = () => getDashboardTitle( - dashboardStateManager.getTitle(), - dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter)); - - // Push breadcrumbs to new header navigation - const updateBreadcrumbs = () => { - chrome.breadcrumbs.set([ - { - text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { - defaultMessage: 'Dashboard', - }), - href: $scope.landingPageUrl() - }, - { text: $scope.getDashTitle() } - ]); - }; - updateBreadcrumbs(); - dashboardStateManager.registerChangeListener(updateBreadcrumbs); - - $scope.newDashboard = () => { kbnUrl.change(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}); }; - $scope.saveState = () => dashboardStateManager.saveState(); - $scope.getShouldShowEditHelp = () => ( - !dashboardStateManager.getPanels().length && - dashboardStateManager.getIsEditMode() && - !dashboardConfig.getHideWriteControls() - ); - $scope.getShouldShowViewHelp = () => ( - !dashboardStateManager.getPanels().length && - dashboardStateManager.getIsViewMode() && - !dashboardConfig.getHideWriteControls() - ); - - $scope.minimizeExpandedPanel = () => { - $scope.expandedPanel = null; - }; - - $scope.expandPanel = (panelIndex) => { - $scope.expandedPanel = - dashboardStateManager.getPanels().find((panel) => panel.panelIndex === panelIndex); - }; - - $scope.updateQueryAndFetch = function ({ query, dateRange }) { - timefilter.setTime(dateRange); - - const oldQuery = $scope.model.query; - if (_.isEqual(oldQuery, query)) { - // The user can still request a reload in the query bar, even if the - // query is the same, and in that case, we have to explicitly ask for - // a reload, since no state changes will cause it. - dashboardStateManager.requestReload(); - } else { - $scope.model.query = query; - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - } - $scope.refresh(); - }; - - $scope.onRefreshChange = function ({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value - }); - }; - - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onCancelApplyFilters = () => { - $scope.appState.$newFilters = []; - }; - - $scope.onApplyFilters = filters => { - queryFilter.addFiltersAndChangeTimeFilter(filters); - $scope.appState.$newFilters = []; - }; - - $scope.$watch('appState.$newFilters', (filters = []) => { - if (filters.length === 1) { - $scope.onApplyFilters(filters); - } - }); - - $scope.indexPatterns = []; - - $scope.onPanelRemoved = (panelIndex) => { - dashboardStateManager.removePanel(panelIndex); - $scope.indexPatterns = dashboardStateManager.getPanelIndexPatterns(); - }; - - $scope.$watch('model.query', (newQuery) => { - const query = migrateLegacyQuery(newQuery); - $scope.updateQueryAndFetch({ query }); - }); - - $scope.$listenAndDigestAsync(timefilter, 'fetch', () => { - dashboardStateManager.handleTimeChange(timefilter.getTime()); - // Currently discover relies on this logic to re-fetch. We need to refactor it to rely instead on the - // directly passed down time filter. Then we can get rid of this reliance on scope broadcasts. - $scope.refresh(); - }); - $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => { - dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); - updateState(); - }); - $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState); - - function updateViewMode(newMode) { - $scope.topNavMenu = getTopNavConfig(newMode, navActions, dashboardConfig.getHideWriteControls()); // eslint-disable-line no-use-before-define - dashboardStateManager.switchViewMode(newMode); - $scope.dashboardViewMode = newMode; - } - - const onChangeViewMode = (newMode) => { - const isPageRefresh = newMode === dashboardStateManager.getViewMode(); - const isLeavingEditMode = !isPageRefresh && newMode === DashboardViewMode.VIEW; - const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); - - if (!willLoseChanges) { - updateViewMode(newMode); - return; - } - - function revertChangesAndExitEditMode() { - dashboardStateManager.resetState(); - kbnUrl.change(dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL); - // This is only necessary for new dashboards, which will default to Edit mode. - updateViewMode(DashboardViewMode.VIEW); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - } - } - - confirmModal( - i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', - { defaultMessage: `Once you discard your changes, there's no getting them back.` } - ), - { - onConfirm: revertChangesAndExitEditMode, - onCancel: _.noop, - confirmButtonText: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', - { defaultMessage: 'Discard changes' } - ), - cancelButtonText: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', - { defaultMessage: 'Continue editing' } - ), - defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', - { defaultMessage: 'Discard changes to dashboard?' } - ) - } - ); - }; - - /** - * Saves the dashboard. - * - * @param {object} [saveOptions={}] - * @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @return {Promise} - * @resolved {String} - The id of the doc - */ - function save(saveOptions) { - return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) - .then(function (id) { - if (id) { - toastNotifications.addSuccess({ - title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', - { - defaultMessage: `Dashboard '{dashTitle}' was saved`, - values: { dashTitle: dash.title }, - }, - ), - 'data-test-subj': 'saveDashboardSuccess', - }); - - if (dash.id !== $routeParams.id) { - kbnUrl.change(createDashboardEditUrl(dash.id)); - } else { - docTitle.change(dash.lastSavedTitle); - updateViewMode(DashboardViewMode.VIEW); - } - } - return { id }; - }).catch((error) => { - toastNotifications.addDanger({ - title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', - { - defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, - values: { - dashTitle: dash.title, - errorMessage: error.message, - }, - }, - ), - 'data-test-subj': 'saveDashboardFailure', - }); - return { error }; - }); - } - - $scope.showFilterBar = () => $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); - - $scope.showAddPanel = () => { - dashboardStateManager.setFullScreenMode(false); - $scope.kbnTopNav.click(TopNavIds.ADD); - }; - $scope.enterEditMode = () => { - dashboardStateManager.setFullScreenMode(false); - $scope.kbnTopNav.click('edit'); - }; - const navActions = {}; - navActions[TopNavIds.FULL_SCREEN] = () => - dashboardStateManager.setFullScreenMode(true); - navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.VIEW); - navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.EDIT); - navActions[TopNavIds.SAVE] = () => { - const currentTitle = dashboardStateManager.getTitle(); - const currentDescription = dashboardStateManager.getDescription(); - const currentTimeRestore = dashboardStateManager.getTimeRestore(); - const onSave = ({ newTitle, newDescription, newCopyOnSave, newTimeRestore, isTitleDuplicateConfirmed, onTitleDuplicate }) => { - dashboardStateManager.setTitle(newTitle); - dashboardStateManager.setDescription(newDescription); - dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave; - dashboardStateManager.setTimeRestore(newTimeRestore); - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - return save(saveOptions).then(({ id, error }) => { - // If the save wasn't successful, put the original values back. - if (!id || error) { - dashboardStateManager.setTitle(currentTitle); - dashboardStateManager.setDescription(currentDescription); - dashboardStateManager.setTimeRestore(currentTimeRestore); - } - return { id, error }; - }); - }; - - const dashboardSaveModal = ( - {}} - title={currentTitle} - description={currentDescription} - timeRestore={currentTimeRestore} - showCopyOnSave={dash.id ? true : false} - /> - ); - showSaveModal(dashboardSaveModal); - }; - navActions[TopNavIds.CLONE] = () => { - const currentTitle = dashboardStateManager.getTitle(); - const onClone = (newTitle, isTitleDuplicateConfirmed, onTitleDuplicate) => { - dashboardStateManager.savedDashboard.copyOnSave = true; - dashboardStateManager.setTitle(newTitle); - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - return save(saveOptions).then(({ id, error }) => { - // If the save wasn't successful, put the original title back. - if (!id || error) { - dashboardStateManager.setTitle(currentTitle); - } - return { id, error }; - }); - }; - - showCloneModal(onClone, currentTitle); - }; - navActions[TopNavIds.ADD] = () => { - const addNewVis = () => { - showNewVisModal(visTypes, { editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM] }); - }; - - showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories); - }; - navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => { - showOptionsPopover({ - anchorElement, - useMargins: dashboardStateManager.getUseMargins(), - onUseMarginsChange: (isChecked) => { - dashboardStateManager.setUseMargins(isChecked); - }, - hidePanelTitles: dashboardStateManager.getHidePanelTitles(), - onHidePanelTitlesChange: (isChecked) => { - dashboardStateManager.setHidePanelTitles(isChecked); - }, - }); - }; - navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { - showShareContextMenu({ - anchorElement, - allowEmbed: true, - allowShortUrl: !dashboardConfig.getHideWriteControls(), - getUnhashableStates, - objectId: dash.id, - objectType: 'dashboard', - shareContextMenuExtensions, - sharingData: { - title: dash.title, - }, - isDirty: dashboardStateManager.getIsDirty(), - }); - }; - - updateViewMode(dashboardStateManager.getViewMode()); - - // update root source when filters update - this.updateSubscription = queryFilter.getUpdates$().subscribe({ - next: () => { - $scope.model.filters = queryFilter.getFilters(); - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - } - }); - - // update data when filters fire fetch event - - this.fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh); - - $scope.$on('$destroy', () => { - this.updateSubscription.unsubscribe(); - this.fetchSubscription.unsubscribe(); - dashboardStateManager.destroy(); - }); - - if ($route.current.params && $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) { - dashboardStateManager.addNewPanel($route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], 'visualization'); - - kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); - kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); - } - } - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx new file mode 100644 index 000000000000..32520e524d75 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -0,0 +1,771 @@ +/* + * 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 React from 'react'; +import angular from 'angular'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import chrome, { IInjector } from 'ui/chrome'; +import { wrapInI18nContext } from 'ui/i18n'; +import { toastNotifications } from 'ui/notify'; + +// @ts-ignore +import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; + +// @ts-ignore +import { DocTitleProvider } from 'ui/doc_title'; + +// @ts-ignore +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; + +import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; +import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; + +// @ts-ignore +import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; + +// @ts-ignore +import { FilterManagerProvider } from 'ui/filter_manager'; +import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; +import { ContextMenuActionsRegistryProvider, Query, EmbeddableFactory } from 'ui/embeddable'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; +import { timefilter } from 'ui/timefilter'; + +import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider'; + +import { + AppStateClass as TAppStateClass, + AppState as TAppState, +} from 'ui/state_management/app_state'; + +import { KbnUrl } from 'ui/url/kbn_url'; +import { Filter } from '@kbn/es-query'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { IndexPattern } from 'ui/index_patterns'; +import { IPrivate } from 'ui/private'; +import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public'; +import { SaveOptions } from 'ui/saved_objects/saved_object'; +import moment from 'moment'; +import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; +import { + DashboardAppState, + SavedDashboardPanel, + EmbeddableFactoryRegistry, + NavAction, +} from './types'; + +// @ts-ignore -- going away soon +import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; + +import { showNewVisModal } from '../visualize/wizard'; +import { showOptionsPopover } from './top_nav/show_options_popover'; +import { showAddPanel } from './top_nav/show_add_panel'; +import { DashboardSaveModal } from './top_nav/save_modal'; +import { showCloneModal } from './top_nav/show_clone_modal'; +import { saveDashboard } from './lib'; +import { DashboardStateManager } from './dashboard_state_manager'; +import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; +import { getTopNavConfig } from './top_nav/get_top_nav_config'; +import { TopNavIds } from './top_nav/top_nav_ids'; +import { DashboardViewMode } from './dashboard_view_mode'; +import { getDashboardTitle } from './dashboard_strings'; +import { panelActionsStore } from './store/panel_actions_store'; + +type ConfirmModalFn = ( + message: string, + confirmOptions: { + onConfirm: () => void; + onCancel: () => void; + confirmButtonText: string; + cancelButtonText: string; + defaultFocusedButton: string; + title: string; + } +) => void; + +type AddFilterFn = ( + { + field, + value, + operator, + index, + }: { + field: string; + value: string; + operator: string; + index: string; + }, + appState: TAppState +) => void; + +interface DashboardAppScope extends ng.IScope { + dash: SavedObjectDashboard; + appState: TAppState; + screenTitle: string; + model: { + query: Query | string; + filters: Filter[]; + timeRestore: boolean; + title: string; + description: string; + timeRange: + | TimeRange + | { to: string | moment.Moment | undefined; from: string | moment.Moment | undefined }; + refreshInterval: any; + }; + refreshInterval: any; + panels: SavedDashboardPanel[]; + indexPatterns: StaticIndexPattern[]; + $evalAsync: any; + dashboardViewMode: DashboardViewMode; + expandedPanel?: string; + getShouldShowEditHelp: () => boolean; + getShouldShowViewHelp: () => boolean; + updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void; + onRefreshChange: ( + { isPaused, refreshInterval }: { isPaused: boolean; refreshInterval: any } + ) => void; + onFiltersUpdated: (filters: Filter[]) => void; + $listenAndDigestAsync: any; + onCancelApplyFilters: () => void; + onApplyFilters: (filters: Filter[]) => void; + topNavMenu: any; + showFilterBar: () => boolean; + showAddPanel: any; + kbnTopNav: any; + enterEditMode: () => void; + $listen: any; + getEmbeddableFactory: (type: string) => EmbeddableFactory; + getDashboardState: () => DashboardStateManager; + refresh: () => void; +} + +class DashboardAppController { + // Part of the exposed plugin API - do not remove without careful consideration. + public appStatus = { + dirty: false, + }; + + constructor({ + $scope, + $rootScope, + $route, + $routeParams, + getAppState, + dashboardConfig, + localStorage, + Private, + kbnUrl, + AppStateClass, + indexPatterns, + config, + confirmModal, + addFilter, + courier, + }: { + courier: { fetch: () => void }; + $scope: DashboardAppScope; + $route: any; + $rootScope: ng.IRootScopeService; + $routeParams: any; + getAppState: { + previouslyStored: () => TAppState | undefined; + }; + indexPatterns: { + getDefault: () => Promise; + }; + dashboardConfig: any; + localStorage: any; + Private: IPrivate; + kbnUrl: KbnUrl; + AppStateClass: TAppStateClass; + config: any; + confirmModal: ( + message: string, + confirmOptions: { + onConfirm: () => void; + onCancel: () => void; + confirmButtonText: string; + cancelButtonText: string; + defaultFocusedButton: string; + title: string; + } + ) => void; + addFilter: AddFilterFn; + }) { + const filterManager = Private(FilterManagerProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); + const docTitle = Private<{ change: (title: string) => void }>(DocTitleProvider); + const embeddableFactories = Private( + EmbeddableFactoriesRegistryProvider + ) as EmbeddableFactoryRegistry; + const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); + const getUnhashableStates = Private(getUnhashableStatesProvider); + const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); + + // @ts-ignore This code is going away shortly. + panelActionsStore.initializeFromRegistry(panelActionsRegistry); + + const visTypes = Private(VisTypesRegistryProvider); + $scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType]; + + const dash = ($scope.dash = $route.current.locals.dash); + if (dash.id) { + docTitle.change(dash.title); + } + + const dashboardStateManager = new DashboardStateManager({ + savedDashboard: dash, + AppStateClass, + hideWriteControls: dashboardConfig.getHideWriteControls(), + addFilter: ({ field, value, operator, index }) => { + filterActions.addFilter( + field, + value, + operator, + index, + dashboardStateManager.getAppState(), + filterManager + ); + }, + }); + + $scope.getDashboardState = () => dashboardStateManager; + $scope.appState = dashboardStateManager.getAppState(); + + // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during + // normal cross app navigation. + if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + } + + const updateState = () => { + // Following the "best practice" of always have a '.' in your ng-models – + // https://github.com/angular/angular.js/wiki/Understanding-Scopes + $scope.model = { + query: dashboardStateManager.getQuery(), + filters: queryFilter.getFilters(), + timeRestore: dashboardStateManager.getTimeRestore(), + title: dashboardStateManager.getTitle(), + description: dashboardStateManager.getDescription(), + timeRange: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }; + $scope.panels = dashboardStateManager.getPanels(); + $scope.screenTitle = dashboardStateManager.getTitle(); + + const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns(); + if (panelIndexPatterns && panelIndexPatterns.length > 0) { + $scope.indexPatterns = panelIndexPatterns; + } else { + indexPatterns.getDefault().then(defaultIndexPattern => { + $scope.$evalAsync(() => { + $scope.indexPatterns = [defaultIndexPattern]; + }); + }); + } + }; + + // Part of the exposed plugin API - do not remove without careful consideration. + this.appStatus = { + dirty: !dash.id, + }; + + dashboardStateManager.registerChangeListener(status => { + this.appStatus.dirty = status.dirty || !dash.id; + updateState(); + }); + + dashboardStateManager.applyFilters( + dashboardStateManager.getQuery() || { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + }, + queryFilter.getFilters() + ); + + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + updateState(); + + $scope.refresh = () => { + $rootScope.$broadcast('fetch'); + courier.fetch(); + }; + dashboardStateManager.handleTimeChange(timefilter.getTime()); + dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); + $scope.dashboardViewMode = dashboardStateManager.getViewMode(); + + const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; + + const getDashTitle = () => + getDashboardTitle( + dashboardStateManager.getTitle(), + dashboardStateManager.getViewMode(), + dashboardStateManager.getIsDirty(timefilter) + ); + + // Push breadcrumbs to new header navigation + const updateBreadcrumbs = () => { + chrome.breadcrumbs.set([ + { + text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { + defaultMessage: 'Dashboard', + }), + href: landingPageUrl(), + }, + { text: getDashTitle() }, + ]); + }; + + updateBreadcrumbs(); + dashboardStateManager.registerChangeListener(updateBreadcrumbs); + + $scope.getShouldShowEditHelp = () => + !dashboardStateManager.getPanels().length && + dashboardStateManager.getIsEditMode() && + !dashboardConfig.getHideWriteControls(); + $scope.getShouldShowViewHelp = () => + !dashboardStateManager.getPanels().length && + dashboardStateManager.getIsViewMode() && + !dashboardConfig.getHideWriteControls(); + + $scope.updateQueryAndFetch = function({ query, dateRange }) { + if (dateRange) { + timefilter.setTime(dateRange); + } + + const oldQuery = $scope.model.query; + if (_.isEqual(oldQuery, query)) { + // The user can still request a reload in the query bar, even if the + // query is the same, and in that case, we have to explicitly ask for + // a reload, since no state changes will cause it. + dashboardStateManager.requestReload(); + } else { + $scope.model.query = query; + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + } + $scope.refresh(); + }; + + $scope.onRefreshChange = function({ isPaused, refreshInterval }) { + timefilter.setRefreshInterval({ + pause: isPaused, + value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, + }); + }; + + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + + $scope.onCancelApplyFilters = () => { + $scope.appState.$newFilters = []; + }; + + $scope.onApplyFilters = filters => { + queryFilter.addFiltersAndChangeTimeFilter(filters); + $scope.appState.$newFilters = []; + }; + + $scope.$watch('appState.$newFilters', (filters: Filter[] = []) => { + if (filters.length === 1) { + $scope.onApplyFilters(filters); + } + }); + + $scope.indexPatterns = []; + + $scope.$watch('model.query', (newQuery: Query) => { + const query = migrateLegacyQuery(newQuery) as Query; + $scope.updateQueryAndFetch({ query }); + }); + + $scope.$listenAndDigestAsync(timefilter, 'fetch', () => { + dashboardStateManager.handleTimeChange(timefilter.getTime()); + // Currently discover relies on this logic to re-fetch. We need to refactor it to rely instead on the + // directly passed down time filter. Then we can get rid of this reliance on scope broadcasts. + $scope.refresh(); + }); + $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => { + dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); + updateState(); + }); + $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState); + + function updateViewMode(newMode: DashboardViewMode) { + $scope.topNavMenu = getTopNavConfig( + newMode, + navActions, + dashboardConfig.getHideWriteControls() + ); // eslint-disable-line no-use-before-define + dashboardStateManager.switchViewMode(newMode); + $scope.dashboardViewMode = newMode; + } + + const onChangeViewMode = (newMode: DashboardViewMode) => { + const isPageRefresh = newMode === dashboardStateManager.getViewMode(); + const isLeavingEditMode = !isPageRefresh && newMode === DashboardViewMode.VIEW; + const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); + + if (!willLoseChanges) { + updateViewMode(newMode); + return; + } + + function revertChangesAndExitEditMode() { + dashboardStateManager.resetState(); + kbnUrl.change( + dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL + ); + // This is only necessary for new dashboards, which will default to Edit mode. + updateViewMode(DashboardViewMode.VIEW); + + // We need to do a hard reset of the timepicker. appState will not reload like + // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on + // reload will cause it not to sync. + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + } + } + + confirmModal( + i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { + defaultMessage: `Once you discard your changes, there's no getting them back.`, + }), + { + onConfirm: revertChangesAndExitEditMode, + onCancel: _.noop, + confirmButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', + { defaultMessage: 'Discard changes' } + ), + cancelButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', + { defaultMessage: 'Continue editing' } + ), + defaultFocusedButton: ConfirmationButtonTypes.CANCEL, + title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { + defaultMessage: 'Discard changes to dashboard?', + }), + } + ); + }; + + /** + * Saves the dashboard. + * + * @param {object} [saveOptions={}] + * @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it + * can confirm an overwrite if a document with the id already exists. + * @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title + * @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists. + * When not provided, confirm modal will be displayed asking user to confirm or cancel save. + * @return {Promise} + * @resolved {String} - The id of the doc + */ + function save(saveOptions: SaveOptions): Promise<{ id?: string } | { error: Error }> { + return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) + .then(function(id) { + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', { + defaultMessage: `Dashboard '{dashTitle}' was saved`, + values: { dashTitle: dash.title }, + }), + 'data-test-subj': 'saveDashboardSuccess', + }); + + if (dash.id !== $routeParams.id) { + kbnUrl.change(createDashboardEditUrl(dash.id)); + } else { + docTitle.change(dash.lastSavedTitle); + updateViewMode(DashboardViewMode.VIEW); + } + } + return { id }; + }) + .catch(error => { + toastNotifications.addDanger({ + title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', { + defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, + values: { + dashTitle: dash.title, + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return { error }; + }); + } + + $scope.showFilterBar = () => + $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + + $scope.showAddPanel = () => { + dashboardStateManager.setFullScreenMode(false); + $scope.kbnTopNav.click(TopNavIds.ADD); + }; + $scope.enterEditMode = () => { + dashboardStateManager.setFullScreenMode(false); + $scope.kbnTopNav.click('edit'); + }; + const navActions: { + [key: string]: NavAction; + } = {}; + navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); + navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.VIEW); + navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.EDIT); + navActions[TopNavIds.SAVE] = () => { + const currentTitle = dashboardStateManager.getTitle(); + const currentDescription = dashboardStateManager.getDescription(); + const currentTimeRestore = dashboardStateManager.getTimeRestore(); + const onSave = ({ + newTitle, + newDescription, + newCopyOnSave, + newTimeRestore, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }: { + newTitle: string; + newDescription: string; + newCopyOnSave: boolean; + newTimeRestore: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; + }) => { + dashboardStateManager.setTitle(newTitle); + dashboardStateManager.setDescription(newDescription); + dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave; + dashboardStateManager.setTimeRestore(newTimeRestore); + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return save(saveOptions).then((response: { id?: string } | { error: Error }) => { + // If the save wasn't successful, put the original values back. + if (!(response as { id: string }).id) { + dashboardStateManager.setTitle(currentTitle); + dashboardStateManager.setDescription(currentDescription); + dashboardStateManager.setTimeRestore(currentTimeRestore); + } + return response; + }); + }; + + const dashboardSaveModal = ( + {}} + title={currentTitle} + description={currentDescription} + timeRestore={currentTimeRestore} + showCopyOnSave={dash.id ? true : false} + /> + ); + showSaveModal(dashboardSaveModal); + }; + navActions[TopNavIds.CLONE] = () => { + const currentTitle = dashboardStateManager.getTitle(); + const onClone = ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => { + dashboardStateManager.savedDashboard.copyOnSave = true; + dashboardStateManager.setTitle(newTitle); + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return save(saveOptions).then((response: { id?: string } | { error: Error }) => { + // If the save wasn't successful, put the original title back. + if ((response as { error: Error }).error) { + dashboardStateManager.setTitle(currentTitle); + } + return response; + }); + }; + + showCloneModal(onClone, currentTitle); + }; + navActions[TopNavIds.ADD] = () => { + const addNewVis = () => { + showNewVisModal(visTypes, { + editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM], + }); + }; + + showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories); + }; + navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => { + showOptionsPopover({ + anchorElement, + useMargins: dashboardStateManager.getUseMargins(), + onUseMarginsChange: (isChecked: boolean) => { + dashboardStateManager.setUseMargins(isChecked); + }, + hidePanelTitles: dashboardStateManager.getHidePanelTitles(), + onHidePanelTitlesChange: (isChecked: boolean) => { + dashboardStateManager.setHidePanelTitles(isChecked); + }, + }); + }; + navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { + showShareContextMenu({ + anchorElement, + allowEmbed: true, + allowShortUrl: !dashboardConfig.getHideWriteControls(), + getUnhashableStates, + objectId: dash.id, + objectType: 'dashboard', + shareContextMenuExtensions: shareContextMenuExtensions.raw, + sharingData: { + title: dash.title, + }, + isDirty: dashboardStateManager.getIsDirty(), + }); + }; + + updateViewMode(dashboardStateManager.getViewMode()); + + // update root source when filters update + const updateSubscription = queryFilter.getUpdates$().subscribe({ + next: () => { + $scope.model.filters = queryFilter.getFilters(); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + }, + }); + + // update data when filters fire fetch event + + const fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh); + + $scope.$on('$destroy', () => { + updateSubscription.unsubscribe(); + fetchSubscription.unsubscribe(); + dashboardStateManager.destroy(); + }); + + if ( + $route.current.params && + $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] + ) { + dashboardStateManager.addNewPanel( + $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], + 'visualization' + ); + + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); + } + } +} + +const app = uiModules.get('app/dashboard', [ + 'elasticsearch', + 'ngRoute', + 'react', + 'kibana/courier', + 'kibana/config', +]); + +app.directive('dashboardViewportProvider', function(reactDirective: any) { + return reactDirective(wrapInI18nContext(DashboardViewportProvider)); +}); + +app.directive('dashboardApp', function($injector: IInjector) { + const AppState = $injector.get>('AppState'); + const kbnUrl = $injector.get('kbnUrl'); + const confirmModal = $injector.get('confirmModal'); + const config = $injector.get('config'); + const courier = $injector.get<{ fetch: () => void }>('courier'); + + const Private = $injector.get('Private'); + + const filterManager = Private(FilterManagerProvider); + const addFilter = ( + { + field, + value, + operator, + index, + }: { + field: string; + value: string; + operator: string; + index: string; + }, + appState: TAppState + ) => { + filterActions.addFilter(field, value, operator, index, appState, filterManager); + }; + + const indexPatterns = $injector.get<{ + getDefault: () => Promise; + }>('indexPatterns'); + + return { + restrict: 'E', + controllerAs: 'dashboardApp', + controller: ( + $scope: DashboardAppScope, + $rootScope: ng.IRootScopeService, + $route: any, + $routeParams: { + id?: string; + }, + getAppState: { + previouslyStored: () => TAppState | undefined; + }, + dashboardConfig: { + getHideWriteControls: () => boolean; + }, + localStorage: WindowLocalStorage + ) => + new DashboardAppController({ + $route, + $rootScope, + $scope, + $routeParams, + getAppState, + dashboardConfig, + localStorage, + Private, + kbnUrl, + AppStateClass: AppState, + indexPatterns, + config, + confirmModal, + addFilter, + courier, + }), + }; +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts index 7f74f904ac61..99e6565389bd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -621,10 +621,12 @@ export class DashboardStateManager { ); } - timeFilter.setTime({ - from: this.savedDashboard.timeFrom, - to: this.savedDashboard.timeTo, - }); + if (this.savedDashboard.timeFrom && this.savedDashboard.timeTo) { + timeFilter.setTime({ + from: this.savedDashboard.timeFrom, + to: this.savedDashboard.timeTo, + }); + } if (this.savedDashboard.refreshInterval) { timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval); @@ -642,7 +644,7 @@ export class DashboardStateManager { * Applies the current filter state to the dashboard. * @param filter {Array.} An array of filter bar filters. */ - public applyFilters(query: Query, filters: Filter[]) { + public applyFilters(query: Query | string, filters: Filter[]) { this.appState.query = query; this.savedDashboard.searchSource.setField('query', query); this.savedDashboard.searchSource.setField('filter', filters); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts index 242b6c4f100a..eddf8289fabb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts @@ -78,17 +78,19 @@ export class FilterUtils { /** * Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like * 'now-15m', then it just returns what it was passed). - * Note** Changing these moment objects to a utc string will actually cause a bug because it'll be in a format not - * expected by the time picker. This should get cleaned up and we should pick a single format to use everywhere. * @param time {string|Moment} - * @returns {string} the time represented in utc format, or if the time range was not able to be parsed into a moment + * @returns the time represented in utc format, or if the time range was not able to be parsed into a moment * object, it returns the same object it was given. */ - public static convertTimeToUTCString(time?: string | Moment): undefined | string | moment.Moment { + public static convertTimeToUTCString(time?: string | Moment): undefined | string { if (moment(time).isValid()) { - return moment(time).utc(); + return moment(time) + .utc() + .format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'); } else { - return time; + // If it's not a valid moment date, then it should be a string representing a relative time + // like 'now' or 'now-15m'. + return time as string; } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 651be0a453a7..303e02d63a7f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -26,14 +26,12 @@ export interface SavedObjectDashboard extends SavedObject { id?: string; copyOnSave: boolean; timeRestore: boolean; - // These optionally being moment objects rather than strings seems more like a bug than by design. It's due to - // some code in udpate_saved_dashboard that should probably get cleaned up. - timeTo: string | moment.Moment | undefined; - timeFrom: string | moment.Moment | undefined; + timeTo?: string; + timeFrom?: string; title: string; description?: string; panelsJSON: string; - optionsJSON: string | undefined; + optionsJSON?: string; // TODO: write a migration to rid of this, it's only around for bwc. uiStateJSON?: string; lastSavedTitle: string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap index 781407d63d83..fb2a6bbc0270 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/__snapshots__/save_modal.test.js.snap @@ -21,7 +21,6 @@ exports[`renders DashboardSaveModal 1`] = ` labelType="label" > { +interface Props { + onClose: () => void; + addNewPanel: (id: string, type: string) => void; + addNewVis: () => void; + embeddableFactories: EmbeddableFactoryRegistry; +} + +export class DashboardAddPanel extends React.Component { + private lastToast?: Toast; + + onAddPanel = (id: string, type: string, name: string) => { this.props.addNewPanel(id, type); // To avoid the clutter of having toast messages cover flyout @@ -76,9 +89,13 @@ export class DashboardAddPanel extends React.Component { Boolean(embeddableFactory.savedObjectMetaData)) - .map(({ savedObjectMetaData }) => savedObjectMetaData)} + savedObjectMetaData={ + this.props.embeddableFactories + .filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData)) + .map(({ savedObjectMetaData }) => savedObjectMetaData) as Array< + SavedObjectMetaData + > + } showFilter={true} noItemsMessage={i18n.translate( 'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage', @@ -88,11 +105,15 @@ export class DashboardAddPanel extends React.Component { )} /> - { capabilities.get().visualize.save ? ( + {capabilities.get().visualize.save ? ( - + - ) : null } + ) : null} ); } } - -DashboardAddPanel.propTypes = { - onClose: PropTypes.func.isRequired, - addNewPanel: PropTypes.func.isRequired, - addNewVis: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx similarity index 78% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx index 757ef8dd63ad..f216dcf4506e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx @@ -18,8 +18,7 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import { EuiButton, @@ -36,8 +35,28 @@ import { EuiCallOut, } from '@elastic/eui'; -class DashboardCloneModalUi extends React.Component { - constructor(props) { +interface Props { + onClone: ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => Promise; + onClose: () => void; + title: string; + intl: InjectedIntl; +} + +interface State { + newDashboardName: string; + isTitleDuplicateConfirmed: boolean; + hasTitleDuplicate: boolean; + isLoading: boolean; +} + +class DashboardCloneModalUi extends React.Component { + private isMounted = false; + + constructor(props: Props) { super(props); this.state = { @@ -48,11 +67,11 @@ class DashboardCloneModalUi extends React.Component { }; } componentDidMount() { - this._isMounted = true; + this.isMounted = true; } componentWillUnmount() { - this._isMounted = false; + this.isMounted = false; } onTitleDuplicate = () => { @@ -60,23 +79,27 @@ class DashboardCloneModalUi extends React.Component { isTitleDuplicateConfirmed: true, hasTitleDuplicate: true, }); - } + }; cloneDashboard = async () => { this.setState({ isLoading: true, }); - await this.props.onClone(this.state.newDashboardName, this.state.isTitleDuplicateConfirmed, this.onTitleDuplicate); + await this.props.onClone( + this.state.newDashboardName, + this.state.isTitleDuplicateConfirmed, + this.onTitleDuplicate + ); - if (this._isMounted) { + if (this.isMounted) { this.setState({ isLoading: false, }); } }; - onInputChange = (event) => { + onInputChange = (event: any) => { this.setState({ newDashboardName: event.target.value, isTitleDuplicateConfirmed: false, @@ -94,12 +117,15 @@ class DashboardCloneModalUi extends React.Component { @@ -122,7 +148,7 @@ class DashboardCloneModalUi extends React.Component { ); - } + }; render() { return ( @@ -162,14 +188,10 @@ class DashboardCloneModalUi extends React.Component { /> {this.renderDuplicateTitleCallout()} - - + } - Returns an array of objects for a top nav configuration, based on the * mode. */ -export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { +export function getTopNavConfig( + dashboardMode: DashboardViewMode, + actions: { [key: string]: NavAction }, + hideWriteControls: boolean +) { switch (dashboardMode) { case DashboardViewMode.VIEW: - return ( - hideWriteControls ? - [ + return hideWriteControls + ? [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), getShareConfig(actions[TopNavIds.SHARE]), ] - : [ + : [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), getShareConfig(actions[TopNavIds.SHARE]), getCloneConfig(actions[TopNavIds.CLONE]), - getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]) - ] - ); + getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), + ]; case DashboardViewMode.EDIT: return [ getSaveConfig(actions[TopNavIds.SAVE]), getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), getAddConfig(actions[TopNavIds.ADD]), getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE])]; + getShareConfig(actions[TopNavIds.SHARE]), + ]; default: return []; } } -function getFullScreenConfig(action) { +function getFullScreenConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', { defaultMessage: 'full screen', @@ -66,14 +70,14 @@ function getFullScreenConfig(action) { defaultMessage: 'Full Screen Mode', }), testId: 'dashboardFullScreenMode', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getEditConfig(action) { +function getEditConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', { defaultMessage: 'edit', @@ -85,14 +89,14 @@ function getEditConfig(action) { // We want to hide the "edit" button on small screens, since those have a responsive // layout, which is not tied to the grid anymore, so we cannot edit the grid on that screens. className: 'eui-hideFor--s eui-hideFor--xs', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action) { +function getSaveConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', { defaultMessage: 'save', @@ -101,14 +105,14 @@ function getSaveConfig(action) { defaultMessage: 'Save your dashboard', }), testId: 'dashboardSaveMenuItem', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getViewConfig(action) { +function getViewConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', { defaultMessage: 'cancel', @@ -117,14 +121,14 @@ function getViewConfig(action) { defaultMessage: 'Cancel editing and switch to view-only mode', }), testId: 'dashboardViewOnlyMode', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getCloneConfig(action) { +function getCloneConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', { defaultMessage: 'clone', @@ -133,14 +137,14 @@ function getCloneConfig(action) { defaultMessage: 'Create a copy of your dashboard', }), testId: 'dashboardClone', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getAddConfig(action) { +function getAddConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', { defaultMessage: 'add', @@ -149,14 +153,14 @@ function getAddConfig(action) { defaultMessage: 'Add a panel to the dashboard', }), testId: 'dashboardAddPanelButton', - run: action + run: action, }; } /** * @returns {kbnTopNavConfig} */ -function getShareConfig(action) { +function getShareConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', { defaultMessage: 'share', @@ -172,7 +176,7 @@ function getShareConfig(action) { /** * @returns {kbnTopNavConfig} */ -function getOptionsConfig(action) { +function getOptionsConfig(action: NavAction) { return { key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', { defaultMessage: 'options', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx similarity index 77% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx index 1080900f3b3f..07c8c392bd80 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx @@ -18,40 +18,48 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { - EuiForm, - EuiFormRow, - EuiSwitch, -} from '@elastic/eui'; +import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; -class OptionsMenuUi extends Component { +interface Props { + useMargins: boolean; + onUseMarginsChange: (useMargins: boolean) => void; + hidePanelTitles: boolean; + onHidePanelTitlesChange: (hideTitles: boolean) => void; + intl: InjectedIntl; +} +interface State { + useMargins: boolean; + hidePanelTitles: boolean; +} + +class OptionsMenuUi extends Component { state = { useMargins: this.props.useMargins, hidePanelTitles: this.props.hidePanelTitles, + }; + + constructor(props: Props) { + super(props); } - handleUseMarginsChange = (evt) => { + handleUseMarginsChange = (evt: any) => { const isChecked = evt.target.checked; this.props.onUseMarginsChange(isChecked); this.setState({ useMargins: isChecked }); - } + }; - handleHidePanelTitlesChange = (evt) => { + handleHidePanelTitlesChange = (evt: any) => { const isChecked = !evt.target.checked; this.props.onHidePanelTitlesChange(isChecked); this.setState({ hidePanelTitles: isChecked }); - } + }; render() { return ( - - + - ); } } -OptionsMenuUi.propTypes = { - useMargins: PropTypes.bool.isRequired, - onUseMarginsChange: PropTypes.func.isRequired, - hidePanelTitles: PropTypes.bool.isRequired, - onHidePanelTitlesChange: PropTypes.func.isRequired, -}; - export const OptionsMenu = injectI18n(OptionsMenuUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx similarity index 54% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx index 8194518c44b4..ad0954da1f7a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx @@ -18,27 +18,65 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -import { - EuiFormRow, - EuiTextArea, - EuiSwitch, -} from '@elastic/eui'; +import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui'; -class DashboardSaveModalUi extends React.Component { - constructor(props) { +interface SaveOptions { + newTitle: string; + newDescription: string; + newCopyOnSave: boolean; + newTimeRestore: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; +} + +interface Props { + onSave: ( + { + newTitle, + newDescription, + newCopyOnSave, + newTimeRestore, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }: SaveOptions + ) => void; + onClose: () => void; + title: string; + description: string; + timeRestore: boolean; + showCopyOnSave: boolean; + intl: InjectedIntl; +} + +interface State { + description: string; + timeRestore: boolean; +} + +class DashboardSaveModalUi extends React.Component { + state: State = { + description: this.props.description, + timeRestore: this.props.timeRestore, + }; + + constructor(props: Props) { super(props); - - this.state = { - description: props.description, - timeRestore: props.timeRestore, - }; } - saveDashboard = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { + saveDashboard = ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }: { + newTitle: string; + newCopyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; + }) => { this.props.onSave({ newTitle, newDescription: this.state.description, @@ -49,13 +87,13 @@ class DashboardSaveModalUi extends React.Component { }); }; - onDescriptionChange = (event) => { + onDescriptionChange = (event: any) => { this.setState({ description: event.target.value, }); }; - onTimeRestoreChange = (event) => { + onTimeRestoreChange = (event: any) => { this.setState({ timeRestore: event.target.checked, }); @@ -65,33 +103,38 @@ class DashboardSaveModalUi extends React.Component { return ( } + label={ + + } > } + helpText={ + + } > } + label={ + + } /> @@ -112,13 +155,4 @@ class DashboardSaveModalUi extends React.Component { } } -DashboardSaveModalUi.propTypes = { - onSave: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - title: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - timeRestore: PropTypes.bool.isRequired, - showCopyOnSave: PropTypes.bool.isRequired, -}; - export const DashboardSaveModal = injectI18n(DashboardSaveModalUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx similarity index 88% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx index ede1432d1048..76df56acd174 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_add_panel.tsx @@ -18,13 +18,18 @@ */ import { I18nContext } from 'ui/i18n'; -import { DashboardAddPanel } from './add_panel'; import React from 'react'; import ReactDOM from 'react-dom'; +import { DashboardAddPanel } from './add_panel'; +import { EmbeddableFactoryRegistry } from '../types'; let isOpen = false; -export function showAddPanel(addNewPanel, addNewVis, embeddableFactories) { +export function showAddPanel( + addNewPanel: (id: string, type: string) => void, + addNewVis: () => void, + embeddableFactories: EmbeddableFactoryRegistry +) { if (isOpen) { return; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx similarity index 69% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx index a3f55aceeac6..c3cd5621b2c8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_clone_modal.tsx @@ -18,24 +18,39 @@ */ import { I18nContext } from 'ui/i18n'; -import { DashboardCloneModal } from './clone_modal'; import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; +import { DashboardCloneModal } from './clone_modal'; -export function showCloneModal(onClone, title) { +export function showCloneModal( + onClone: ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => Promise<{ id?: string } | { error: Error }>, + title: string +) { const container = document.createElement('div'); const closeModal = () => { ReactDOM.unmountComponentAtNode(container); document.body.removeChild(container); }; - const onCloneConfirmed = (newTitle, isTitleDuplicateConfirmed, onTitleDuplicate) => { - onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then(({ id, error }) => { - if (id || error) { - closeModal(); + const onCloneConfirmed = async ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => { + onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then( + (response: { id?: string } | { error: Error }) => { + // The only time you don't want to close the modal is if it's asking you + // to confirm a duplicate title, in which case there will be no error and no id. + if ((response as { error: Error }).error || (response as { id?: string }).id) { + closeModal(); + } } - }); + ); }; document.body.appendChild(container); const element = ( diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx similarity index 83% rename from src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js rename to src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx index ea816e497d2b..8640d7dbc6bd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/show_options_popover.tsx @@ -21,12 +21,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nContext } from 'ui/i18n'; +import { EuiWrappingPopover } from '@elastic/eui'; import { OptionsMenu } from './options'; -import { - EuiWrappingPopover, -} from '@elastic/eui'; - let isOpen = false; const container = document.createElement('div'); @@ -42,6 +39,12 @@ export function showOptionsPopover({ onUseMarginsChange, hidePanelTitles, onHidePanelTitlesChange, +}: { + anchorElement: HTMLElement; + useMargins: boolean; + onUseMarginsChange: (useMargins: boolean) => void; + hidePanelTitles: boolean; + onHidePanelTitlesChange: (hideTitles: boolean) => void; }) { if (isOpen) { onClose(); @@ -53,12 +56,7 @@ export function showOptionsPopover({ document.body.appendChild(container); const element = ( - + { + byName: { [key: string]: EmbeddableFactory }; +} + +export type NavAction = (menuItem: any, navController: any, anchorElement: any) => void; + export interface GridData { w: number; h: number; diff --git a/src/legacy/ui/public/chrome/index.d.ts b/src/legacy/ui/public/chrome/index.d.ts index 7f61c7a2ca8c..e464cc516818 100644 --- a/src/legacy/ui/public/chrome/index.d.ts +++ b/src/legacy/ui/public/chrome/index.d.ts @@ -24,7 +24,7 @@ import { BreadcrumbsApi } from './api/breadcrumbs'; import { HelpExtensionApi } from './api/help_extension'; import { ChromeNavLinks } from './api/nav'; -interface IInjector { +export interface IInjector { get(injectable: string): T; } diff --git a/src/legacy/ui/public/share/index.js b/src/legacy/ui/public/share/index.ts similarity index 100% rename from src/legacy/ui/public/share/index.js rename to src/legacy/ui/public/share/index.ts diff --git a/src/legacy/ui/public/share/share_action_registry.ts b/src/legacy/ui/public/share/share_action_registry.ts index b6f828bbf56f..eec743d783a3 100644 --- a/src/legacy/ui/public/share/share_action_registry.ts +++ b/src/legacy/ui/public/share/share_action_registry.ts @@ -19,8 +19,9 @@ // @ts-ignore: implicit any for JS file import { uiRegistry } from 'ui/registry/_registry'; +import { ShareActionProvider } from './share_action'; -export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({ +export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({ name: 'shareContextMenuExtensions', index: ['id'], }); diff --git a/src/legacy/ui/public/timefilter/timefilter.d.ts b/src/legacy/ui/public/timefilter/timefilter.d.ts index 4a6571ae9dfd..b032caf684cb 100644 --- a/src/legacy/ui/public/timefilter/timefilter.d.ts +++ b/src/legacy/ui/public/timefilter/timefilter.d.ts @@ -19,7 +19,6 @@ import { Moment } from 'moment'; import { TimeRange } from './time_history'; -import moment = require('moment'); // NOTE: These types are somewhat guessed, they may be incorrect. @@ -29,23 +28,9 @@ export interface RefreshInterval { } export interface Timefilter { - time: { - // NOTE: It's unclear if this is supposed to actually allow a moment object, or undefined, or if this is just - // a bug... should be investigated. This should probably be the TimeRange type, but most TimeRange interfaces - // don't account for the possibility of the moment object, and it is actually a possibility. - to: string | moment.Moment | undefined; - from: string | moment.Moment | undefined; - }; - getTime: () => { - to: string | moment.Moment | undefined; - from: string | moment.Moment | undefined; - }; - setTime: ( - timeRange: { - to: string | moment.Moment | undefined; - from: string | moment.Moment | undefined; - } - ) => void; + time: TimeRange; + getTime: () => TimeRange; + setTime: (timeRange: TimeRange) => void; setRefreshInterval: (refreshInterval: RefreshInterval) => void; getRefreshInterval: () => RefreshInterval; disableAutoRefreshSelector: () => void; diff --git a/src/legacy/ui/public/url/kbn_url.d.ts b/src/legacy/ui/public/url/kbn_url.d.ts new file mode 100644 index 000000000000..42b6a8f19f9a --- /dev/null +++ b/src/legacy/ui/public/url/kbn_url.d.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface KbnUrl { + change: (url: string) => void; + removeParam: (param: string) => void; +}