Skip to content

Commit

Permalink
Move dashboard panel rendering logic to each registered type.
Browse files Browse the repository at this point in the history
Let external plugins register panel renderer providers

keep loadedPanel promise on the scope for tests

Not great to keep this around only for tests, but it follows the
previous logic.

Fix tests that expect the panel-content div to be the old style

Communicate panel title from renderer.

Fix tests with extra request

Theres an extra request in there because of the getTitle function. Our
tests assumed one, this makes it handle any number of requests in them
mget (assuming the mock response for each one is sufficient).
  • Loading branch information
stacey-gammon committed Jun 2, 2017
1 parent 46c832d commit 70f1827
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 151 deletions.
3 changes: 2 additions & 1 deletion src/core_plugins/kibana/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ module.exports = function (kibana) {
'navbarExtensions',
'managementSections',
'devTools',
'docViews'
'docViews',
'embeddableHandlers',
],
injectVars,
},
Expand Down
16 changes: 13 additions & 3 deletions src/core_plugins/kibana/public/dashboard/__tests__/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ describe('dashboard panel', function () {
function init(mockDocResponse) {
ngMock.module('kibana');
ngMock.inject(($rootScope, $compile, esAdmin) => {
sinon.stub(esAdmin, 'mget').returns(Promise.resolve({ docs: [ mockDocResponse ] }));
sinon.stub(esAdmin, 'mget', function (request) {
const response = {
docs: []
};
request.body.docs.forEach(() => {
response.docs.push(mockDocResponse);
});

return Promise.resolve(response);
});

sinon.stub(esAdmin.indices, 'getFieldMapping').returns(Promise.resolve({
'.kibana': {
mappings: {
Expand Down Expand Up @@ -63,7 +73,7 @@ describe('dashboard panel', function () {
expect($scope.error).to.be('Could not locate that visualization (id: foo1)');
parentScope.$digest();
const content = $el.find('.panel-content');
expect(content).to.have.length(0);
expect(content.children().length).to.be(0);
});
});

Expand All @@ -73,7 +83,7 @@ describe('dashboard panel', function () {
expect($scope.error).not.to.be.ok();
parentScope.$digest();
const content = $el.find('.panel-content');
expect(content).to.have.length(1);
expect(content.children().length).to.be.greaterThan(0);
});
});
});

This file was deleted.

This file was deleted.

40 changes: 6 additions & 34 deletions src/core_plugins/kibana/public/dashboard/panel/panel.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<div class="panel panel-default" ng-class="{'panel--edit-mode': !isViewOnlyMode()}" ng-switch on="panel.type" ng-if="savedObj || error">
<div class="panel panel-default" ng-class="{'panel--edit-mode': !isViewOnlyMode()}" ng-switch on="panel.type">
<div class="panel-heading">
<span
data-test-subj="dashboardPanelTitle"
class="panel-title"
aria-label="{{:: 'Dashboard panel: ' + savedObj.title }}"
aria-label="{{:: 'Dashboard panel: ' + title }}"
>
{{::savedObj.title}}
{{::title}}
</span>
<div class="kuiMicroButtonGroup">
<a
Expand Down Expand Up @@ -76,36 +76,8 @@
<span ng-bind="error"></span>
</div>

<visualize
ng-if="!error"
ng-switch-when="visualization"
vis="savedObj.vis"
search-source="savedObj.searchSource"
show-spy-panel="!isFullScreenMode"
ui-state="uiState"
data-shared-item
data-title="{{savedObj.title}}"
data-description="{{savedObj.description}}"
render-counter
class="panel-content">
</visualize>

<doc-table
ng-if="!error"
ng-switch-when="search"
search-source="savedObj.searchSource"
sorting="panel.sort"
columns="panel.columns"
data-shared-item
data-title="{{savedObj.title}}"
data-description="{{savedObj.description}}"
render-counter
<div
id="renderPanel"
class="panel-content"
filter="filter"
on-add-column="addColumn"
on-change-sort-order="setSortOrder"
on-move-column="moveColumn"
on-remove-column="removeColumn"
>
</doc-table>
></div>
</div>
132 changes: 45 additions & 87 deletions src/core_plugins/kibana/public/dashboard/panel/panel.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import _ from 'lodash';
import 'ui/visualize';
import 'ui/doc_table';
import * as columnActions from 'ui/doc_table/actions/columns';
import 'plugins/kibana/dashboard/panel/get_object_loaders_for_dashboard';
import 'plugins/kibana/visualize/saved_visualizations';
import 'plugins/kibana/discover/saved_searches';
import { FilterManagerProvider } from 'ui/filter_manager';
import { uiModules } from 'ui/modules';
import panelTemplate from 'plugins/kibana/dashboard/panel/panel.html';
import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
import { loadSavedObject } from 'plugins/kibana/dashboard/panel/load_saved_object';
import { DashboardViewMode } from '../dashboard_view_mode';
import { EmbeddableHandlersRegistryProvider } from 'ui/registry/embeddable_handlers';

uiModules
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector, getObjectLoadersForDashboard) {
const filterManager = Private(FilterManagerProvider);

.directive('dashboardPanel', function (Notifier, Private, $injector) {
const services = savedObjectManagementRegistry.all().map(function (serviceObj) {
const service = $injector.get(serviceObj.service);
return {
Expand Down Expand Up @@ -84,94 +78,58 @@ uiModules
link: function ($scope, element) {
if (!$scope.panel.id || !$scope.panel.type) return;

/**
* Initializes the panel for the saved object.
* @param {{savedObj: SavedObject, editUrl: String}} savedObjectInfo
*/
function initializePanel(savedObjectInfo) {
$scope.savedObj = savedObjectInfo.savedObj;
$scope.editUrl = savedObjectInfo.editUrl;

element.on('$destroy', function () {
$scope.savedObj.destroy();
$scope.$destroy();
});
const saveState = (panel) => {
$scope.panel = Object.assign($scope.panel, panel);
$scope.saveState();
};

// create child ui state from the savedObj
const uiState = $scope.savedObj.uiStateJSON ? JSON.parse($scope.savedObj.uiStateJSON) : {};
$scope.uiState = $scope.createChildUiState(getPersistedStateId($scope.panel), uiState);
$scope.isViewOnlyMode = () => {
return $scope.dashboardViewMode === DashboardViewMode.VIEW || $scope.isFullScreenMode;
};

if ($scope.panel.type === savedVisualizations.type && $scope.savedObj.vis) {
$scope.savedObj.vis.setUiState($scope.uiState);
$scope.savedObj.vis.listeners.click = $scope.getVisClickHandler();
$scope.savedObj.vis.listeners.brush = $scope.getVisBrushHandler();
} else if ($scope.panel.type === savedSearches.type) {
// This causes changes to a saved search to be hidden, but also allows
// the user to locally modify and save changes to a saved search only in a dashboard.
// See https://github.com/elastic/kibana/issues/9523 for more details.
$scope.panel.columns = $scope.panel.columns || $scope.savedObj.columns;
$scope.panel.sort = $scope.panel.sort || $scope.savedObj.sort;
// TODO: vis actions should be generalized for use by all panel renderers, e.g. updateFilters, updateTimeRange.
const actions = {
getVisClickHandler: $scope.getVisClickHandler,
getVisBrushHandler: $scope.getVisBrushHandler,
saveState,
getIsViewOnlyMode: $scope.isViewOnlyMode,
createChildUiState: $scope.createChildUiState
};

$scope.setSortOrder = function setSortOrder(columnName, direction) {
$scope.panel.sort = [columnName, direction];
$scope.saveState();
};
const handleError = (error) => {
$scope.error = error.message;

$scope.addColumn = function addColumn(columnName) {
$scope.savedObj.searchSource.get('index').popularizeField(columnName, 1);
columnActions.addColumn($scope.panel.columns, columnName);
$scope.saveState(); // sync to sharing url
};
// Dashboard listens for this broadcast, once for every visualization (pendingVisCount).
// We need to broadcast even in the event of an error or it'll never fetch the data for
// other visualizations.
$scope.$root.$broadcast('ready:vis');

$scope.removeColumn = function removeColumn(columnName) {
$scope.savedObj.searchSource.get('index').popularizeField(columnName, 1);
columnActions.removeColumn($scope.panel.columns, columnName);
$scope.saveState(); // sync to sharing url
};
// If the savedObjectType matches the panel type, this means the object itself has been deleted,
// so we shouldn't even have an edit link. If they don't match, it means something else is wrong
// with the object (but the object still exists), so we link to the object editor instead.
const objectItselfDeleted = error.savedObjectType === $scope.panel.type;
if (objectItselfDeleted) return;

$scope.moveColumn = function moveColumn(columnName, newIndex) {
columnActions.moveColumn($scope.panel.columns, columnName, newIndex);
$scope.saveState(); // sync to sharing url
};
}
const type = $scope.panel.type;
const id = $scope.panel.id;
const service = _.find(services, { type: type });
if (!service) return;

$scope.filter = function (field, value, operator) {
const index = $scope.savedObj.searchSource.get('index').id;
filterManager.add(field, value, operator, index);
};
$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + error.savedObjectType;
};

const embeddableHandlers = Private(EmbeddableHandlersRegistryProvider);
const embeddableHandler = embeddableHandlers.byName[$scope.panel.type];
if (!embeddableHandler) {
handleError(`No embeddable handler for panel type ${$scope.panel.type} was found.`);
return;
}

$scope.loadedPanel = loadSavedObject(getObjectLoadersForDashboard(), $scope.panel)
.then(initializePanel)
.catch(function (e) {
$scope.error = e.message;

// Dashboard listens for this broadcast, once for every visualization (pendingVisCount).
// We need to broadcast even in the event of an error or it'll never fetch the data for
// other visualizations.
$scope.$root.$broadcast('ready:vis');

// If the savedObjectType matches the panel type, this means the object itself has been deleted,
// so we shouldn't even have an edit link. If they don't match, it means something else is wrong
// with the object (but the object still exists), so we link to the object editor instead.
const objectItselfDeleted = e.savedObjectType === $scope.panel.type;
if (objectItselfDeleted) return;

const type = $scope.panel.type;
const id = $scope.panel.id;
const service = _.find(services, { type: type });
if (!service) return;

$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + id + '?notFound=' + e.savedObjectType;
});

/**
* @returns {boolean} True if the user can only view, not edit.
*/
$scope.isViewOnlyMode = () => {
return $scope.dashboardViewMode === DashboardViewMode.VIEW || $scope.isFullScreenMode;
};
$scope.editUrl = embeddableHandler.getEditPath($scope.panel);
embeddableHandler.getTitleFor($scope.panel).then(title => {
$scope.title = title;
});
$scope.loadedPanel =
embeddableHandler.renderAt(element.find('.panel-content').get(0), $scope.panel, actions).catch(handleError);
}
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import searchTemplate from './search_template.html';
import angular from 'angular';
import * as columnActions from 'ui/doc_table/actions/columns';
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
import { FilterManagerProvider } from 'ui/filter_manager';

export class SearchEmbeddableHandler {
constructor($compile, $rootScope, searchLoader, Private) {
this.$compile = $compile;
this.searchLoader = searchLoader;
this.filterManager = Private(FilterManagerProvider);
this.$rootScope = $rootScope;
this.name = 'search';
this.title = 'Saved Searches';
}

getEditPath(panel) {
return this.searchLoader.urlFor(panel.id);
}

canRenderType(type) {
return type === 'search';
}

getTitleFor(panel) {
return this.searchLoader.get(panel.id).then(savedObject => savedObject.title);
}

renderAt(domNode, panel, actions) {
return this.searchLoader.get(panel.id).then((savedObject) => {
const editUrl = this.searchLoader.urlFor(panel.id);
const searchScope = this.$rootScope.$new();
searchScope.editUrl = editUrl;
searchScope.savedObj = savedObject;
searchScope.panel = panel;

// This causes changes to a saved search to be hidden, but also allows
// the user to locally modify and save changes to a saved search only in a dashboard.
// See https://github.com/elastic/kibana/issues/9523 for more details.
actions.saveState({
columns: searchScope.panel.columns || searchScope.savedObj.columns,
sort: searchScope.panel.sort || searchScope.savedObj.sort
});

const uiState = savedObject.uiStateJSON ? JSON.parse(savedObject.uiStateJSON) : {};
searchScope.uiState = actions.createChildUiState(getPersistedStateId(panel), uiState);

searchScope.setSortOrder = function setSortOrder(columnName, direction) {
actions.saveState({ sort: [columnName, direction] });
};

searchScope.addColumn = function addColumn(columnName) {
savedObject.searchSource.get('index').popularizeField(columnName, 1);
columnActions.addColumn(searchScope.panel.columns, columnName);
actions.saveState({}); // sync to sharing url
};

searchScope.removeColumn = function removeColumn(columnName) {
savedObject.searchSource.get('index').popularizeField(columnName, 1);
columnActions.removeColumn(searchScope.panel.columns, columnName);
actions.saveState({}); // sync to sharing url
};

searchScope.moveColumn = function moveColumn(columnName, newIndex) {
columnActions.moveColumn(searchScope.panel.columns, columnName, newIndex);
actions.saveState({}); // sync to sharing url
};

searchScope.filter = function (field, value, operator) {
const index = savedObject.searchSource.get('index').id;
this.filterManager.add(field, value, operator, index);
};

const searchInstance = this.$compile(searchTemplate)(searchScope);
const rootNode = angular.element(domNode);
rootNode.append(searchInstance);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SearchEmbeddableHandler } from './search_embeddable_handler';

export function searchEmbeddableHandlerProvider(Private) {
const SearchEmbeddableHandlerProvider = ($compile, $rootScope, savedSearches, Private) => {
return new SearchEmbeddableHandler($compile, $rootScope, savedSearches, Private);
};
return Private(SearchEmbeddableHandlerProvider);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<doc-table
search-source="savedObj.searchSource"
sorting="panel.sort"
columns="panel.columns"
data-shared-item
data-title="{{savedObj.title}}"
data-description="{{savedObj.description}}"
render-counter
class="panel-content"
filter="filter"
on-add-column="addColumn"
on-change-sort-order="setSortOrder"
on-move-column="moveColumn"
on-remove-column="removeColumn"
>
</doc-table>
Loading

0 comments on commit 70f1827

Please sign in to comment.