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 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeFilter.setTime({
|
if (this.savedDashboard.timeFrom && this.savedDashboard.timeTo) {
|
||||||
from: this.savedDashboard.timeFrom,
|
timeFilter.setTime({
|
||||||
to: this.savedDashboard.timeTo,
|
from: this.savedDashboard.timeFrom,
|
||||||
});
|
to: this.savedDashboard.timeTo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.savedDashboard.refreshInterval) {
|
if (this.savedDashboard.refreshInterval) {
|
||||||
timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval);
|
timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval);
|
||||||
|
@ -642,7 +644,7 @@ export class DashboardStateManager {
|
||||||
* Applies the current filter state to the dashboard.
|
* Applies the current filter state to the dashboard.
|
||||||
* @param filter {Array.<Object>} An array of filter bar filters.
|
* @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.appState.query = query;
|
||||||
this.savedDashboard.searchSource.setField('query', query);
|
this.savedDashboard.searchSource.setField('query', query);
|
||||||
this.savedDashboard.searchSource.setField('filter', filters);
|
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
|
* 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).
|
* '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}
|
* @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.
|
* 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()) {
|
if (moment(time).isValid()) {
|
||||||
return moment(time).utc();
|
return moment(time)
|
||||||
|
.utc()
|
||||||
|
.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
|
||||||
} else {
|
} 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;
|
id?: string;
|
||||||
copyOnSave: boolean;
|
copyOnSave: boolean;
|
||||||
timeRestore: boolean;
|
timeRestore: boolean;
|
||||||
// These optionally being moment objects rather than strings seems more like a bug than by design. It's due to
|
timeTo?: string;
|
||||||
// some code in udpate_saved_dashboard that should probably get cleaned up.
|
timeFrom?: string;
|
||||||
timeTo: string | moment.Moment | undefined;
|
|
||||||
timeFrom: string | moment.Moment | undefined;
|
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
panelsJSON: string;
|
panelsJSON: string;
|
||||||
optionsJSON: string | undefined;
|
optionsJSON?: string;
|
||||||
// TODO: write a migration to rid of this, it's only around for bwc.
|
// TODO: write a migration to rid of this, it's only around for bwc.
|
||||||
uiStateJSON?: string;
|
uiStateJSON?: string;
|
||||||
lastSavedTitle: string;
|
lastSavedTitle: string;
|
||||||
|
|
|
@ -21,7 +21,6 @@ exports[`renders DashboardSaveModal 1`] = `
|
||||||
labelType="label"
|
labelType="label"
|
||||||
>
|
>
|
||||||
<EuiTextArea
|
<EuiTextArea
|
||||||
compressed={true}
|
|
||||||
data-test-subj="dashboardDescription"
|
data-test-subj="dashboardDescription"
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
|
|
@ -18,12 +18,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { capabilities } from 'ui/capabilities';
|
import { capabilities } from 'ui/capabilities';
|
||||||
import { toastNotifications } from 'ui/notify';
|
import { toastNotifications, Toast } from 'ui/notify';
|
||||||
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
|
import {
|
||||||
|
SavedObjectFinder,
|
||||||
|
SavedObjectMetaData,
|
||||||
|
} from 'ui/saved_objects/components/saved_object_finder';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
|
@ -35,9 +37,20 @@ import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
EuiTitle,
|
EuiTitle,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
import { SavedObjectAttributes } from 'src/legacy/server/saved_objects';
|
||||||
|
import { EmbeddableFactoryRegistry } from '../types';
|
||||||
|
|
||||||
export class DashboardAddPanel extends React.Component {
|
interface Props {
|
||||||
onAddPanel = (id, type, name) => {
|
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);
|
this.props.addNewPanel(id, type);
|
||||||
|
|
||||||
// To avoid the clutter of having toast messages cover flyout
|
// To avoid the clutter of having toast messages cover flyout
|
||||||
|
@ -76,9 +89,13 @@ export class DashboardAddPanel extends React.Component {
|
||||||
<EuiFlyoutBody>
|
<EuiFlyoutBody>
|
||||||
<SavedObjectFinder
|
<SavedObjectFinder
|
||||||
onChoose={this.onAddPanel}
|
onChoose={this.onAddPanel}
|
||||||
savedObjectMetaData={this.props.embeddableFactories
|
savedObjectMetaData={
|
||||||
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
|
this.props.embeddableFactories
|
||||||
.map(({ savedObjectMetaData }) => savedObjectMetaData)}
|
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
|
||||||
|
.map(({ savedObjectMetaData }) => savedObjectMetaData) as Array<
|
||||||
|
SavedObjectMetaData<SavedObjectAttributes>
|
||||||
|
>
|
||||||
|
}
|
||||||
showFilter={true}
|
showFilter={true}
|
||||||
noItemsMessage={i18n.translate(
|
noItemsMessage={i18n.translate(
|
||||||
'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
|
'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
|
||||||
|
@ -88,11 +105,15 @@ export class DashboardAddPanel extends React.Component {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</EuiFlyoutBody>
|
</EuiFlyoutBody>
|
||||||
{ capabilities.get().visualize.save ? (
|
{capabilities.get().visualize.save ? (
|
||||||
<EuiFlyoutFooter>
|
<EuiFlyoutFooter>
|
||||||
<EuiFlexGroup justifyContent="flexEnd">
|
<EuiFlexGroup justifyContent="flexEnd">
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButton fill onClick={this.props.addNewVis} data-test-subj="addNewSavedObjectLink">
|
<EuiButton
|
||||||
|
fill
|
||||||
|
onClick={this.props.addNewVis}
|
||||||
|
data-test-subj="addNewSavedObjectLink"
|
||||||
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
|
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
|
||||||
defaultMessage="Create new visualization"
|
defaultMessage="Create new visualization"
|
||||||
|
@ -101,14 +122,8 @@ export class DashboardAddPanel extends React.Component {
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
</EuiFlyoutFooter>
|
</EuiFlyoutFooter>
|
||||||
) : null }
|
) : null}
|
||||||
</EuiFlyout>
|
</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 React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EuiButton,
|
EuiButton,
|
||||||
|
@ -36,8 +35,28 @@ import {
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
|
|
||||||
class DashboardCloneModalUi extends React.Component {
|
interface Props {
|
||||||
constructor(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);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -48,11 +67,11 @@ class DashboardCloneModalUi extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._isMounted = true;
|
this.isMounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this.isMounted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onTitleDuplicate = () => {
|
onTitleDuplicate = () => {
|
||||||
|
@ -60,23 +79,27 @@ class DashboardCloneModalUi extends React.Component {
|
||||||
isTitleDuplicateConfirmed: true,
|
isTitleDuplicateConfirmed: true,
|
||||||
hasTitleDuplicate: true,
|
hasTitleDuplicate: true,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
cloneDashboard = async () => {
|
cloneDashboard = async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true,
|
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({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onInputChange = (event) => {
|
onInputChange = (event: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
newDashboardName: event.target.value,
|
newDashboardName: event.target.value,
|
||||||
isTitleDuplicateConfirmed: false,
|
isTitleDuplicateConfirmed: false,
|
||||||
|
@ -94,12 +117,15 @@ class DashboardCloneModalUi extends React.Component {
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
size="s"
|
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.',
|
id: 'kbn.dashboard.topNav.cloneModal.dashboardExistsTitle',
|
||||||
}, {
|
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
|
||||||
newDashboardName: `'${this.state.newDashboardName}'`,
|
},
|
||||||
})}
|
{
|
||||||
|
newDashboardName: `'${this.state.newDashboardName}'`,
|
||||||
|
}
|
||||||
|
)}
|
||||||
color="warning"
|
color="warning"
|
||||||
data-test-subj="titleDupicateWarnMsg"
|
data-test-subj="titleDupicateWarnMsg"
|
||||||
>
|
>
|
||||||
|
@ -122,7 +148,7 @@ class DashboardCloneModalUi extends React.Component {
|
||||||
</EuiCallOut>
|
</EuiCallOut>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
@ -162,14 +188,10 @@ class DashboardCloneModalUi extends React.Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{this.renderDuplicateTitleCallout()}
|
{this.renderDuplicateTitleCallout()}
|
||||||
|
|
||||||
</EuiModalBody>
|
</EuiModalBody>
|
||||||
|
|
||||||
<EuiModalFooter>
|
<EuiModalFooter>
|
||||||
<EuiButtonEmpty
|
<EuiButtonEmpty data-test-subj="cloneCancelButton" onClick={this.props.onClose}>
|
||||||
data-test-subj="cloneCancelButton"
|
|
||||||
onClick={this.props.onClose}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="kbn.dashboard.topNav.cloneModal.cancelButtonLabel"
|
id="kbn.dashboard.topNav.cloneModal.cancelButtonLabel"
|
||||||
defaultMessage="Cancel"
|
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);
|
export const DashboardCloneModal = injectI18n(DashboardCloneModalUi);
|
|
@ -20,6 +20,7 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { DashboardViewMode } from '../dashboard_view_mode';
|
import { DashboardViewMode } from '../dashboard_view_mode';
|
||||||
import { TopNavIds } from './top_nav_ids';
|
import { TopNavIds } from './top_nav_ids';
|
||||||
|
import { NavAction } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DashboardMode} dashboardMode.
|
* @param {DashboardMode} dashboardMode.
|
||||||
|
@ -29,35 +30,38 @@ import { TopNavIds } from './top_nav_ids';
|
||||||
* @return {Array<kbnTopNavConfig>} - Returns an array of objects for a top nav configuration, based on the
|
* @return {Array<kbnTopNavConfig>} - Returns an array of objects for a top nav configuration, based on the
|
||||||
* mode.
|
* mode.
|
||||||
*/
|
*/
|
||||||
export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
|
export function getTopNavConfig(
|
||||||
|
dashboardMode: DashboardViewMode,
|
||||||
|
actions: { [key: string]: NavAction },
|
||||||
|
hideWriteControls: boolean
|
||||||
|
) {
|
||||||
switch (dashboardMode) {
|
switch (dashboardMode) {
|
||||||
case DashboardViewMode.VIEW:
|
case DashboardViewMode.VIEW:
|
||||||
return (
|
return hideWriteControls
|
||||||
hideWriteControls ?
|
? [
|
||||||
[
|
|
||||||
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
|
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
|
||||||
getShareConfig(actions[TopNavIds.SHARE]),
|
getShareConfig(actions[TopNavIds.SHARE]),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
|
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
|
||||||
getShareConfig(actions[TopNavIds.SHARE]),
|
getShareConfig(actions[TopNavIds.SHARE]),
|
||||||
getCloneConfig(actions[TopNavIds.CLONE]),
|
getCloneConfig(actions[TopNavIds.CLONE]),
|
||||||
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE])
|
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]),
|
||||||
]
|
];
|
||||||
);
|
|
||||||
case DashboardViewMode.EDIT:
|
case DashboardViewMode.EDIT:
|
||||||
return [
|
return [
|
||||||
getSaveConfig(actions[TopNavIds.SAVE]),
|
getSaveConfig(actions[TopNavIds.SAVE]),
|
||||||
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
|
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
|
||||||
getAddConfig(actions[TopNavIds.ADD]),
|
getAddConfig(actions[TopNavIds.ADD]),
|
||||||
getOptionsConfig(actions[TopNavIds.OPTIONS]),
|
getOptionsConfig(actions[TopNavIds.OPTIONS]),
|
||||||
getShareConfig(actions[TopNavIds.SHARE])];
|
getShareConfig(actions[TopNavIds.SHARE]),
|
||||||
|
];
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFullScreenConfig(action) {
|
function getFullScreenConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', {
|
||||||
defaultMessage: 'full screen',
|
defaultMessage: 'full screen',
|
||||||
|
@ -66,14 +70,14 @@ function getFullScreenConfig(action) {
|
||||||
defaultMessage: 'Full Screen Mode',
|
defaultMessage: 'Full Screen Mode',
|
||||||
}),
|
}),
|
||||||
testId: 'dashboardFullScreenMode',
|
testId: 'dashboardFullScreenMode',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getEditConfig(action) {
|
function getEditConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', {
|
||||||
defaultMessage: 'edit',
|
defaultMessage: 'edit',
|
||||||
|
@ -85,14 +89,14 @@ function getEditConfig(action) {
|
||||||
// We want to hide the "edit" button on small screens, since those have a responsive
|
// 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.
|
// 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',
|
className: 'eui-hideFor--s eui-hideFor--xs',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getSaveConfig(action) {
|
function getSaveConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', {
|
||||||
defaultMessage: 'save',
|
defaultMessage: 'save',
|
||||||
|
@ -101,14 +105,14 @@ function getSaveConfig(action) {
|
||||||
defaultMessage: 'Save your dashboard',
|
defaultMessage: 'Save your dashboard',
|
||||||
}),
|
}),
|
||||||
testId: 'dashboardSaveMenuItem',
|
testId: 'dashboardSaveMenuItem',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getViewConfig(action) {
|
function getViewConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', {
|
||||||
defaultMessage: 'cancel',
|
defaultMessage: 'cancel',
|
||||||
|
@ -117,14 +121,14 @@ function getViewConfig(action) {
|
||||||
defaultMessage: 'Cancel editing and switch to view-only mode',
|
defaultMessage: 'Cancel editing and switch to view-only mode',
|
||||||
}),
|
}),
|
||||||
testId: 'dashboardViewOnlyMode',
|
testId: 'dashboardViewOnlyMode',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getCloneConfig(action) {
|
function getCloneConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', {
|
||||||
defaultMessage: 'clone',
|
defaultMessage: 'clone',
|
||||||
|
@ -133,14 +137,14 @@ function getCloneConfig(action) {
|
||||||
defaultMessage: 'Create a copy of your dashboard',
|
defaultMessage: 'Create a copy of your dashboard',
|
||||||
}),
|
}),
|
||||||
testId: 'dashboardClone',
|
testId: 'dashboardClone',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getAddConfig(action) {
|
function getAddConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', {
|
||||||
defaultMessage: 'add',
|
defaultMessage: 'add',
|
||||||
|
@ -149,14 +153,14 @@ function getAddConfig(action) {
|
||||||
defaultMessage: 'Add a panel to the dashboard',
|
defaultMessage: 'Add a panel to the dashboard',
|
||||||
}),
|
}),
|
||||||
testId: 'dashboardAddPanelButton',
|
testId: 'dashboardAddPanelButton',
|
||||||
run: action
|
run: action,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getShareConfig(action) {
|
function getShareConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', {
|
||||||
defaultMessage: 'share',
|
defaultMessage: 'share',
|
||||||
|
@ -172,7 +176,7 @@ function getShareConfig(action) {
|
||||||
/**
|
/**
|
||||||
* @returns {kbnTopNavConfig}
|
* @returns {kbnTopNavConfig}
|
||||||
*/
|
*/
|
||||||
function getOptionsConfig(action) {
|
function getOptionsConfig(action: NavAction) {
|
||||||
return {
|
return {
|
||||||
key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', {
|
key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', {
|
||||||
defaultMessage: 'options',
|
defaultMessage: 'options',
|
|
@ -18,40 +18,48 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
|
||||||
import { injectI18n } from '@kbn/i18n/react';
|
|
||||||
|
|
||||||
import {
|
import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||||
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 = {
|
state = {
|
||||||
useMargins: this.props.useMargins,
|
useMargins: this.props.useMargins,
|
||||||
hidePanelTitles: this.props.hidePanelTitles,
|
hidePanelTitles: this.props.hidePanelTitles,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUseMarginsChange = (evt) => {
|
handleUseMarginsChange = (evt: any) => {
|
||||||
const isChecked = evt.target.checked;
|
const isChecked = evt.target.checked;
|
||||||
this.props.onUseMarginsChange(isChecked);
|
this.props.onUseMarginsChange(isChecked);
|
||||||
this.setState({ useMargins: isChecked });
|
this.setState({ useMargins: isChecked });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleHidePanelTitlesChange = (evt) => {
|
handleHidePanelTitlesChange = (evt: any) => {
|
||||||
const isChecked = !evt.target.checked;
|
const isChecked = !evt.target.checked;
|
||||||
this.props.onHidePanelTitlesChange(isChecked);
|
this.props.onHidePanelTitlesChange(isChecked);
|
||||||
this.setState({ hidePanelTitles: isChecked });
|
this.setState({ hidePanelTitles: isChecked });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<EuiForm
|
<EuiForm data-test-subj="dashboardOptionsMenu">
|
||||||
data-test-subj="dashboardOptionsMenu"
|
|
||||||
>
|
|
||||||
|
|
||||||
<EuiFormRow>
|
<EuiFormRow>
|
||||||
<EuiSwitch
|
<EuiSwitch
|
||||||
label={this.props.intl.formatMessage({
|
label={this.props.intl.formatMessage({
|
||||||
|
@ -75,17 +83,9 @@ class OptionsMenuUi extends Component {
|
||||||
data-test-subj="dashboardPanelTitlesCheckbox"
|
data-test-subj="dashboardPanelTitlesCheckbox"
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
|
|
||||||
</EuiForm>
|
</EuiForm>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionsMenuUi.propTypes = {
|
|
||||||
useMargins: PropTypes.bool.isRequired,
|
|
||||||
onUseMarginsChange: PropTypes.func.isRequired,
|
|
||||||
hidePanelTitles: PropTypes.bool.isRequired,
|
|
||||||
onHidePanelTitlesChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OptionsMenu = injectI18n(OptionsMenuUi);
|
export const OptionsMenu = injectI18n(OptionsMenuUi);
|
|
@ -18,27 +18,65 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
|
||||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
|
|
||||||
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
|
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
|
||||||
import {
|
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
|
||||||
EuiFormRow,
|
|
||||||
EuiTextArea,
|
|
||||||
EuiSwitch,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
|
|
||||||
class DashboardSaveModalUi extends React.Component {
|
interface SaveOptions {
|
||||||
constructor(props) {
|
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<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
description: this.props.description,
|
||||||
|
timeRestore: this.props.timeRestore,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
super(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({
|
this.props.onSave({
|
||||||
newTitle,
|
newTitle,
|
||||||
newDescription: this.state.description,
|
newDescription: this.state.description,
|
||||||
|
@ -49,13 +87,13 @@ class DashboardSaveModalUi extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onDescriptionChange = (event) => {
|
onDescriptionChange = (event: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
description: event.target.value,
|
description: event.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onTimeRestoreChange = (event) => {
|
onTimeRestoreChange = (event: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
timeRestore: event.target.checked,
|
timeRestore: event.target.checked,
|
||||||
});
|
});
|
||||||
|
@ -65,33 +103,38 @@ class DashboardSaveModalUi extends React.Component {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={<FormattedMessage
|
label={
|
||||||
id="kbn.dashboard.topNav.saveModal.descriptionFormRowLabel"
|
<FormattedMessage
|
||||||
defaultMessage="Description"
|
id="kbn.dashboard.topNav.saveModal.descriptionFormRowLabel"
|
||||||
/>}
|
defaultMessage="Description"
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<EuiTextArea
|
<EuiTextArea
|
||||||
data-test-subj="dashboardDescription"
|
data-test-subj="dashboardDescription"
|
||||||
value={this.state.description}
|
value={this.state.description}
|
||||||
onChange={this.onDescriptionChange}
|
onChange={this.onDescriptionChange}
|
||||||
compressed
|
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
|
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
helpText={<FormattedMessage
|
helpText={
|
||||||
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
|
<FormattedMessage
|
||||||
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
|
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
|
||||||
/>}
|
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<EuiSwitch
|
<EuiSwitch
|
||||||
data-test-subj="storeTimeWithDashboard"
|
data-test-subj="storeTimeWithDashboard"
|
||||||
checked={this.state.timeRestore}
|
checked={this.state.timeRestore}
|
||||||
onChange={this.onTimeRestoreChange}
|
onChange={this.onTimeRestoreChange}
|
||||||
label={<FormattedMessage
|
label={
|
||||||
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
|
<FormattedMessage
|
||||||
defaultMessage="Store time with dashboard"
|
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
|
||||||
/>}
|
defaultMessage="Store time with dashboard"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
</Fragment>
|
</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);
|
export const DashboardSaveModal = injectI18n(DashboardSaveModalUi);
|
|
@ -18,13 +18,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { I18nContext } from 'ui/i18n';
|
import { I18nContext } from 'ui/i18n';
|
||||||
import { DashboardAddPanel } from './add_panel';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
import { DashboardAddPanel } from './add_panel';
|
||||||
|
import { EmbeddableFactoryRegistry } from '../types';
|
||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
|
||||||
export function showAddPanel(addNewPanel, addNewVis, embeddableFactories) {
|
export function showAddPanel(
|
||||||
|
addNewPanel: (id: string, type: string) => void,
|
||||||
|
addNewVis: () => void,
|
||||||
|
embeddableFactories: EmbeddableFactoryRegistry
|
||||||
|
) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
|
@ -18,24 +18,39 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { I18nContext } from 'ui/i18n';
|
import { I18nContext } from 'ui/i18n';
|
||||||
import { DashboardCloneModal } from './clone_modal';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { i18n } from '@kbn/i18n';
|
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 container = document.createElement('div');
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
ReactDOM.unmountComponentAtNode(container);
|
ReactDOM.unmountComponentAtNode(container);
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloneConfirmed = (newTitle, isTitleDuplicateConfirmed, onTitleDuplicate) => {
|
const onCloneConfirmed = async (
|
||||||
onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then(({ id, error }) => {
|
newTitle: string,
|
||||||
if (id || error) {
|
isTitleDuplicateConfirmed: boolean,
|
||||||
closeModal();
|
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);
|
document.body.appendChild(container);
|
||||||
const element = (
|
const element = (
|
|
@ -21,12 +21,9 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { I18nContext } from 'ui/i18n';
|
import { I18nContext } from 'ui/i18n';
|
||||||
|
|
||||||
|
import { EuiWrappingPopover } from '@elastic/eui';
|
||||||
import { OptionsMenu } from './options';
|
import { OptionsMenu } from './options';
|
||||||
|
|
||||||
import {
|
|
||||||
EuiWrappingPopover,
|
|
||||||
} from '@elastic/eui';
|
|
||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
@ -42,6 +39,12 @@ export function showOptionsPopover({
|
||||||
onUseMarginsChange,
|
onUseMarginsChange,
|
||||||
hidePanelTitles,
|
hidePanelTitles,
|
||||||
onHidePanelTitlesChange,
|
onHidePanelTitlesChange,
|
||||||
|
}: {
|
||||||
|
anchorElement: HTMLElement;
|
||||||
|
useMargins: boolean;
|
||||||
|
onUseMarginsChange: (useMargins: boolean) => void;
|
||||||
|
hidePanelTitles: boolean;
|
||||||
|
onHidePanelTitlesChange: (hideTitles: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -53,12 +56,7 @@ export function showOptionsPopover({
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
const element = (
|
const element = (
|
||||||
<I18nContext>
|
<I18nContext>
|
||||||
<EuiWrappingPopover
|
<EuiWrappingPopover id="popover" button={anchorElement} isOpen={true} closePopover={onClose}>
|
||||||
id="popover"
|
|
||||||
button={anchorElement}
|
|
||||||
isOpen={true}
|
|
||||||
closePopover={onClose}
|
|
||||||
>
|
|
||||||
<OptionsMenu
|
<OptionsMenu
|
||||||
useMargins={useMargins}
|
useMargins={useMargins}
|
||||||
onUseMarginsChange={onUseMarginsChange}
|
onUseMarginsChange={onUseMarginsChange}
|
|
@ -17,11 +17,18 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Query } from 'ui/embeddable';
|
import { Query, EmbeddableFactory } from 'ui/embeddable';
|
||||||
import { AppState } from 'ui/state_management/app_state';
|
import { AppState } from 'ui/state_management/app_state';
|
||||||
|
import { UIRegistry } from 'ui/registry/_registry';
|
||||||
import { Filter } from '@kbn/es-query';
|
import { Filter } from '@kbn/es-query';
|
||||||
import { DashboardViewMode } from './dashboard_view_mode';
|
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 {
|
export interface GridData {
|
||||||
w: number;
|
w: number;
|
||||||
h: 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 { HelpExtensionApi } from './api/help_extension';
|
||||||
import { ChromeNavLinks } from './api/nav';
|
import { ChromeNavLinks } from './api/nav';
|
||||||
|
|
||||||
interface IInjector {
|
export interface IInjector {
|
||||||
get<T>(injectable: string): T;
|
get<T>(injectable: string): T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,9 @@
|
||||||
|
|
||||||
// @ts-ignore: implicit any for JS file
|
// @ts-ignore: implicit any for JS file
|
||||||
import { uiRegistry } from 'ui/registry/_registry';
|
import { uiRegistry } from 'ui/registry/_registry';
|
||||||
|
import { ShareActionProvider } from './share_action';
|
||||||
|
|
||||||
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({
|
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry<ShareActionProvider>({
|
||||||
name: 'shareContextMenuExtensions',
|
name: 'shareContextMenuExtensions',
|
||||||
index: ['id'],
|
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 { Moment } from 'moment';
|
||||||
import { TimeRange } from './time_history';
|
import { TimeRange } from './time_history';
|
||||||
import moment = require('moment');
|
|
||||||
|
|
||||||
// NOTE: These types are somewhat guessed, they may be incorrect.
|
// NOTE: These types are somewhat guessed, they may be incorrect.
|
||||||
|
|
||||||
|
@ -29,23 +28,9 @@ export interface RefreshInterval {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Timefilter {
|
export interface Timefilter {
|
||||||
time: {
|
time: TimeRange;
|
||||||
// NOTE: It's unclear if this is supposed to actually allow a moment object, or undefined, or if this is just
|
getTime: () => TimeRange;
|
||||||
// a bug... should be investigated. This should probably be the TimeRange type, but most TimeRange interfaces
|
setTime: (timeRange: TimeRange) => void;
|
||||||
// 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;
|
|
||||||
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
|
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
|
||||||
getRefreshInterval: () => RefreshInterval;
|
getRefreshInterval: () => RefreshInterval;
|
||||||
disableAutoRefreshSelector: () => void;
|
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