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:
Stacey Gammon 2019-06-14 12:51:11 -04:00 committed by GitHub
parent 2d9975c147
commit 156467e535
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1063 additions and 728 deletions

View file

@ -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);
}
}
};
});

View file

@ -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,
}),
};
});

View file

@ -621,10 +621,12 @@ export class DashboardStateManager {
);
}
timeFilter.setTime({
from: this.savedDashboard.timeFrom,
to: this.savedDashboard.timeTo,
});
if (this.savedDashboard.timeFrom && this.savedDashboard.timeTo) {
timeFilter.setTime({
from: this.savedDashboard.timeFrom,
to: this.savedDashboard.timeTo,
});
}
if (this.savedDashboard.refreshInterval) {
timeFilter.setRefreshInterval(this.savedDashboard.refreshInterval);
@ -642,7 +644,7 @@ export class DashboardStateManager {
* Applies the current filter state to the dashboard.
* @param filter {Array.<Object>} An array of filter bar filters.
*/
public applyFilters(query: Query, filters: Filter[]) {
public applyFilters(query: Query | string, filters: Filter[]) {
this.appState.query = query;
this.savedDashboard.searchSource.setField('query', query);
this.savedDashboard.searchSource.setField('filter', filters);

View file

@ -78,17 +78,19 @@ export class FilterUtils {
/**
* Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like
* 'now-15m', then it just returns what it was passed).
* Note** Changing these moment objects to a utc string will actually cause a bug because it'll be in a format not
* expected by the time picker. This should get cleaned up and we should pick a single format to use everywhere.
* @param time {string|Moment}
* @returns {string} the time represented in utc format, or if the time range was not able to be parsed into a moment
* @returns the time represented in utc format, or if the time range was not able to be parsed into a moment
* object, it returns the same object it was given.
*/
public static convertTimeToUTCString(time?: string | Moment): undefined | string | moment.Moment {
public static convertTimeToUTCString(time?: string | Moment): undefined | string {
if (moment(time).isValid()) {
return moment(time).utc();
return moment(time)
.utc()
.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]');
} else {
return time;
// If it's not a valid moment date, then it should be a string representing a relative time
// like 'now' or 'now-15m'.
return time as string;
}
}

View file

@ -26,14 +26,12 @@ export interface SavedObjectDashboard extends SavedObject {
id?: string;
copyOnSave: boolean;
timeRestore: boolean;
// These optionally being moment objects rather than strings seems more like a bug than by design. It's due to
// some code in udpate_saved_dashboard that should probably get cleaned up.
timeTo: string | moment.Moment | undefined;
timeFrom: string | moment.Moment | undefined;
timeTo?: string;
timeFrom?: string;
title: string;
description?: string;
panelsJSON: string;
optionsJSON: string | undefined;
optionsJSON?: string;
// TODO: write a migration to rid of this, it's only around for bwc.
uiStateJSON?: string;
lastSavedTitle: string;

View file

@ -21,7 +21,6 @@ exports[`renders DashboardSaveModal 1`] = `
labelType="label"
>
<EuiTextArea
compressed={true}
data-test-subj="dashboardDescription"
fullWidth={false}
onChange={[Function]}

View file

@ -18,12 +18,14 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { capabilities } from 'ui/capabilities';
import { toastNotifications } from 'ui/notify';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import { toastNotifications, Toast } from 'ui/notify';
import {
SavedObjectFinder,
SavedObjectMetaData,
} from 'ui/saved_objects/components/saved_object_finder';
import {
EuiFlexGroup,
@ -35,9 +37,20 @@ import {
EuiButton,
EuiTitle,
} from '@elastic/eui';
import { SavedObjectAttributes } from 'src/legacy/server/saved_objects';
import { EmbeddableFactoryRegistry } from '../types';
export class DashboardAddPanel extends React.Component {
onAddPanel = (id, type, name) => {
interface Props {
onClose: () => void;
addNewPanel: (id: string, type: string) => void;
addNewVis: () => void;
embeddableFactories: EmbeddableFactoryRegistry;
}
export class DashboardAddPanel extends React.Component<Props> {
private lastToast?: Toast;
onAddPanel = (id: string, type: string, name: string) => {
this.props.addNewPanel(id, type);
// To avoid the clutter of having toast messages cover flyout
@ -76,9 +89,13 @@ export class DashboardAddPanel extends React.Component {
<EuiFlyoutBody>
<SavedObjectFinder
onChoose={this.onAddPanel}
savedObjectMetaData={this.props.embeddableFactories
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
.map(({ savedObjectMetaData }) => savedObjectMetaData)}
savedObjectMetaData={
this.props.embeddableFactories
.filter(embeddableFactory => Boolean(embeddableFactory.savedObjectMetaData))
.map(({ savedObjectMetaData }) => savedObjectMetaData) as Array<
SavedObjectMetaData<SavedObjectAttributes>
>
}
showFilter={true}
noItemsMessage={i18n.translate(
'kbn.dashboard.topNav.addPanel.noMatchingObjectsMessage',
@ -88,11 +105,15 @@ export class DashboardAddPanel extends React.Component {
)}
/>
</EuiFlyoutBody>
{ capabilities.get().visualize.save ? (
{capabilities.get().visualize.save ? (
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton fill onClick={this.props.addNewVis} data-test-subj="addNewSavedObjectLink">
<EuiButton
fill
onClick={this.props.addNewVis}
data-test-subj="addNewSavedObjectLink"
>
<FormattedMessage
id="kbn.dashboard.topNav.addPanel.createNewVisualizationButtonLabel"
defaultMessage="Create new visualization"
@ -101,14 +122,8 @@ export class DashboardAddPanel extends React.Component {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
) : null }
) : null}
</EuiFlyout>
);
}
}
DashboardAddPanel.propTypes = {
onClose: PropTypes.func.isRequired,
addNewPanel: PropTypes.func.isRequired,
addNewVis: PropTypes.func.isRequired,
};

View file

@ -18,8 +18,7 @@
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
import {
EuiButton,
@ -36,8 +35,28 @@ import {
EuiCallOut,
} from '@elastic/eui';
class DashboardCloneModalUi extends React.Component {
constructor(props) {
interface Props {
onClone: (
newTitle: string,
isTitleDuplicateConfirmed: boolean,
onTitleDuplicate: () => void
) => Promise<void>;
onClose: () => void;
title: string;
intl: InjectedIntl;
}
interface State {
newDashboardName: string;
isTitleDuplicateConfirmed: boolean;
hasTitleDuplicate: boolean;
isLoading: boolean;
}
class DashboardCloneModalUi extends React.Component<Props, State> {
private isMounted = false;
constructor(props: Props) {
super(props);
this.state = {
@ -48,11 +67,11 @@ class DashboardCloneModalUi extends React.Component {
};
}
componentDidMount() {
this._isMounted = true;
this.isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
this.isMounted = false;
}
onTitleDuplicate = () => {
@ -60,23 +79,27 @@ class DashboardCloneModalUi extends React.Component {
isTitleDuplicateConfirmed: true,
hasTitleDuplicate: true,
});
}
};
cloneDashboard = async () => {
this.setState({
isLoading: true,
});
await this.props.onClone(this.state.newDashboardName, this.state.isTitleDuplicateConfirmed, this.onTitleDuplicate);
await this.props.onClone(
this.state.newDashboardName,
this.state.isTitleDuplicateConfirmed,
this.onTitleDuplicate
);
if (this._isMounted) {
if (this.isMounted) {
this.setState({
isLoading: false,
});
}
};
onInputChange = (event) => {
onInputChange = (event: any) => {
this.setState({
newDashboardName: event.target.value,
isTitleDuplicateConfirmed: false,
@ -94,12 +117,15 @@ class DashboardCloneModalUi extends React.Component {
<EuiSpacer />
<EuiCallOut
size="s"
title={this.props.intl.formatMessage({
id: 'kbn.dashboard.topNav.cloneModal.dashboardExistsTitle',
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
}, {
newDashboardName: `'${this.state.newDashboardName}'`,
})}
title={this.props.intl.formatMessage(
{
id: 'kbn.dashboard.topNav.cloneModal.dashboardExistsTitle',
defaultMessage: 'A dashboard with the title {newDashboardName} already exists.',
},
{
newDashboardName: `'${this.state.newDashboardName}'`,
}
)}
color="warning"
data-test-subj="titleDupicateWarnMsg"
>
@ -122,7 +148,7 @@ class DashboardCloneModalUi extends React.Component {
</EuiCallOut>
</Fragment>
);
}
};
render() {
return (
@ -162,14 +188,10 @@ class DashboardCloneModalUi extends React.Component {
/>
{this.renderDuplicateTitleCallout()}
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
data-test-subj="cloneCancelButton"
onClick={this.props.onClose}
>
<EuiButtonEmpty data-test-subj="cloneCancelButton" onClick={this.props.onClose}>
<FormattedMessage
id="kbn.dashboard.topNav.cloneModal.cancelButtonLabel"
defaultMessage="Cancel"
@ -194,10 +216,4 @@ class DashboardCloneModalUi extends React.Component {
}
}
DashboardCloneModalUi.propTypes = {
onClone: PropTypes.func,
onClose: PropTypes.func,
title: PropTypes.string
};
export const DashboardCloneModal = injectI18n(DashboardCloneModalUi);

View file

@ -20,6 +20,7 @@
import { i18n } from '@kbn/i18n';
import { DashboardViewMode } from '../dashboard_view_mode';
import { TopNavIds } from './top_nav_ids';
import { NavAction } from '../types';
/**
* @param {DashboardMode} dashboardMode.
@ -29,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
* mode.
*/
export function getTopNavConfig(dashboardMode, actions, hideWriteControls) {
export function getTopNavConfig(
dashboardMode: DashboardViewMode,
actions: { [key: string]: NavAction },
hideWriteControls: boolean
) {
switch (dashboardMode) {
case DashboardViewMode.VIEW:
return (
hideWriteControls ?
[
return hideWriteControls
? [
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
getShareConfig(actions[TopNavIds.SHARE]),
]
: [
: [
getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]),
getShareConfig(actions[TopNavIds.SHARE]),
getCloneConfig(actions[TopNavIds.CLONE]),
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE])
]
);
getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]),
];
case DashboardViewMode.EDIT:
return [
getSaveConfig(actions[TopNavIds.SAVE]),
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
getAddConfig(actions[TopNavIds.ADD]),
getOptionsConfig(actions[TopNavIds.OPTIONS]),
getShareConfig(actions[TopNavIds.SHARE])];
getShareConfig(actions[TopNavIds.SHARE]),
];
default:
return [];
}
}
function getFullScreenConfig(action) {
function getFullScreenConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', {
defaultMessage: 'full screen',
@ -66,14 +70,14 @@ function getFullScreenConfig(action) {
defaultMessage: 'Full Screen Mode',
}),
testId: 'dashboardFullScreenMode',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getEditConfig(action) {
function getEditConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', {
defaultMessage: 'edit',
@ -85,14 +89,14 @@ function getEditConfig(action) {
// We want to hide the "edit" button on small screens, since those have a responsive
// layout, which is not tied to the grid anymore, so we cannot edit the grid on that screens.
className: 'eui-hideFor--s eui-hideFor--xs',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getSaveConfig(action) {
function getSaveConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', {
defaultMessage: 'save',
@ -101,14 +105,14 @@ function getSaveConfig(action) {
defaultMessage: 'Save your dashboard',
}),
testId: 'dashboardSaveMenuItem',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getViewConfig(action) {
function getViewConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', {
defaultMessage: 'cancel',
@ -117,14 +121,14 @@ function getViewConfig(action) {
defaultMessage: 'Cancel editing and switch to view-only mode',
}),
testId: 'dashboardViewOnlyMode',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getCloneConfig(action) {
function getCloneConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', {
defaultMessage: 'clone',
@ -133,14 +137,14 @@ function getCloneConfig(action) {
defaultMessage: 'Create a copy of your dashboard',
}),
testId: 'dashboardClone',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getAddConfig(action) {
function getAddConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', {
defaultMessage: 'add',
@ -149,14 +153,14 @@ function getAddConfig(action) {
defaultMessage: 'Add a panel to the dashboard',
}),
testId: 'dashboardAddPanelButton',
run: action
run: action,
};
}
/**
* @returns {kbnTopNavConfig}
*/
function getShareConfig(action) {
function getShareConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', {
defaultMessage: 'share',
@ -172,7 +176,7 @@ function getShareConfig(action) {
/**
* @returns {kbnTopNavConfig}
*/
function getOptionsConfig(action) {
function getOptionsConfig(action: NavAction) {
return {
key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', {
defaultMessage: 'options',

View file

@ -18,40 +18,48 @@
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectI18n } from '@kbn/i18n/react';
import { injectI18n, InjectedIntl } from '@kbn/i18n/react';
import {
EuiForm,
EuiFormRow,
EuiSwitch,
} from '@elastic/eui';
import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui';
class OptionsMenuUi extends Component {
interface Props {
useMargins: boolean;
onUseMarginsChange: (useMargins: boolean) => void;
hidePanelTitles: boolean;
onHidePanelTitlesChange: (hideTitles: boolean) => void;
intl: InjectedIntl;
}
interface State {
useMargins: boolean;
hidePanelTitles: boolean;
}
class OptionsMenuUi extends Component<Props, State> {
state = {
useMargins: this.props.useMargins,
hidePanelTitles: this.props.hidePanelTitles,
};
constructor(props: Props) {
super(props);
}
handleUseMarginsChange = (evt) => {
handleUseMarginsChange = (evt: any) => {
const isChecked = evt.target.checked;
this.props.onUseMarginsChange(isChecked);
this.setState({ useMargins: isChecked });
}
};
handleHidePanelTitlesChange = (evt) => {
handleHidePanelTitlesChange = (evt: any) => {
const isChecked = !evt.target.checked;
this.props.onHidePanelTitlesChange(isChecked);
this.setState({ hidePanelTitles: isChecked });
}
};
render() {
return (
<EuiForm
data-test-subj="dashboardOptionsMenu"
>
<EuiForm data-test-subj="dashboardOptionsMenu">
<EuiFormRow>
<EuiSwitch
label={this.props.intl.formatMessage({
@ -75,17 +83,9 @@ class OptionsMenuUi extends Component {
data-test-subj="dashboardPanelTitlesCheckbox"
/>
</EuiFormRow>
</EuiForm>
);
}
}
OptionsMenuUi.propTypes = {
useMargins: PropTypes.bool.isRequired,
onUseMarginsChange: PropTypes.func.isRequired,
hidePanelTitles: PropTypes.bool.isRequired,
onHidePanelTitlesChange: PropTypes.func.isRequired,
};
export const OptionsMenu = injectI18n(OptionsMenuUi);

View file

@ -18,27 +18,65 @@
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react';
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
import {
EuiFormRow,
EuiTextArea,
EuiSwitch,
} from '@elastic/eui';
import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui';
class DashboardSaveModalUi extends React.Component {
constructor(props) {
interface SaveOptions {
newTitle: string;
newDescription: string;
newCopyOnSave: boolean;
newTimeRestore: boolean;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}
interface Props {
onSave: (
{
newTitle,
newDescription,
newCopyOnSave,
newTimeRestore,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: SaveOptions
) => void;
onClose: () => void;
title: string;
description: string;
timeRestore: boolean;
showCopyOnSave: boolean;
intl: InjectedIntl;
}
interface State {
description: string;
timeRestore: boolean;
}
class DashboardSaveModalUi extends React.Component<Props, State> {
state: State = {
description: this.props.description,
timeRestore: this.props.timeRestore,
};
constructor(props: Props) {
super(props);
this.state = {
description: props.description,
timeRestore: props.timeRestore,
};
}
saveDashboard = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => {
saveDashboard = ({
newTitle,
newCopyOnSave,
isTitleDuplicateConfirmed,
onTitleDuplicate,
}: {
newTitle: string;
newCopyOnSave: boolean;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}) => {
this.props.onSave({
newTitle,
newDescription: this.state.description,
@ -49,13 +87,13 @@ class DashboardSaveModalUi extends React.Component {
});
};
onDescriptionChange = (event) => {
onDescriptionChange = (event: any) => {
this.setState({
description: event.target.value,
});
};
onTimeRestoreChange = (event) => {
onTimeRestoreChange = (event: any) => {
this.setState({
timeRestore: event.target.checked,
});
@ -65,33 +103,38 @@ class DashboardSaveModalUi extends React.Component {
return (
<Fragment>
<EuiFormRow
label={<FormattedMessage
id="kbn.dashboard.topNav.saveModal.descriptionFormRowLabel"
defaultMessage="Description"
/>}
label={
<FormattedMessage
id="kbn.dashboard.topNav.saveModal.descriptionFormRowLabel"
defaultMessage="Description"
/>
}
>
<EuiTextArea
data-test-subj="dashboardDescription"
value={this.state.description}
onChange={this.onDescriptionChange}
compressed
/>
</EuiFormRow>
<EuiFormRow
helpText={<FormattedMessage
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
/>}
helpText={
<FormattedMessage
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
/>
}
>
<EuiSwitch
data-test-subj="storeTimeWithDashboard"
checked={this.state.timeRestore}
onChange={this.onTimeRestoreChange}
label={<FormattedMessage
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
defaultMessage="Store time with dashboard"
/>}
label={
<FormattedMessage
id="kbn.dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
defaultMessage="Store time with dashboard"
/>
}
/>
</EuiFormRow>
</Fragment>
@ -112,13 +155,4 @@ class DashboardSaveModalUi extends React.Component {
}
}
DashboardSaveModalUi.propTypes = {
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
timeRestore: PropTypes.bool.isRequired,
showCopyOnSave: PropTypes.bool.isRequired,
};
export const DashboardSaveModal = injectI18n(DashboardSaveModalUi);

View file

@ -18,13 +18,18 @@
*/
import { I18nContext } from 'ui/i18n';
import { DashboardAddPanel } from './add_panel';
import React from 'react';
import ReactDOM from 'react-dom';
import { DashboardAddPanel } from './add_panel';
import { EmbeddableFactoryRegistry } from '../types';
let isOpen = false;
export function showAddPanel(addNewPanel, addNewVis, embeddableFactories) {
export function showAddPanel(
addNewPanel: (id: string, type: string) => void,
addNewVis: () => void,
embeddableFactories: EmbeddableFactoryRegistry
) {
if (isOpen) {
return;
}

View file

@ -18,24 +18,39 @@
*/
import { I18nContext } from 'ui/i18n';
import { DashboardCloneModal } from './clone_modal';
import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { DashboardCloneModal } from './clone_modal';
export function showCloneModal(onClone, title) {
export function showCloneModal(
onClone: (
newTitle: string,
isTitleDuplicateConfirmed: boolean,
onTitleDuplicate: () => void
) => Promise<{ id?: string } | { error: Error }>,
title: string
) {
const container = document.createElement('div');
const closeModal = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
};
const onCloneConfirmed = (newTitle, isTitleDuplicateConfirmed, onTitleDuplicate) => {
onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then(({ id, error }) => {
if (id || error) {
closeModal();
const onCloneConfirmed = async (
newTitle: string,
isTitleDuplicateConfirmed: boolean,
onTitleDuplicate: () => void
) => {
onClone(newTitle, isTitleDuplicateConfirmed, onTitleDuplicate).then(
(response: { id?: string } | { error: Error }) => {
// The only time you don't want to close the modal is if it's asking you
// to confirm a duplicate title, in which case there will be no error and no id.
if ((response as { error: Error }).error || (response as { id?: string }).id) {
closeModal();
}
}
});
);
};
document.body.appendChild(container);
const element = (

View file

@ -21,12 +21,9 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { I18nContext } from 'ui/i18n';
import { EuiWrappingPopover } from '@elastic/eui';
import { OptionsMenu } from './options';
import {
EuiWrappingPopover,
} from '@elastic/eui';
let isOpen = false;
const container = document.createElement('div');
@ -42,6 +39,12 @@ export function showOptionsPopover({
onUseMarginsChange,
hidePanelTitles,
onHidePanelTitlesChange,
}: {
anchorElement: HTMLElement;
useMargins: boolean;
onUseMarginsChange: (useMargins: boolean) => void;
hidePanelTitles: boolean;
onHidePanelTitlesChange: (hideTitles: boolean) => void;
}) {
if (isOpen) {
onClose();
@ -53,12 +56,7 @@ export function showOptionsPopover({
document.body.appendChild(container);
const element = (
<I18nContext>
<EuiWrappingPopover
id="popover"
button={anchorElement}
isOpen={true}
closePopover={onClose}
>
<EuiWrappingPopover id="popover" button={anchorElement} isOpen={true} closePopover={onClose}>
<OptionsMenu
useMargins={useMargins}
onUseMarginsChange={onUseMarginsChange}

View file

@ -17,11 +17,18 @@
* under the License.
*/
import { Query } from 'ui/embeddable';
import { Query, EmbeddableFactory } from 'ui/embeddable';
import { AppState } from 'ui/state_management/app_state';
import { UIRegistry } from 'ui/registry/_registry';
import { Filter } from '@kbn/es-query';
import { DashboardViewMode } from './dashboard_view_mode';
export interface EmbeddableFactoryRegistry extends UIRegistry<EmbeddableFactory> {
byName: { [key: string]: EmbeddableFactory };
}
export type NavAction = (menuItem: any, navController: any, anchorElement: any) => void;
export interface GridData {
w: number;
h: number;

View file

@ -24,7 +24,7 @@ import { BreadcrumbsApi } from './api/breadcrumbs';
import { HelpExtensionApi } from './api/help_extension';
import { ChromeNavLinks } from './api/nav';
interface IInjector {
export interface IInjector {
get<T>(injectable: string): T;
}

View file

@ -19,8 +19,9 @@
// @ts-ignore: implicit any for JS file
import { uiRegistry } from 'ui/registry/_registry';
import { ShareActionProvider } from './share_action';
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry({
export const ShareContextMenuExtensionsRegistryProvider = uiRegistry<ShareActionProvider>({
name: 'shareContextMenuExtensions',
index: ['id'],
});

View file

@ -19,7 +19,6 @@
import { Moment } from 'moment';
import { TimeRange } from './time_history';
import moment = require('moment');
// NOTE: These types are somewhat guessed, they may be incorrect.
@ -29,23 +28,9 @@ export interface RefreshInterval {
}
export interface Timefilter {
time: {
// NOTE: It's unclear if this is supposed to actually allow a moment object, or undefined, or if this is just
// a bug... should be investigated. This should probably be the TimeRange type, but most TimeRange interfaces
// don't account for the possibility of the moment object, and it is actually a possibility.
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
};
getTime: () => {
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
};
setTime: (
timeRange: {
to: string | moment.Moment | undefined;
from: string | moment.Moment | undefined;
}
) => void;
time: TimeRange;
getTime: () => TimeRange;
setTime: (timeRange: TimeRange) => void;
setRefreshInterval: (refreshInterval: RefreshInterval) => void;
getRefreshInterval: () => RefreshInterval;
disableAutoRefreshSelector: () => void;

23
src/legacy/ui/public/url/kbn_url.d.ts vendored Normal file
View 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;
}