mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
merge Refactor out state logic from dashboard directive (and a bugfix (#10267)
This commit is contained in:
parent
0046164246
commit
e661dd336b
17 changed files with 802 additions and 387 deletions
|
@ -23,9 +23,9 @@ describe('dashboard panels', function () {
|
|||
style="width: 600px; height: 600px;"
|
||||
ng-if="!hasExpandedPanel()"
|
||||
on-panel-removed="onPanelRemoved"
|
||||
panels="state.panels"
|
||||
get-vis-click-handler="filterBarClickHandler(state)"
|
||||
get-vis-brush-handler="brushEvent(state)"
|
||||
panels="panels"
|
||||
get-vis-click-handler="filterBarClickHandler"
|
||||
get-vis-brush-handler="brushEvent"
|
||||
save-state="saveState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
create-child-ui-state="createChildUiState"
|
||||
|
@ -38,7 +38,7 @@ describe('dashboard panels', function () {
|
|||
}
|
||||
|
||||
function findPanelWithVisualizationId(id) {
|
||||
return $scope.state.panels.find((panel) => { return panel.id === id; });
|
||||
return $scope.panels.find((panel) => { return panel.id === id; });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -56,7 +56,7 @@ describe('dashboard panels', function () {
|
|||
dash.init();
|
||||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(0);
|
||||
expect($scope.panels.length).to.be(0);
|
||||
});
|
||||
|
||||
it('loads one vizualization', function () {
|
||||
|
@ -66,7 +66,7 @@ describe('dashboard panels', function () {
|
|||
dash.panelsJSON = `[{"col":3,"id":"foo1","row":1,"size_x":2,"size_y":2,"type":"visualization"}]`;
|
||||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(1);
|
||||
expect($scope.panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('loads vizualizations in correct order', function () {
|
||||
|
@ -92,7 +92,7 @@ describe('dashboard panels', function () {
|
|||
{"col":1,"id":"foo17","row":3,"size_x":4,"size_y":3,"type":"visualization"}]`;
|
||||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(16);
|
||||
expect($scope.panels.length).to.be(16);
|
||||
const foo8Panel = findPanelWithVisualizationId('foo8');
|
||||
expect(foo8Panel).to.not.be(null);
|
||||
expect(foo8Panel.row).to.be(8);
|
||||
|
@ -108,7 +108,7 @@ describe('dashboard panels', function () {
|
|||
{"col":5,"id":"foo2","row":1,"size_x":5,"size_y":9,"type":"visualization"}]`;
|
||||
compile(dash);
|
||||
});
|
||||
expect($scope.state.panels.length).to.be(2);
|
||||
expect($scope.panels.length).to.be(2);
|
||||
const foo1Panel = findPanelWithVisualizationId('foo1');
|
||||
expect(foo1Panel).to.not.be(null);
|
||||
expect(foo1Panel.size_x).to.be(DEFAULT_PANEL_WIDTH);
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
|
||||
import { DashboardState } from '../dashboard_state';
|
||||
|
||||
describe('DashboardState', function () {
|
||||
let AppState;
|
||||
let dashboardState;
|
||||
let savedDashboard;
|
||||
let SavedDashboard;
|
||||
let timefilter;
|
||||
let quickTimeRanges;
|
||||
|
||||
function initDashboardState() {
|
||||
dashboardState = new DashboardState(savedDashboard, timefilter, true, quickTimeRanges, AppState);
|
||||
}
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function ($injector) {
|
||||
timefilter = $injector.get('timefilter');
|
||||
quickTimeRanges = $injector.get('quickRanges');
|
||||
AppState = $injector.get('AppState');
|
||||
SavedDashboard = $injector.get('SavedDashboard');
|
||||
savedDashboard = new SavedDashboard();
|
||||
}));
|
||||
|
||||
describe('timefilter', function () {
|
||||
|
||||
describe('when timeRestore is true', function () {
|
||||
it('syncs quick time', function () {
|
||||
savedDashboard.timeRestore = true;
|
||||
savedDashboard.timeFrom = 'now/w';
|
||||
savedDashboard.timeTo = 'now/w';
|
||||
|
||||
timefilter.time.from = '2015-09-19 06:31:44.000';
|
||||
timefilter.time.to = '2015-09-29 06:31:44.000';
|
||||
timefilter.time.mode = 'absolute';
|
||||
|
||||
initDashboardState();
|
||||
|
||||
expect(timefilter.time.mode).to.equal('quick');
|
||||
expect(timefilter.time.to).to.equal('now/w');
|
||||
expect(timefilter.time.from).to.equal('now/w');
|
||||
});
|
||||
|
||||
it('syncs relative time', function () {
|
||||
savedDashboard.timeRestore = true;
|
||||
savedDashboard.timeFrom = 'now-13d';
|
||||
savedDashboard.timeTo = 'now';
|
||||
|
||||
timefilter.time.from = '2015-09-19 06:31:44.000';
|
||||
timefilter.time.to = '2015-09-29 06:31:44.000';
|
||||
timefilter.time.mode = 'absolute';
|
||||
|
||||
initDashboardState();
|
||||
|
||||
expect(timefilter.time.mode).to.equal('relative');
|
||||
expect(timefilter.time.to).to.equal('now');
|
||||
expect(timefilter.time.from).to.equal('now-13d');
|
||||
});
|
||||
|
||||
it('syncs absolute time', function () {
|
||||
savedDashboard.timeRestore = true;
|
||||
savedDashboard.timeFrom = '2015-09-19 06:31:44.000';
|
||||
savedDashboard.timeTo = '2015-09-29 06:31:44.000';
|
||||
|
||||
timefilter.time.from = 'now/w';
|
||||
timefilter.time.to = 'now/w';
|
||||
timefilter.time.mode = 'quick';
|
||||
|
||||
initDashboardState();
|
||||
|
||||
expect(timefilter.time.mode).to.equal('absolute');
|
||||
expect(timefilter.time.to).to.equal(savedDashboard.timeTo);
|
||||
expect(timefilter.time.from).to.equal(savedDashboard.timeFrom);
|
||||
});
|
||||
});
|
||||
|
||||
it('is not synced when timeRestore is false', function () {
|
||||
savedDashboard.timeRestore = false;
|
||||
savedDashboard.timeFrom = 'now/w';
|
||||
savedDashboard.timeTo = 'now/w';
|
||||
|
||||
timefilter.time.timeFrom = '2015-09-19 06:31:44.000';
|
||||
timefilter.time.timeTo = '2015-09-29 06:31:44.000';
|
||||
timefilter.time.mode = 'absolute';
|
||||
|
||||
initDashboardState();
|
||||
|
||||
expect(timefilter.time.mode).to.equal('absolute');
|
||||
expect(timefilter.time.timeFrom).to.equal('2015-09-19 06:31:44.000');
|
||||
expect(timefilter.time.timeTo).to.equal('2015-09-29 06:31:44.000');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -30,7 +30,7 @@
|
|||
parse-query
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
ng-model="state.query"
|
||||
ng-model="model.query"
|
||||
placeholder="Filter..."
|
||||
aria-label="Filter input"
|
||||
type="text"
|
||||
|
@ -63,9 +63,9 @@
|
|||
<dashboard-grid
|
||||
ng-show="!hasExpandedPanel()"
|
||||
on-panel-removed="onPanelRemoved"
|
||||
panels="state.panels"
|
||||
get-vis-click-handler="filterBarClickHandler(state)"
|
||||
get-vis-brush-handler="brushEvent(state)"
|
||||
panels="panels"
|
||||
get-vis-click-handler="getFilterBarClickHandler"
|
||||
get-vis-brush-handler="getBrushEvent"
|
||||
save-state="saveState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
create-child-ui-state="createChildUiState"
|
||||
|
@ -76,8 +76,8 @@
|
|||
panel="expandedPanel"
|
||||
is-full-screen-mode="!chrome.getVisible()"
|
||||
is-expanded="true"
|
||||
get-vis-click-handler="filterBarClickHandler(state)"
|
||||
get-vis-brush-handler="brushEvent(state)"
|
||||
get-vis-click-handler="getFilterBarClickHandler"
|
||||
get-vis-brush-handler="getBrushEvent"
|
||||
save-state="saveState"
|
||||
create-child-ui-state="createChildUiState"
|
||||
toggle-expand="toggleExpandPanel(expandedPanel.panelIndex)">
|
||||
|
|
|
@ -10,14 +10,11 @@ import 'plugins/kibana/dashboard/panel/panel';
|
|||
import dashboardTemplate from 'plugins/kibana/dashboard/dashboard.html';
|
||||
import FilterBarQueryFilterProvider from 'ui/filter_bar/query_filter';
|
||||
import DocTitleProvider from 'ui/doc_title';
|
||||
import stateMonitorFactory from 'ui/state_management/state_monitor_factory';
|
||||
import { getTopNavConfig } from './top_nav/get_top_nav_config';
|
||||
import { createPanelState } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
import { DashboardConstants } from './dashboard_constants';
|
||||
import UtilsBrushEventProvider from 'ui/utils/brush_event';
|
||||
import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler';
|
||||
import { FilterUtils } from './filter_utils';
|
||||
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
import { DashboardState } from './dashboard_state';
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
'elasticsearch',
|
||||
|
@ -52,7 +49,7 @@ uiRoutes
|
|||
}
|
||||
});
|
||||
|
||||
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl, Private) {
|
||||
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, quickRanges, kbnUrl, Private) {
|
||||
const brushEvent = Private(UtilsBrushEventProvider);
|
||||
const filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider);
|
||||
|
||||
|
@ -60,117 +57,76 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
restrict: 'E',
|
||||
controllerAs: 'dashboardApp',
|
||||
controller: function ($scope, $rootScope, $route, $routeParams, $location, Private, getAppState) {
|
||||
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
|
||||
const notify = new Notifier({
|
||||
location: 'Dashboard'
|
||||
});
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const notify = new Notifier({ location: 'Dashboard' });
|
||||
|
||||
const dash = $scope.dash = $route.current.locals.dash;
|
||||
|
||||
if (dash.timeRestore && dash.timeTo && dash.timeFrom && !getAppState.previouslyStored()) {
|
||||
timefilter.time.to = dash.timeTo;
|
||||
timefilter.time.from = dash.timeFrom;
|
||||
if (dash.refreshInterval) {
|
||||
timefilter.refreshInterval = dash.refreshInterval;
|
||||
}
|
||||
if (dash.id) {
|
||||
docTitle.change(dash.title);
|
||||
}
|
||||
|
||||
const stateDefaults = {
|
||||
title: dash.title,
|
||||
panels: dash.panelsJSON ? JSON.parse(dash.panelsJSON) : [],
|
||||
options: dash.optionsJSON ? JSON.parse(dash.optionsJSON) : {},
|
||||
uiState: dash.uiStateJSON ? JSON.parse(dash.uiStateJSON) : {},
|
||||
query: FilterUtils.getQueryFilterForDashboard(dash),
|
||||
filters: FilterUtils.getFilterBarsForDashboard(dash),
|
||||
};
|
||||
const dashboardState = new DashboardState(
|
||||
dash,
|
||||
timefilter,
|
||||
!getAppState.previouslyStored(),
|
||||
quickRanges,
|
||||
AppState);
|
||||
|
||||
let stateMonitor;
|
||||
const $state = $scope.state = new AppState(stateDefaults);
|
||||
const $uiState = $scope.uiState = $state.makeStateful('uiState');
|
||||
const $appStatus = $scope.appStatus = this.appStatus = {};
|
||||
|
||||
$scope.$watchCollection('state.options', function (newVal, oldVal) {
|
||||
if (!angular.equals(newVal, oldVal)) $state.save();
|
||||
});
|
||||
|
||||
$scope.$watch('state.options.darkTheme', setDarkTheme);
|
||||
|
||||
$scope.topNavMenu = getTopNavConfig(kbnUrl);
|
||||
|
||||
$scope.refresh = _.bindKey(courier, 'fetch');
|
||||
dashboardState.updateFilters(queryFilter);
|
||||
let pendingVisCount = _.size(dashboardState.getPanels());
|
||||
|
||||
timefilter.enabled = true;
|
||||
$scope.timefilter = timefilter;
|
||||
$scope.$listen(timefilter, 'fetch', $scope.refresh);
|
||||
|
||||
courier.setRootSearchSource(dash.searchSource);
|
||||
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
// Following the "best practice" of always have a '.' in your ng-models –
|
||||
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
|
||||
$scope.model = { query: dashboardState.getQuery() };
|
||||
|
||||
function init() {
|
||||
updateQueryOnRootSource();
|
||||
$scope.panels = dashboardState.getPanels();
|
||||
$scope.topNavMenu = getTopNavConfig(kbnUrl);
|
||||
$scope.refresh = _.bindKey(courier, 'fetch');
|
||||
$scope.timefilter = timefilter;
|
||||
$scope.expandedPanel = null;
|
||||
|
||||
if (dash.id) {
|
||||
docTitle.change(dash.title);
|
||||
}
|
||||
$scope.getBrushEvent = () => brushEvent(dashboardState.getAppState());
|
||||
$scope.getFilterBarClickHandler = () => filterBarClickHandler(dashboardState.getAppState());
|
||||
$scope.hasExpandedPanel = () => $scope.expandedPanel !== null;
|
||||
$scope.getDashTitle = () => {
|
||||
return dashboardState.dashboard.lastSavedTitle || `${dashboardState.dashboard.title} (unsaved)`;
|
||||
};
|
||||
$scope.newDashboard = () => { kbnUrl.change(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}); };
|
||||
$scope.saveState = () => dashboardState.saveState();
|
||||
|
||||
initPanelIndexes();
|
||||
|
||||
// watch for state changes and update the appStatus.dirty value
|
||||
stateMonitor = stateMonitorFactory.create($state, stateDefaults);
|
||||
stateMonitor.onChange((status) => {
|
||||
$appStatus.dirty = status.dirty;
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
stateMonitor.destroy();
|
||||
dash.destroy();
|
||||
|
||||
// Remove dark theme to keep it from affecting the appearance of other apps.
|
||||
setDarkTheme(false);
|
||||
});
|
||||
|
||||
$scope.$emit('application.load');
|
||||
}
|
||||
|
||||
function initPanelIndexes() {
|
||||
// find the largest panelIndex in all the panels
|
||||
let maxIndex = getMaxPanelIndex();
|
||||
|
||||
// ensure that all panels have a panelIndex
|
||||
$scope.state.panels.forEach(function (panel) {
|
||||
if (!panel.panelIndex) {
|
||||
panel.panelIndex = maxIndex++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMaxPanelIndex() {
|
||||
let maxId = $scope.state.panels.reduce(function (id, panel) {
|
||||
return Math.max(id, panel.panelIndex || id);
|
||||
}, 0);
|
||||
return ++maxId;
|
||||
}
|
||||
|
||||
function updateQueryOnRootSource() {
|
||||
const filters = queryFilter.getFilters();
|
||||
if ($state.query) {
|
||||
dash.searchSource.set('filter', _.union(filters, [{
|
||||
query: $state.query
|
||||
}]));
|
||||
$scope.toggleExpandPanel = (panelIndex) => {
|
||||
if ($scope.expandedPanel && $scope.expandedPanel.panelIndex === panelIndex) {
|
||||
$scope.expandedPanel = null;
|
||||
} else {
|
||||
dash.searchSource.set('filter', filters);
|
||||
$scope.expandedPanel =
|
||||
dashboardState.getPanels().find((panel) => panel.panelIndex === panelIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setDarkTheme(enabled) {
|
||||
const theme = Boolean(enabled) ? 'theme-dark' : 'theme-light';
|
||||
chrome.removeApplicationClass(['theme-dark', 'theme-light']);
|
||||
chrome.addApplicationClass(theme);
|
||||
}
|
||||
$scope.filterResults = function () {
|
||||
dashboardState.setQuery($scope.model.query);
|
||||
dashboardState.updateFilters(queryFilter);
|
||||
$scope.refresh();
|
||||
};
|
||||
|
||||
// called by the saved-object-finder when a user clicks a vis
|
||||
$scope.addVis = function (hit) {
|
||||
pendingVisCount++;
|
||||
dashboardState.addNewPanel(hit.id, 'visualization');
|
||||
};
|
||||
|
||||
$scope.addSearch = function (hit) {
|
||||
pendingVisCount++;
|
||||
dashboardState.addNewPanel(hit.id, 'search');
|
||||
};
|
||||
|
||||
$scope.showEditHelpText = () => {
|
||||
return !dashboardState.getPanels().length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a child ui state for the panel. It's passed the ui state to use, but needs to
|
||||
|
@ -180,104 +136,74 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
* @returns {Object}
|
||||
*/
|
||||
$scope.createChildUiState = function createChildUiState(path, uiState) {
|
||||
return $scope.uiState.createChild(path, uiState, true);
|
||||
return dashboardState.uiState.createChild(path, uiState, true);
|
||||
};
|
||||
|
||||
$scope.saveState = function saveState() {
|
||||
$state.save();
|
||||
};
|
||||
$scope.onPanelRemoved = (panelIndex) => dashboardState.removePanel(panelIndex);
|
||||
|
||||
$scope.onPanelRemoved = (panelIndex) => {
|
||||
_.remove($scope.state.panels, function (panel) {
|
||||
if (panel.panelIndex === panelIndex) {
|
||||
$scope.uiState.removeChild(getPersistedStateId(panel));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
$scope.save = function () {
|
||||
return dashboardState.saveDashboard(angular.toJson).then(function (id) {
|
||||
$scope.kbnTopNav.close('save');
|
||||
if (id) {
|
||||
notify.info(`Saved Dashboard as "${dash.title}"`);
|
||||
if (dash.id !== $routeParams.id) {
|
||||
kbnUrl.change(
|
||||
`${DashboardConstants.EXISTING_DASHBOARD_URL}`,
|
||||
{ id: dash.id });
|
||||
} else {
|
||||
docTitle.change(dash.lastSavedTitle);
|
||||
}
|
||||
}
|
||||
});
|
||||
$state.save();
|
||||
}).catch(notify.fatal);
|
||||
};
|
||||
|
||||
$scope.brushEvent = brushEvent;
|
||||
$scope.filterBarClickHandler = filterBarClickHandler;
|
||||
$scope.expandedPanel = null;
|
||||
$scope.hasExpandedPanel = () => $scope.expandedPanel !== null;
|
||||
$scope.toggleExpandPanel = (panelIndex) => {
|
||||
if ($scope.expandedPanel && $scope.expandedPanel.panelIndex === panelIndex) {
|
||||
$scope.expandedPanel = null;
|
||||
} else {
|
||||
$scope.expandedPanel =
|
||||
$scope.state.panels.find((panel) => panel.panelIndex === panelIndex);
|
||||
}
|
||||
};
|
||||
$scope.$watchCollection(() => dashboardState.getOptions(), () => dashboardState.saveState());
|
||||
$scope.$watch(() => dashboardState.getOptions().darkTheme, updateTheme);
|
||||
|
||||
$scope.$watch('model.query', function () {
|
||||
dashboardState.setQuery($scope.model.query);
|
||||
});
|
||||
|
||||
$scope.$listen(timefilter, 'fetch', $scope.refresh);
|
||||
|
||||
// update root source when filters update
|
||||
$scope.$listen(queryFilter, 'update', function () {
|
||||
updateQueryOnRootSource();
|
||||
$state.save();
|
||||
dashboardState.updateFilters(queryFilter);
|
||||
});
|
||||
|
||||
// update data when filters fire fetch event
|
||||
$scope.$listen(queryFilter, 'fetch', $scope.refresh);
|
||||
|
||||
$scope.getDashTitle = function () {
|
||||
return dash.lastSavedTitle || `${dash.title} (unsaved)`;
|
||||
};
|
||||
$scope.$on('$destroy', () => {
|
||||
dashboardState.destroy();
|
||||
|
||||
$scope.newDashboard = function () {
|
||||
kbnUrl.change('/dashboard', {});
|
||||
};
|
||||
// Remove dark theme to keep it from affecting the appearance of other apps.
|
||||
setLightTheme();
|
||||
});
|
||||
|
||||
$scope.filterResults = function () {
|
||||
updateQueryOnRootSource();
|
||||
$state.save();
|
||||
$scope.refresh();
|
||||
};
|
||||
function updateTheme() {
|
||||
const useDarkTheme = dashboardState.getOptions().darkTheme;
|
||||
useDarkTheme ? setDarkTheme() : setLightTheme();
|
||||
}
|
||||
|
||||
$scope.save = function () {
|
||||
$state.save();
|
||||
function setDarkTheme() {
|
||||
chrome.removeApplicationClass(['theme-light']);
|
||||
chrome.addApplicationClass('theme-dark');
|
||||
}
|
||||
|
||||
const timeRestoreObj = _.pick(timefilter.refreshInterval, ['display', 'pause', 'section', 'value']);
|
||||
function setLightTheme() {
|
||||
chrome.removeApplicationClass(['theme-dark']);
|
||||
chrome.addApplicationClass('theme-light');
|
||||
}
|
||||
|
||||
dash.panelsJSON = angular.toJson($state.panels);
|
||||
dash.uiStateJSON = angular.toJson($uiState.getChanges());
|
||||
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
|
||||
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
|
||||
dash.refreshInterval = dash.timeRestore ? timeRestoreObj : undefined;
|
||||
dash.optionsJSON = angular.toJson($state.options);
|
||||
|
||||
dash.save()
|
||||
.then(function (id) {
|
||||
stateMonitor.setInitialState($state.toJSON());
|
||||
$scope.kbnTopNav.close('save');
|
||||
if (id) {
|
||||
notify.info('Saved Dashboard as "' + dash.title + '"');
|
||||
if (dash.id !== $routeParams.id) {
|
||||
kbnUrl.change('/dashboard/{{id}}', { id: dash.id });
|
||||
} else {
|
||||
docTitle.change(dash.lastSavedTitle);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(notify.fatal);
|
||||
};
|
||||
|
||||
let pendingVis = _.size($state.panels);
|
||||
$scope.$on('ready:vis', function () {
|
||||
if (pendingVis) pendingVis--;
|
||||
if (pendingVis === 0) {
|
||||
$state.save();
|
||||
if (pendingVisCount > 0) pendingVisCount--;
|
||||
if (pendingVisCount === 0) {
|
||||
dashboardState.saveState();
|
||||
$scope.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// called by the saved-object-finder when a user clicks a vis
|
||||
$scope.addVis = function (hit) {
|
||||
pendingVis++;
|
||||
$state.panels.push(createPanelState(hit.id, 'visualization', getMaxPanelIndex()));
|
||||
};
|
||||
|
||||
if ($route.current.params && $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM]) {
|
||||
$scope.addVis({ id: $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] });
|
||||
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM);
|
||||
|
@ -288,15 +214,10 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
kbnUrl.change(`/visualize?${DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM}`);
|
||||
};
|
||||
|
||||
$scope.addSearch = function (hit) {
|
||||
pendingVis++;
|
||||
$state.panels.push(createPanelState(hit.id, 'search', getMaxPanelIndex()));
|
||||
};
|
||||
|
||||
// Setup configurable values for config directive, after objects are initialized
|
||||
$scope.opts = {
|
||||
dashboard: dash,
|
||||
ui: $state.options,
|
||||
ui: dashboardState.getOptions(),
|
||||
save: $scope.save,
|
||||
addVis: $scope.addVis,
|
||||
addNewVis,
|
||||
|
@ -304,11 +225,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
|
|||
timefilter: $scope.timefilter
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
$scope.showEditHelpText = () => {
|
||||
return !$scope.state.panels.length;
|
||||
};
|
||||
$scope.$emit('application.load');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -2,5 +2,7 @@
|
|||
export const DashboardConstants = {
|
||||
ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard',
|
||||
NEW_VISUALIZATION_ID_PARAM: 'addVisualization',
|
||||
LANDING_PAGE_PATH: '/dashboard'
|
||||
LANDING_PAGE_PATH: '/dashboard',
|
||||
CREATE_NEW_DASHBOARD_URL: '/dashboard/create',
|
||||
EXISTING_DASHBOARD_URL: '/dashboard/{{id}}'
|
||||
};
|
||||
|
|
175
src/core_plugins/kibana/public/dashboard/dashboard_state.js
Normal file
175
src/core_plugins/kibana/public/dashboard/dashboard_state.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
import _ from 'lodash';
|
||||
import { FilterUtils } from './filter_utils';
|
||||
import { PanelUtils } from './panel/panel_utils';
|
||||
import moment from 'moment';
|
||||
|
||||
import stateMonitorFactory from 'ui/state_management/state_monitor_factory';
|
||||
import { createPanelState } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
|
||||
function getStateDefaults(dashboard) {
|
||||
return {
|
||||
title: dashboard.title,
|
||||
panels: dashboard.panelsJSON ? JSON.parse(dashboard.panelsJSON) : [],
|
||||
options: dashboard.optionsJSON ? JSON.parse(dashboard.optionsJSON) : {},
|
||||
uiState: dashboard.uiStateJSON ? JSON.parse(dashboard.uiStateJSON) : {},
|
||||
query: FilterUtils.getQueryFilterForDashboard(dashboard),
|
||||
filters: FilterUtils.getFilterBarsForDashboard(dashboard)
|
||||
};
|
||||
}
|
||||
|
||||
export class DashboardState {
|
||||
/**
|
||||
* @param dashboard {SavedDashboard}
|
||||
* @param timefilter {Object}
|
||||
* @param timeFilterPreviouslyStored {boolean} - I'm honestly not sure what this value
|
||||
* means but preserving original logic after a refactor.
|
||||
* @param quickTimeRanges {Array<Object>} An array of default time ranges that should be
|
||||
* classified as 'quick' mode.
|
||||
* @param AppState {Object} A class that will be used to instantiate the appState.
|
||||
*/
|
||||
constructor(dashboard, timefilter, timeFilterPreviouslyStored, quickTimeRanges, AppState) {
|
||||
this.stateDefaults = getStateDefaults(dashboard);
|
||||
|
||||
this.appState = new AppState(this.stateDefaults);
|
||||
this.uiState = this.appState.makeStateful('uiState');
|
||||
this.timefilter = timefilter;
|
||||
this.dashboard = dashboard;
|
||||
|
||||
this.stateMonitor = stateMonitorFactory.create(this.appState, this.stateDefaults);
|
||||
|
||||
if (this.getShouldSyncTimefilterWithDashboard() && timeFilterPreviouslyStored) {
|
||||
this.syncTimefilterWithDashboard(quickTimeRanges);
|
||||
}
|
||||
|
||||
PanelUtils.initPanelIndexes(this.getPanels());
|
||||
}
|
||||
|
||||
getAppState() {
|
||||
return this.appState;
|
||||
}
|
||||
|
||||
getQuery() {
|
||||
return this.appState.query;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.appState.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the timefilter should match the time stored with the dashboard.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getShouldSyncTimefilterWithDashboard() {
|
||||
return this.dashboard.timeRestore && this.dashboard.timeTo && this.dashboard.timeFrom;
|
||||
}
|
||||
|
||||
getPanels() {
|
||||
return this.appState.panels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a basic panel, adding it to the state.
|
||||
* @param {number} id
|
||||
* @param {string} type
|
||||
*/
|
||||
addNewPanel(id, type) {
|
||||
const maxPanelIndex = PanelUtils.getMaxPanelIndex(this.getPanels());
|
||||
this.getPanels().push(createPanelState(id, type, maxPanelIndex));
|
||||
}
|
||||
|
||||
removePanel(panelIndex) {
|
||||
_.remove(this.getPanels(), (panel) => {
|
||||
if (panel.panelIndex === panelIndex) {
|
||||
this.uiState.removeChild(getPersistedStateId(panel));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the time filter to match the values stored in the dashboard.
|
||||
* @param {Array<Object>} quickTimeRanges - An array of often used default relative time ranges.
|
||||
* Used to determine whether a relative query should be classified as a "quick" time mode or
|
||||
* simply a "relative" time mode.
|
||||
*/
|
||||
syncTimefilterWithDashboard(quickTimeRanges) {
|
||||
this.timefilter.time.to = this.dashboard.timeTo;
|
||||
this.timefilter.time.from = this.dashboard.timeFrom;
|
||||
const isMoment = moment(this.dashboard.timeTo).isValid();
|
||||
if (isMoment) {
|
||||
this.timefilter.time.mode = 'absolute';
|
||||
} else {
|
||||
const quickTime = _.find(
|
||||
quickTimeRanges,
|
||||
(timeRange) => timeRange.from === this.dashboard.timeFrom && timeRange.to === this.dashboard.timeTo);
|
||||
|
||||
this.timefilter.time.mode = quickTime ? 'quick' : 'relative';
|
||||
}
|
||||
if (this.dashboard.refreshInterval) {
|
||||
this.timefilter.refreshInterval = this.dashboard.refreshInterval;
|
||||
}
|
||||
}
|
||||
|
||||
setQuery(newQuery) {
|
||||
this.appState.query = newQuery;
|
||||
}
|
||||
|
||||
saveState() {
|
||||
this.appState.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the dashboard.
|
||||
* @param toJson {function} A custom toJson function. Used because the previous code used
|
||||
* the angularized toJson version, and it was unclear whether there was a reason not to use
|
||||
* JSON.stringify
|
||||
* @returns {Promise<string>} A promise that if resolved, will contain the id of the newly saved
|
||||
* dashboard.
|
||||
*/
|
||||
saveDashboard(toJson) {
|
||||
this.saveState();
|
||||
|
||||
const timeRestoreObj = _.pick(this.timefilter.refreshInterval, ['display', 'pause', 'section', 'value']);
|
||||
this.dashboard.panelsJSON = toJson(this.appState.panels);
|
||||
this.dashboard.uiStateJSON = toJson(this.uiState.getChanges());
|
||||
this.dashboard.timeFrom = this.dashboard.timeRestore ? this.timefilter.time.from : undefined;
|
||||
this.dashboard.timeTo = this.dashboard.timeRestore ? this.timefilter.time.to : undefined;
|
||||
this.dashboard.refreshInterval = this.dashboard.timeRestore ? timeRestoreObj : undefined;
|
||||
this.dashboard.optionsJSON = toJson(this.appState.options);
|
||||
|
||||
return this.dashboard.save()
|
||||
.then((id) => {
|
||||
this.stateMonitor.setInitialState(this.appState.toJSON());
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given filter with the dashboard and to the state.
|
||||
* @param filter
|
||||
*/
|
||||
updateFilters(filter) {
|
||||
const filters = filter.getFilters();
|
||||
if (this.appState.query) {
|
||||
this.dashboard.searchSource.set('filter', _.union(filters, [{
|
||||
query: this.appState.query
|
||||
}]));
|
||||
} else {
|
||||
this.dashboard.searchSource.set('filter', filters);
|
||||
}
|
||||
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.stateMonitor) {
|
||||
this.stateMonitor.destroy();
|
||||
}
|
||||
this.dashboard.destroy();
|
||||
}
|
||||
}
|
|
@ -31,12 +31,12 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
* Returns a click handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisClickHandler: '&',
|
||||
getVisClickHandler: '=',
|
||||
/**
|
||||
* Returns a brush event handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisBrushHandler: '&',
|
||||
getVisBrushHandler: '=',
|
||||
/**
|
||||
* Call when changes should be propagated to the url and thus saved in state.
|
||||
* @type {function}
|
||||
|
@ -173,8 +173,8 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
panel="findPanelByPanelIndex(${panel.panelIndex}, panels)"
|
||||
is-full-screen-mode="isFullScreenMode"
|
||||
is-expanded="false"
|
||||
get-vis-click-handler="getVisClickHandler(state)"
|
||||
get-vis-brush-handler="getVisBrushHandler(state)"
|
||||
get-vis-click-handler="getVisClickHandler"
|
||||
get-vis-brush-handler="getVisBrushHandler"
|
||||
save-state="saveState"
|
||||
toggle-expand="toggleExpand(${panel.panelIndex})"
|
||||
create-child-ui-state="createChildUiState">
|
||||
|
|
|
@ -59,12 +59,12 @@ uiModules
|
|||
* Returns a click handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisClickHandler: '&',
|
||||
getVisClickHandler: '=',
|
||||
/**
|
||||
* Returns a brush event handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisBrushHandler: '&',
|
||||
getVisBrushHandler: '=',
|
||||
/**
|
||||
* Call when changes should be propagated to the url and thus saved in state.
|
||||
* @type {function}
|
||||
|
|
|
@ -44,4 +44,23 @@ export class PanelUtils {
|
|||
static findPanelByPanelIndex(panelIndex, panels) {
|
||||
return _.find(panels, (panel) => panel.panelIndex === panelIndex);
|
||||
}
|
||||
|
||||
static initPanelIndexes(panels) {
|
||||
// find the largest panelIndex in all the panels
|
||||
let maxIndex = this.getMaxPanelIndex(panels);
|
||||
|
||||
// ensure that all panels have a panelIndex
|
||||
panels.forEach(function (panel) {
|
||||
if (!panel.panelIndex) {
|
||||
panel.panelIndex = maxIndex++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static getMaxPanelIndex(panels) {
|
||||
let maxId = panels.reduce(function (id, panel) {
|
||||
return Math.max(id, panel.panelIndex || id);
|
||||
}, 0);
|
||||
return ++maxId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
<div class="kuiLocalDropdownTitle">Options</div>
|
||||
<div class="input-group">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="opts.ui.darkTheme" ng-checked="opts.ui.darkTheme">
|
||||
<input
|
||||
type="checkbox"
|
||||
ng-model="opts.ui.darkTheme"
|
||||
ng-checked="opts.ui.darkTheme"
|
||||
data-test-subj="dashboardDarkThemeCheckbox"
|
||||
>
|
||||
Use dark theme
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,12 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="opts.dashboard.timeRestore" ng-checked="opts.dashboard.timeRestore">
|
||||
<input
|
||||
type="checkbox"
|
||||
ng-model="opts.dashboard.timeRestore"
|
||||
ng-checked="opts.dashboard.timeRestore"
|
||||
data-test-subj="storeTimeWithDashboard"
|
||||
>
|
||||
Store time with {{opts.dashboard.getDisplayName()}}
|
||||
<kbn-info placement="bottom" info="Change the time filter to the currently selected time each time this dashboard is loaded"></kbn-info>
|
||||
</label>
|
||||
|
|
|
@ -14,128 +14,97 @@ import {
|
|||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
bdd.describe('dashboard tab', function describeIndexTests() {
|
||||
bdd.before(function () {
|
||||
PageObjects.common.debug('Starting dashboard before method');
|
||||
const logstash = scenarioManager.loadIfEmpty('logstashFunctional');
|
||||
// delete .kibana index and update configDoc
|
||||
return esClient.deleteAndUpdateConfigDoc({ 'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*' })
|
||||
// and load a set of makelogs data
|
||||
.then(function loadkibanaVisualizations() {
|
||||
PageObjects.common.debug('load kibana index with visualizations');
|
||||
return elasticDump.elasticLoad('dashboard','.kibana');
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.debug('navigateToApp dashboard');
|
||||
return PageObjects.common.navigateToApp('dashboard');
|
||||
})
|
||||
// wait for the logstash data load to finish if it hasn't already
|
||||
.then(function () {
|
||||
return logstash;
|
||||
});
|
||||
|
||||
bdd.before(async function () {
|
||||
return PageObjects.dashboard.initTests();
|
||||
});
|
||||
|
||||
bdd.describe('add visualizations to dashboard', function dashboardTest() {
|
||||
const visualizations = ['Visualization漢字 AreaChart',
|
||||
'Visualization☺漢字 DataTable',
|
||||
'Visualization漢字 LineChart',
|
||||
'Visualization PieChart',
|
||||
'Visualization TileMap',
|
||||
'Visualization☺ VerticalBarChart',
|
||||
'Visualization MetricChart'
|
||||
];
|
||||
bdd.it('should be able to add visualizations to dashboard', async function addVisualizations() {
|
||||
PageObjects.common.saveScreenshot('Dashboard-no-visualizations');
|
||||
|
||||
bdd.it('should be able to add visualizations to dashboard', function addVisualizations() {
|
||||
PageObjects.common.saveScreenshot('Dashboard-no-visualizations');
|
||||
// This flip between apps fixes the url so state is preserved when switching apps in test mode.
|
||||
// Without this flip the url in test mode looks something like
|
||||
// "http://localhost:5620/app/kibana?_t=1486069030837#/dashboard?_g=...."
|
||||
// after the initial flip, the url will look like this: "http://localhost:5620/app/kibana#/dashboard?_g=...."
|
||||
await PageObjects.header.clickVisualize();
|
||||
await PageObjects.header.clickDashboard();
|
||||
|
||||
function addVisualizations(arr) {
|
||||
return arr.reduce(function (promise, vizName) {
|
||||
return promise
|
||||
.then(function () {
|
||||
return PageObjects.dashboard.addVisualization(vizName);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.dashboard.addVisualizations(PageObjects.dashboard.getTestVisualizationNames());
|
||||
|
||||
return PageObjects.dashboard.clickNewDashboard()
|
||||
.then(() => addVisualizations(visualizations))
|
||||
.then(function () {
|
||||
PageObjects.common.debug('done adding visualizations');
|
||||
PageObjects.common.saveScreenshot('Dashboard-add-visualizations');
|
||||
});
|
||||
PageObjects.common.debug('done adding visualizations');
|
||||
PageObjects.common.saveScreenshot('Dashboard-add-visualizations');
|
||||
});
|
||||
|
||||
bdd.it('set the timepicker time to that which contains our test data', async function setTimepicker() {
|
||||
await PageObjects.dashboard.setTimepickerInDataRange();
|
||||
});
|
||||
|
||||
bdd.it('should save and load dashboard', async function saveAndLoadDashboard() {
|
||||
const dashboardName = 'Dashboard Test 1';
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName);
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
|
||||
await PageObjects.common.try(function () {
|
||||
PageObjects.common.debug('now re-load previously saved dashboard');
|
||||
return PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
});
|
||||
await PageObjects.common.saveScreenshot('Dashboard-load-saved');
|
||||
});
|
||||
|
||||
bdd.it('set the timepicker time to that which contains our test data', function setTimepicker() {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
// .then(function () {
|
||||
PageObjects.common.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
|
||||
return PageObjects.header.setAbsoluteRange(fromTime, toTime)
|
||||
.then(function () {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
})
|
||||
.then(function takeScreenshot() {
|
||||
PageObjects.common.saveScreenshot('Dashboard-set-timepicker');
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should save and load dashboard', function saveAndLoadDashboard() {
|
||||
const dashboardName = 'Dashboard Test 1';
|
||||
// TODO: save time on the dashboard and test it
|
||||
return PageObjects.dashboard.saveDashboard(dashboardName)
|
||||
.then(() => PageObjects.dashboard.gotoDashboardLandingPage())
|
||||
// click New Dashboard just to clear the one we just created
|
||||
.then(function () {
|
||||
return PageObjects.common.try(function () {
|
||||
PageObjects.common.debug('saved Dashboard, now click New Dashboard');
|
||||
return PageObjects.dashboard.clickNewDashboard();
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
return PageObjects.common.try(function () {
|
||||
PageObjects.common.debug('now re-load previously saved dashboard');
|
||||
return PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.saveScreenshot('Dashboard-load-saved');
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should have all the expected visualizations', function checkVisualizations() {
|
||||
return PageObjects.common.tryForTime(10000, function () {
|
||||
return PageObjects.dashboard.getPanelTitles()
|
||||
bdd.it('should have all the expected visualizations', function checkVisualizations() {
|
||||
return PageObjects.common.tryForTime(10000, function () {
|
||||
return PageObjects.dashboard.getPanelTitles()
|
||||
.then(function (panelTitles) {
|
||||
PageObjects.common.log('visualization titles = ' + panelTitles);
|
||||
expect(panelTitles).to.eql(visualizations);
|
||||
expect(panelTitles).to.eql(PageObjects.dashboard.getTestVisualizationNames());
|
||||
});
|
||||
})
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.saveScreenshot('Dashboard-has-visualizations');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('should have all the expected initial sizes', function checkVisualizationSizes() {
|
||||
const width = DEFAULT_PANEL_WIDTH;
|
||||
const height = DEFAULT_PANEL_HEIGHT;
|
||||
const visObjects = [
|
||||
{ dataCol: '1', dataRow: '1', dataSizeX: width, dataSizeY: height, title: 'Visualization漢字 AreaChart' },
|
||||
{ dataCol: width + 1, dataRow: '1', dataSizeX: width, dataSizeY: height, title: 'Visualization☺漢字 DataTable' },
|
||||
{ dataCol: '1', dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: 'Visualization漢字 LineChart' },
|
||||
{ dataCol: width + 1, dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: 'Visualization PieChart' },
|
||||
{ dataCol: '1', dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: 'Visualization TileMap' },
|
||||
{ dataCol: width + 1, dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: 'Visualization☺ VerticalBarChart' },
|
||||
{ dataCol: '1', dataRow: (height * 3) + 1, dataSizeX: width, dataSizeY: height, title: 'Visualization MetricChart' }
|
||||
];
|
||||
|
||||
return PageObjects.common.tryForTime(10000, function () {
|
||||
return PageObjects.dashboard.getPanelData()
|
||||
bdd.it('should have all the expected initial sizes', function checkVisualizationSizes() {
|
||||
const width = DEFAULT_PANEL_WIDTH;
|
||||
const height = DEFAULT_PANEL_HEIGHT;
|
||||
const titles = PageObjects.dashboard.getTestVisualizationNames();
|
||||
const visObjects = [
|
||||
{ dataCol: '1', dataRow: '1', dataSizeX: width, dataSizeY: height, title: titles[0] },
|
||||
{ dataCol: width + 1, dataRow: '1', dataSizeX: width, dataSizeY: height, title: titles[1] },
|
||||
{ dataCol: '1', dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: titles[2] },
|
||||
{ dataCol: width + 1, dataRow: height + 1, dataSizeX: width, dataSizeY: height, title: titles[3] },
|
||||
{ dataCol: '1', dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: titles[4] },
|
||||
{ dataCol: width + 1, dataRow: (height * 2) + 1, dataSizeX: width, dataSizeY: height, title: titles[5] },
|
||||
{ dataCol: '1', dataRow: (height * 3) + 1, dataSizeX: width, dataSizeY: height, title: titles[6] }
|
||||
];
|
||||
return PageObjects.common.tryForTime(10000, function () {
|
||||
return PageObjects.dashboard.getPanelData()
|
||||
.then(function (panelTitles) {
|
||||
PageObjects.common.log('visualization titles = ' + panelTitles);
|
||||
PageObjects.common.saveScreenshot('Dashboard-visualization-sizes');
|
||||
expect(panelTitles).to.eql(visObjects);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
PageObjects.common.saveScreenshot('Dashboard-has-visualizations');
|
||||
});
|
||||
});
|
||||
|
||||
bdd.it('filters when a pie chart slice is clicked', async function () {
|
||||
let descriptions = await PageObjects.dashboard.getFilterDescriptions(1000);
|
||||
expect(descriptions.length).to.equal(0);
|
||||
|
||||
await PageObjects.dashboard.filterOnPieSlice();
|
||||
descriptions = await PageObjects.dashboard.getFilterDescriptions();
|
||||
expect(descriptions.length).to.equal(1);
|
||||
});
|
||||
|
||||
bdd.it('retains dark theme in state', async function () {
|
||||
await PageObjects.dashboard.useDarkTheme(true);
|
||||
await PageObjects.header.clickVisualize();
|
||||
await PageObjects.header.clickDashboard();
|
||||
const isDarkThemeOn = await PageObjects.dashboard.isDarkThemeOn();
|
||||
expect(isDarkThemeOn).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
|
67
test/functional/apps/dashboard/_dashboard_time.js
Normal file
67
test/functional/apps/dashboard/_dashboard_time.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import expect from 'expect.js';
|
||||
|
||||
import { bdd } from '../../../support';
|
||||
import PageObjects from '../../../support/page_objects';
|
||||
|
||||
const dashboardName = 'Dashboard Test Time';
|
||||
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
bdd.describe('dashboard time', function dashboardSaveWithTime() {
|
||||
bdd.before(async function () {
|
||||
await PageObjects.dashboard.initTests();
|
||||
});
|
||||
|
||||
bdd.describe('dashboard without stored timed', async function () {
|
||||
bdd.it('is saved', async function () {
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
await PageObjects.dashboard.addVisualizations([PageObjects.dashboard.getTestVisualizationNames()[0]]);
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName, false);
|
||||
});
|
||||
|
||||
bdd.it('Does not set the time picker on open', async function () {
|
||||
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
|
||||
const fromTimeNext = await PageObjects.header.getFromTime();
|
||||
const toTimeNext = await PageObjects.header.getToTime();
|
||||
expect(fromTimeNext).to.equal(fromTime);
|
||||
expect(toTimeNext).to.equal(toTime);
|
||||
});
|
||||
});
|
||||
|
||||
bdd.describe('dashboard with stored timed', async function () {
|
||||
bdd.it('is saved with quick time', async function () {
|
||||
await PageObjects.header.setQuickTime('Today');
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName, true);
|
||||
});
|
||||
|
||||
bdd.it('sets quick time on open', async function () {
|
||||
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
|
||||
const prettyPrint = await PageObjects.header.getPrettyDuration();
|
||||
expect(prettyPrint).to.equal('Today');
|
||||
});
|
||||
|
||||
bdd.it('is saved with absolute time', async function () {
|
||||
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
await PageObjects.dashboard.saveDashboard(dashboardName, true);
|
||||
});
|
||||
|
||||
bdd.it('sets absolute time on open', async function () {
|
||||
await PageObjects.header.setQuickTime('Today');
|
||||
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
|
||||
const fromTimeNext = await PageObjects.header.getFromTime();
|
||||
const toTimeNext = await PageObjects.header.getToTime();
|
||||
expect(fromTimeNext).to.equal(fromTime);
|
||||
expect(toTimeNext).to.equal(toTime);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -14,4 +14,5 @@ bdd.describe('dashboard app', function () {
|
|||
});
|
||||
|
||||
require('./_dashboard');
|
||||
require('./_dashboard_time');
|
||||
});
|
||||
|
|
|
@ -6,14 +6,12 @@ import bluebird, {
|
|||
import fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import mkdirp from 'mkdirp';
|
||||
import moment from 'moment';
|
||||
import path from 'path';
|
||||
import testSubjSelector from '@spalger/test-subj-selector';
|
||||
import {
|
||||
format,
|
||||
parse
|
||||
} from 'url';
|
||||
import util from 'util';
|
||||
|
||||
import getUrl from '../../utils/get_url';
|
||||
|
||||
|
@ -272,6 +270,35 @@ export default class Common {
|
|||
}
|
||||
}
|
||||
|
||||
async doesCssSelectorExist(selector) {
|
||||
PageObjects.common.debug(`doesCssSelectorExist ${selector}`);
|
||||
const exists = await this.remote
|
||||
.setFindTimeout(1000)
|
||||
.findByCssSelector(selector)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
this.remote.setFindTimeout(defaultFindTimeout);
|
||||
|
||||
PageObjects.common.debug(`exists? ${exists}`);
|
||||
return exists;
|
||||
}
|
||||
|
||||
findByCssSelector(selector) {
|
||||
PageObjects.common.debug(`findByCssSelector ${selector}`);
|
||||
return this.remote.setFindTimeout(defaultFindTimeout).findByCssSelector(selector);
|
||||
}
|
||||
|
||||
async doesTestSubjectExist(selector) {
|
||||
PageObjects.common.debug(`doesTestSubjectExist ${selector}`);
|
||||
const exists = await this.remote
|
||||
.setFindTimeout(1000)
|
||||
.findDisplayedByCssSelector(testSubjSelector(selector))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
this.remote.setFindTimeout(defaultFindTimeout);
|
||||
return exists;
|
||||
}
|
||||
|
||||
findTestSubject(selector) {
|
||||
this.debug('in findTestSubject: ' + testSubjSelector(selector));
|
||||
return this.remote
|
||||
|
@ -281,9 +308,17 @@ export default class Common {
|
|||
|
||||
async findAllTestSubjects(selector) {
|
||||
this.debug('in findAllTestSubjects: ' + testSubjSelector(selector));
|
||||
const remote = this.remote.setFindTimeout(defaultFindTimeout);
|
||||
const all = await remote.findAllByCssSelector(testSubjSelector(selector));
|
||||
const all = await this.findAllByCssSelector(testSubjSelector(selector));
|
||||
return await filterAsync(all, el => el.isDisplayed());
|
||||
}
|
||||
|
||||
async findAllByCssSelector(selector, timeout = defaultFindTimeout) {
|
||||
this.debug('in findAllByCssSelector: ' + selector);
|
||||
const remote = this.remote.setFindTimeout(timeout);
|
||||
let elements = await remote.findAllByCssSelector(selector);
|
||||
this.remote.setFindTimeout(defaultFindTimeout);
|
||||
if (!elements) elements = [];
|
||||
this.debug(`Found ${elements.length} for selector ${selector}`);
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import _ from 'lodash';
|
||||
import { defaultFindTimeout } from '../';
|
||||
|
||||
import {
|
||||
defaultFindTimeout,
|
||||
scenarioManager,
|
||||
esClient,
|
||||
elasticDump
|
||||
} from '../';
|
||||
|
||||
import PageObjects from './';
|
||||
|
||||
export default class DashboardPage {
|
||||
|
@ -12,20 +15,74 @@ export default class DashboardPage {
|
|||
this.findTimeout = this.remote.setFindTimeout(defaultFindTimeout);
|
||||
}
|
||||
|
||||
gotoDashboardLandingPage() {
|
||||
return this.findTimeout
|
||||
.findByCssSelector('a[href="#/dashboard"]')
|
||||
.click();
|
||||
async initTests() {
|
||||
const logstash = scenarioManager.loadIfEmpty('logstashFunctional');
|
||||
await esClient.deleteAndUpdateConfigDoc({ 'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*' });
|
||||
|
||||
PageObjects.common.debug('load kibana index with visualizations');
|
||||
await elasticDump.elasticLoad('dashboard','.kibana');
|
||||
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
|
||||
return logstash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if already on the dashboard landing page (that page doesn't have a link to itself).
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async onDashboardLandingPage() {
|
||||
PageObjects.common.debug(`onDashboardLandingPage`);
|
||||
const exists = await PageObjects.common.doesCssSelectorExist('a[href="#/dashboard"]');
|
||||
return !exists;
|
||||
}
|
||||
|
||||
async gotoDashboardLandingPage() {
|
||||
PageObjects.common.debug('Go to dashboard landing page');
|
||||
const onPage = await this.onDashboardLandingPage();
|
||||
if (!onPage) {
|
||||
return PageObjects.common.findByCssSelector('a[href="#/dashboard"]').click();
|
||||
}
|
||||
}
|
||||
|
||||
clickNewDashboard() {
|
||||
return PageObjects.common.findTestSubject('newDashboardLink')
|
||||
.click();
|
||||
return PageObjects.common.findTestSubject('newDashboardLink').click();
|
||||
}
|
||||
|
||||
clickAddVisualization() {
|
||||
return PageObjects.common.findTestSubject('dashboardAddPanelButton')
|
||||
.click();
|
||||
return PageObjects.common.findTestSubject('dashboardAddPanelButton').click();
|
||||
}
|
||||
|
||||
clickOptions() {
|
||||
return PageObjects.common.findTestSubject('dashboardOptionsButton').click();
|
||||
}
|
||||
|
||||
isOptionsOpen() {
|
||||
PageObjects.common.debug('isOptionsOpen');
|
||||
return PageObjects.common.doesTestSubjectExist('dashboardDarkThemeCheckbox');
|
||||
}
|
||||
|
||||
async openOptions() {
|
||||
PageObjects.common.debug('openOptions');
|
||||
const isOpen = await this.isOptionsOpen();
|
||||
if (!isOpen) {
|
||||
return PageObjects.common.findTestSubject('dashboardOptionsButton').click();
|
||||
}
|
||||
}
|
||||
|
||||
async isDarkThemeOn() {
|
||||
PageObjects.common.debug('isDarkThemeOn');
|
||||
await this.openOptions();
|
||||
const darkThemeCheckbox = await PageObjects.common.findTestSubject('dashboardDarkThemeCheckbox');
|
||||
return await darkThemeCheckbox.getProperty('checked');
|
||||
}
|
||||
|
||||
async useDarkTheme(on) {
|
||||
await this.openOptions();
|
||||
const isDarkThemeOn = await this.isDarkThemeOn();
|
||||
if (isDarkThemeOn !== on) {
|
||||
return PageObjects.common.findTestSubject('dashboardDarkThemeCheckbox').click();
|
||||
}
|
||||
}
|
||||
|
||||
filterVizNames(vizName) {
|
||||
|
@ -42,7 +99,7 @@ export default class DashboardPage {
|
|||
}
|
||||
|
||||
closeAddVizualizationPanel() {
|
||||
PageObjects.common.debug('-------------close panel');
|
||||
PageObjects.common.debug('closeAddVizualizationPanel');
|
||||
return this.findTimeout
|
||||
.findByCssSelector('i.fa fa-chevron-up')
|
||||
.click();
|
||||
|
@ -73,48 +130,36 @@ export default class DashboardPage {
|
|||
});
|
||||
}
|
||||
|
||||
saveDashboard(dashName) {
|
||||
return PageObjects.common.findTestSubject('dashboardSaveButton')
|
||||
.click()
|
||||
.then(() => {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.common.sleep(1000);
|
||||
})
|
||||
.then(() => {
|
||||
PageObjects.common.debug('saveButton button clicked');
|
||||
return this.findTimeout
|
||||
.findById('dashboardTitle')
|
||||
.type(dashName);
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.common.sleep(1000);
|
||||
})
|
||||
// click save button
|
||||
.then(() => {
|
||||
return PageObjects.common.try(() => {
|
||||
PageObjects.common.debug('clicking final Save button for named dashboard');
|
||||
return this.findTimeout
|
||||
.findByCssSelector('.btn-primary')
|
||||
.click();
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
})
|
||||
async saveDashboard(dashName, storeTimeWithDash) {
|
||||
await PageObjects.common.findTestSubject('dashboardSaveButton').click();
|
||||
|
||||
await PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
await PageObjects.common.sleep(1000);
|
||||
|
||||
PageObjects.common.debug('entering new title');
|
||||
await this.findTimeout.findById('dashboardTitle').type(dashName);
|
||||
|
||||
if (storeTimeWithDash !== undefined) {
|
||||
await this.storeTimeWithDashboard(storeTimeWithDash);
|
||||
}
|
||||
|
||||
await PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
await PageObjects.common.sleep(1000);
|
||||
|
||||
await PageObjects.common.try(() => {
|
||||
PageObjects.common.debug('clicking final Save button for named dashboard');
|
||||
return this.findTimeout.findByCssSelector('.btn-primary').click();
|
||||
});
|
||||
|
||||
await PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
|
||||
// verify that green message at the top of the page.
|
||||
// it's only there for about 5 seconds
|
||||
.then(() => {
|
||||
return PageObjects.common.try(() => {
|
||||
PageObjects.common.debug('verify toast-message for saved dashboard');
|
||||
return this.findTimeout
|
||||
await PageObjects.common.try(() => {
|
||||
PageObjects.common.debug('verify toast-message for saved dashboard');
|
||||
return this.findTimeout
|
||||
.findByCssSelector('kbn-truncated.toast-message.ng-isolate-scope')
|
||||
.getVisibleText();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -126,28 +171,18 @@ export default class DashboardPage {
|
|||
|
||||
// use the search filter box to narrow the results down to a single
|
||||
// entry, or at least to a single page of results
|
||||
loadSavedDashboard(dashName) {
|
||||
async loadSavedDashboard(dashName) {
|
||||
PageObjects.common.debug(`Load Saved Dashboard ${dashName}`);
|
||||
const self = this;
|
||||
return this.gotoDashboardLandingPage()
|
||||
.then(function filterDashboard() {
|
||||
PageObjects.common.debug('Load Saved Dashboard button clicked');
|
||||
return PageObjects.common.findTestSubject('searchFilter')
|
||||
.click()
|
||||
.type(dashName.replace('-',' '));
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.common.sleep(1000);
|
||||
})
|
||||
.then(function clickDashboardByLinkedText() {
|
||||
return self
|
||||
.clickDashboardByLinkText(dashName);
|
||||
})
|
||||
.then(() => {
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
});
|
||||
await this.gotoDashboardLandingPage();
|
||||
const searchBox = await PageObjects.common.findTestSubject('searchFilter');
|
||||
await searchBox.click();
|
||||
await searchBox.type(dashName.replace('-',' '));
|
||||
|
||||
await PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
await PageObjects.common.sleep(1000);
|
||||
await this.clickDashboardByLinkText(dashName);
|
||||
return PageObjects.header.isGlobalLoadingIndicatorHidden();
|
||||
}
|
||||
|
||||
getPanelTitles() {
|
||||
|
@ -216,4 +251,54 @@ export default class DashboardPage {
|
|||
});
|
||||
}
|
||||
|
||||
getTestVisualizationNames() {
|
||||
return [
|
||||
'Visualization PieChart',
|
||||
'Visualization☺ VerticalBarChart',
|
||||
'Visualization漢字 AreaChart',
|
||||
'Visualization☺漢字 DataTable',
|
||||
'Visualization漢字 LineChart',
|
||||
'Visualization TileMap',
|
||||
'Visualization MetricChart'
|
||||
];
|
||||
}
|
||||
|
||||
addVisualizations(visualizations) {
|
||||
return visualizations.reduce(function (promise, vizName) {
|
||||
return promise
|
||||
.then(() => PageObjects.dashboard.addVisualization(vizName));
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
async setTimepickerInDataRange() {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
|
||||
}
|
||||
|
||||
async storeTimeWithDashboard(on) {
|
||||
PageObjects.common.debug('Storing time with dashboard: ' + on);
|
||||
const storeTimeCheckbox = await PageObjects.common.findTestSubject('storeTimeWithDashboard');
|
||||
const checked = await storeTimeCheckbox.getProperty('checked');
|
||||
if (checked === true && on === false ||
|
||||
checked === false && on === true) {
|
||||
PageObjects.common.debug('Flipping store time checkbox');
|
||||
await storeTimeCheckbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
async getFilterDescriptions(timeout = defaultFindTimeout) {
|
||||
const filters = await PageObjects.common.findAllByCssSelector(
|
||||
'.filter-bar > .filter > .filter-description',
|
||||
timeout);
|
||||
return _.map(filters, async (filter) => await filter.getVisibleText());
|
||||
}
|
||||
|
||||
async filterOnPieSlice() {
|
||||
PageObjects.common.debug('Filtering on a pie slice');
|
||||
const slices = await PageObjects.common.findAllByCssSelector('svg > g > path.slice');
|
||||
PageObjects.common.debug('Slices found:' + slices.length);
|
||||
return slices[0].click();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,28 +12,29 @@ export default class HeaderPage {
|
|||
}
|
||||
|
||||
clickSelector(selector) {
|
||||
return this.try(() => {
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByCssSelector(selector)
|
||||
.then(tab => {
|
||||
return tab.click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clickDiscover() {
|
||||
PageObjects.common.debug('click Discover tab');
|
||||
this.clickSelector('a[href*=\'discover\']');
|
||||
return PageObjects.common.sleep(3000);
|
||||
}
|
||||
|
||||
clickVisualize() {
|
||||
PageObjects.common.debug('click Visualize tab');
|
||||
this.clickSelector('a[href*=\'visualize\']');
|
||||
return PageObjects.common.sleep(3000);
|
||||
}
|
||||
|
||||
clickDashboard() {
|
||||
PageObjects.common.debug('click Dashboard tab');
|
||||
this.clickSelector('a[href*=\'dashboard\']');
|
||||
return PageObjects.common.sleep(3000);
|
||||
}
|
||||
|
||||
clickSettings() {
|
||||
|
@ -48,6 +49,11 @@ export default class HeaderPage {
|
|||
});
|
||||
}
|
||||
|
||||
clickQuickButton() {
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByLinkText('Quick').click();
|
||||
}
|
||||
|
||||
isTimepickerOpen() {
|
||||
return this.remote.setFindTimeout(2000)
|
||||
.findDisplayedByCssSelector('.kbn-timepicker')
|
||||
|
@ -60,6 +66,20 @@ export default class HeaderPage {
|
|||
.findByLinkText('Absolute').click();
|
||||
}
|
||||
|
||||
async getFromTime() {
|
||||
await this.ensureTimePickerIsOpen();
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByCssSelector('input[ng-model=\'absolute.from\']')
|
||||
.getProperty('value');
|
||||
}
|
||||
|
||||
async getToTime() {
|
||||
await this.ensureTimePickerIsOpen();
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByCssSelector('input[ng-model=\'absolute.to\']')
|
||||
.getProperty('value');
|
||||
}
|
||||
|
||||
setFromTime(timeString) {
|
||||
return this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByCssSelector('input[ng-model=\'absolute.from\']')
|
||||
|
@ -129,4 +149,24 @@ export default class HeaderPage {
|
|||
.findByCssSelector('[data-test-subj="globalLoadingIndicator"].ng-hide');
|
||||
}
|
||||
|
||||
async ensureTimePickerIsOpen() {
|
||||
const isOpen = await PageObjects.header.isTimepickerOpen();
|
||||
PageObjects.common.debug(`time picker open: ${isOpen}`);
|
||||
if (!isOpen) {
|
||||
PageObjects.common.debug('--Opening time picker');
|
||||
await PageObjects.header.clickTimepicker();
|
||||
}
|
||||
}
|
||||
|
||||
async setQuickTime(quickTime) {
|
||||
await this.ensureTimePickerIsOpen();
|
||||
PageObjects.common.debug('--Clicking Quick button');
|
||||
await this.clickQuickButton();
|
||||
await this.remote.setFindTimeout(defaultFindTimeout)
|
||||
.findByLinkText(quickTime).click();
|
||||
}
|
||||
|
||||
async getPrettyDuration() {
|
||||
return await PageObjects.common.findTestSubject('globalTimepickerRange').getVisibleText();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue