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
This commit is contained in:
parent
2d9975c147
commit
156467e535
|
@ -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 = (
|
||||
<DashboardSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => {}}
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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<IndexPattern>;
|
||||
};
|
||||
dashboardConfig: any;
|
||||
localStorage: any;
|
||||
Private: IPrivate;
|
||||
kbnUrl: KbnUrl;
|
||||
AppStateClass: TAppStateClass<DashboardAppState>;
|
||||
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 = (
|
||||
<DashboardSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => {}}
|
||||
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<TAppStateClass<DashboardAppState>>('AppState');
|
||||
const kbnUrl = $injector.get<KbnUrl>('kbnUrl');
|
||||
const confirmModal = $injector.get<ConfirmModalFn>('confirmModal');
|
||||
const config = $injector.get('config');
|
||||
const courier = $injector.get<{ fetch: () => void }>('courier');
|
||||
|
||||
const Private = $injector.get<IPrivate>('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<IndexPattern>;
|
||||
}>('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,
|
||||
}),
|
||||
};
|
||||
});
|
|
@ -621,10 +621,12 @@ export class DashboardStateManager {
|
|||
);
|
||||
}
|
||||
|
||||
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.<Object>} 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,7 +21,6 @@ exports[`renders DashboardSaveModal 1`] = `
|
|||
labelType="label"
|
||||
>
|
||||
<EuiTextArea
|
||||
compressed={true}
|
||||
data-test-subj="dashboardDescription"
|
||||
fullWidth={false}
|
||||
onChange={[Function]}
|
||||
|
|
|
@ -18,12 +18,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
|
||||
import { toastNotifications, Toast } from 'ui/notify';
|
||||
import {
|
||||
SavedObjectFinder,
|
||||
SavedObjectMetaData,
|
||||
} from 'ui/saved_objects/components/saved_object_finder';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
|
@ -35,9 +37,20 @@ import {
|
|||
EuiButton,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { SavedObjectAttributes } from 'src/legacy/server/saved_objects';
|
||||
import { EmbeddableFactoryRegistry } from '../types';
|
||||
|
||||
export class DashboardAddPanel extends React.Component {
|
||||
onAddPanel = (id, type, name) => {
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
addNewPanel: (id: string, type: string) => void;
|
||||
addNewVis: () => void;
|
||||
embeddableFactories: EmbeddableFactoryRegistry;
|
||||
}
|
||||
|
||||
export class DashboardAddPanel extends React.Component<Props> {
|
||||
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 {
|
|||
<EuiFlyoutBody>
|
||||
<SavedObjectFinder
|
||||
onChoose={this.onAddPanel}
|
||||
savedObjectMetaData={this.props.embeddableFactories
|
||||
savedObjectMetaData={
|
||||
this.props.embeddableFactories
|
||||
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData)}
|
||||
.map(({ savedObjectMetaData }) => savedObjectMetaData) as Array<
|
||||
SavedObjectMetaData<SavedObjectAttributes>
|
||||
>
|
||||
}
|
||||
showFilter={true}
|
||||
noItemsMessage={i18n.translate(
|
||||
'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
|
||||
|
@ -88,11 +105,15 @@ export class DashboardAddPanel extends React.Component {
|
|||
)}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
{ capabilities.get().visualize.save ? (
|
||||
{capabilities.get().visualize.save ? (
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill onClick={this.props.addNewVis} data-test-subj="addNewSavedObjectLink">
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={this.props.addNewVis}
|
||||
data-test-subj="addNewSavedObjectLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
|
||||
defaultMessage="Create new visualization"
|
||||
|
@ -101,14 +122,8 @@ export class DashboardAddPanel extends React.Component {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
) : null }
|
||||
) : null}
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DashboardAddPanel.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
addNewPanel: PropTypes.func.isRequired,
|
||||
addNewVis: PropTypes.func.isRequired,
|
||||
};
|
|
@ -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<void>;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
interface State {
|
||||
newDashboardName: string;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
hasTitleDuplicate: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
class DashboardCloneModalUi extends React.Component<Props, State> {
|
||||
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 {
|
|||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={this.props.intl.formatMessage({
|
||||
title={this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'kbn.dashboard.topNav.cloneModal.dashboardExistsTitle',
|
||||
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
newDashboardName: `'${this.state.newDashboardName}'`,
|
||||
})}
|
||||
}
|
||||
)}
|
||||
color="warning"
|
||||
data-test-subj="titleDupicateWarnMsg"
|
||||
>
|
||||
|
@ -122,7 +148,7 @@ class DashboardCloneModalUi extends React.Component {
|
|||
</EuiCallOut>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
@ -162,14 +188,10 @@ class DashboardCloneModalUi extends React.Component {
|
|||
/>
|
||||
|
||||
{this.renderDuplicateTitleCallout()}
|
||||
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="cloneCancelButton"
|
||||
onClick={this.props.onClose}
|
||||
>
|
||||
<EuiButtonEmpty data-test-subj="cloneCancelButton" onClick={this.props.onClose}>
|
||||
<FormattedMessage
|
||||
id="kbn.dashboard.topNav.cloneModal.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
|
@ -194,10 +216,4 @@ class DashboardCloneModalUi extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
DashboardCloneModalUi.propTypes = {
|
||||
onClone: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export const DashboardCloneModal = injectI18n(DashboardCloneModalUi);
|
|
@ -20,6 +20,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { DashboardViewMode } from '../dashboard_view_mode';
|
||||
import { TopNavIds } from './top_nav_ids';
|
||||
import { NavAction } from '../types';
|
||||
|
||||
/**
|
||||
* @param {DashboardMode} dashboardMode.
|
||||
|
@ -29,12 +30,15 @@ import { TopNavIds } from './top_nav_ids';
|
|||
* @return {Array<kbnTopNavConfig>} - 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]),
|
||||
]
|
||||
|
@ -42,22 +46,22 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
|
|||
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',
|
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<EuiForm
|
||||
data-test-subj="dashboardOptionsMenu"
|
||||
>
|
||||
|
||||
<EuiForm data-test-subj="dashboardOptionsMenu">
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={this.props.intl.formatMessage({
|
||||
|
@ -75,17 +83,9 @@ class OptionsMenuUi extends Component {
|
|||
data-test-subj="dashboardPanelTitlesCheckbox"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionsMenuUi.propTypes = {
|
||||
useMargins: PropTypes.bool.isRequired,
|
||||
onUseMarginsChange: PropTypes.func.isRequired,
|
||||
hidePanelTitles: PropTypes.bool.isRequired,
|
||||
onHidePanelTitlesChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const OptionsMenu = injectI18n(OptionsMenuUi);
|
|
@ -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) {
|
||||
super(props);
|
||||
interface SaveOptions {
|
||||
newTitle: string;
|
||||
newDescription: string;
|
||||
newCopyOnSave: boolean;
|
||||
newTimeRestore: boolean;
|
||||
isTitleDuplicateConfirmed: boolean;
|
||||
onTitleDuplicate: () => void;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
description: props.description,
|
||||
timeRestore: props.timeRestore,
|
||||
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<Props, State> {
|
||||
state: State = {
|
||||
description: this.props.description,
|
||||
timeRestore: this.props.timeRestore,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={<FormattedMessage
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="kbn.dashboard.topNav.saveModal.descriptionFormRowLabel"
|
||||
defaultMessage="Description"
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="dashboardDescription"
|
||||
value={this.state.description}
|
||||
onChange={this.onDescriptionChange}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
helpText={<FormattedMessage
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
|
||||
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="storeTimeWithDashboard"
|
||||
checked={this.state.timeRestore}
|
||||
onChange={this.onTimeRestoreChange}
|
||||
label={<FormattedMessage
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
|
||||
defaultMessage="Store time with dashboard"
|
||||
/>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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) {
|
||||
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 = (
|
|
@ -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 = (
|
||||
<I18nContext>
|
||||
<EuiWrappingPopover
|
||||
id="popover"
|
||||
button={anchorElement}
|
||||
isOpen={true}
|
||||
closePopover={onClose}
|
||||
>
|
||||
<EuiWrappingPopover id="popover" button={anchorElement} isOpen={true} closePopover={onClose}>
|
||||
<OptionsMenu
|
||||
useMargins={useMargins}
|
||||
onUseMarginsChange={onUseMarginsChange}
|
|
@ -17,11 +17,18 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Query } from 'ui/embeddable';
|
||||
import { Query, EmbeddableFactory } from 'ui/embeddable';
|
||||
import { AppState } from 'ui/state_management/app_state';
|
||||
import { UIRegistry } from 'ui/registry/_registry';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { DashboardViewMode } from './dashboard_view_mode';
|
||||
|
||||
export interface EmbeddableFactoryRegistry extends UIRegistry<EmbeddableFactory> {
|
||||
byName: { [key: string]: EmbeddableFactory };
|
||||
}
|
||||
|
||||
export type NavAction = (menuItem: any, navController: any, anchorElement: any) => void;
|
||||
|
||||
export interface GridData {
|
||||
w: number;
|
||||
h: number;
|
||||
|
|
2
src/legacy/ui/public/chrome/index.d.ts
vendored
2
src/legacy/ui/public/chrome/index.d.ts
vendored
|
@ -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<T>(injectable: string): T;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ShareActionProvider>({
|
||||
name: 'shareContextMenuExtensions',
|
||||
index: ['id'],
|
||||
});
|
||||
|
|
21
src/legacy/ui/public/timefilter/timefilter.d.ts
vendored
21
src/legacy/ui/public/timefilter/timefilter.d.ts
vendored
|
@ -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;
|
||||
|
|
23
src/legacy/ui/public/url/kbn_url.d.ts
vendored
Normal file
23
src/legacy/ui/public/url/kbn_url.d.ts
vendored
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue