mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Create the concept of embeddableHandlers (#12146)
* Move dashboard panel rendering logic to each registered type. * Remove dashboard knowledge of click and brush handlers for visualizations Move it to the VisualizeEmbeddableHandler. * merge master with manual changes * No need to use lodash * Add EmbeddableHandler base class * Use correct path to embeddable_handlers_registry * clean up * Set visualize scope uiState that is of type PersistedState, otherwise it won't actually be set. * add retry to loading saved search data * Fix handleError param and remove unnecessary private param * Rename savePanelState updatePanel and return the new object rather than mutating the original * Make ContainerAPI a base class and move the dashboard specific functionality into a new class * Make api's async and clean up documentation * Fix panel tests * Fix bug which broke tests - need to pass container-api to dashboard-panel * Address code comments - Rename onFilter to addFilter - Use angular promises instead of async/await - fix jsdoc - rename createChildUiState to getInitialState * changed the wrong variable name * no need for async or Promise.reject on interface * add tests that will fail due to spy pane issue in this PR * Fix logic with spy pane toggle There is still a bit of a bug here as mentioned in https://github.com/elastic/kibana/issues/13340 but it will be fixed separately as it’s also an issue in master * Fix failing test
This commit is contained in:
parent
9f05b8d5b4
commit
6e744521b3
25 changed files with 453 additions and 294 deletions
|
@ -59,7 +59,8 @@ export default function (kibana) {
|
|||
'navbarExtensions',
|
||||
'managementSections',
|
||||
'devTools',
|
||||
'docViews'
|
||||
'docViews',
|
||||
'embeddableHandlers',
|
||||
],
|
||||
injectVars,
|
||||
},
|
||||
|
|
|
@ -2,14 +2,18 @@ import angular from 'angular';
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import 'plugins/kibana/dashboard/saved_dashboard/saved_dashboard';
|
||||
import { DashboardContainerAPI } from '../dashboard_container_api';
|
||||
import { DashboardState } from '../dashboard_state';
|
||||
import { DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
|
||||
describe('dashboard panels', function () {
|
||||
let $scope;
|
||||
let $el;
|
||||
let AppState;
|
||||
|
||||
function compile(dashboard) {
|
||||
ngMock.inject(($rootScope, $controller, $compile, $route) => {
|
||||
ngMock.inject(($injector, $rootScope, $controller, $compile, $route) => {
|
||||
AppState = $injector.get('AppState');
|
||||
$scope = $rootScope.$new();
|
||||
$route.current = {
|
||||
locals: {
|
||||
|
@ -18,6 +22,8 @@ describe('dashboard panels', function () {
|
|||
params: {}
|
||||
};
|
||||
|
||||
const dashboardState = new DashboardState(dashboard, AppState, false);
|
||||
$scope.containerApi = new DashboardContainerAPI(dashboardState);
|
||||
$el = angular.element(`
|
||||
<dashboard-app>
|
||||
<dashboard-grid
|
||||
|
@ -25,12 +31,9 @@ describe('dashboard panels', function () {
|
|||
ng-if="!hasExpandedPanel()"
|
||||
on-panel-removed="onPanelRemoved"
|
||||
panels="panels"
|
||||
get-vis-click-handler="filterBarClickHandler"
|
||||
get-vis-brush-handler="brushEvent"
|
||||
save-state="saveState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
create-child-ui-state="createChildUiState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
container-api="containerApi"
|
||||
></dashboard-grid>
|
||||
</dashboard-app>`);
|
||||
$compile($el)($scope);
|
||||
|
|
|
@ -3,28 +3,30 @@ import ngMock from 'ng_mock';
|
|||
import Promise from 'bluebird';
|
||||
import sinon from 'sinon';
|
||||
import noDigestPromise from 'test_utils/no_digest_promises';
|
||||
import mockUiState from 'fixtures/mock_ui_state';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { DashboardContainerAPI } from '../dashboard_container_api';
|
||||
import { DashboardState } from '../dashboard_state';
|
||||
import { SavedObjectsClient } from 'ui/saved_objects';
|
||||
|
||||
describe('dashboard panel', function () {
|
||||
let $scope;
|
||||
let $el;
|
||||
let parentScope;
|
||||
let savedDashboard;
|
||||
let AppState;
|
||||
|
||||
noDigestPromise.activateForSuite();
|
||||
|
||||
function init(mockDocResponse) {
|
||||
ngMock.module('kibana');
|
||||
ngMock.inject(($rootScope, $compile, Private) => {
|
||||
Private.swap(SavedObjectsClientProvider, () => {
|
||||
return {
|
||||
get: sinon.stub().returns(Promise.resolve(mockDocResponse))
|
||||
};
|
||||
});
|
||||
|
||||
ngMock.inject(($rootScope, $compile, Private, $injector) => {
|
||||
const SavedDashboard = $injector.get('SavedDashboard');
|
||||
AppState = $injector.get('AppState');
|
||||
savedDashboard = new SavedDashboard();
|
||||
sinon.stub(SavedObjectsClient.prototype, 'get').returns(Promise.resolve(mockDocResponse));
|
||||
parentScope = $rootScope.$new();
|
||||
parentScope.saveState = sinon.stub();
|
||||
parentScope.createChildUiState = sinon.stub().returns(mockUiState);
|
||||
const dashboardState = new DashboardState(savedDashboard, AppState, false);
|
||||
parentScope.containerApi = new DashboardContainerAPI(dashboardState);
|
||||
parentScope.getVisClickHandler = sinon.stub();
|
||||
parentScope.getVisBrushHandler = sinon.stub();
|
||||
parentScope.registerPanelIndexPattern = sinon.stub();
|
||||
|
@ -41,11 +43,8 @@ describe('dashboard panel', function () {
|
|||
panel="panel"
|
||||
is-full-screen-mode="false"
|
||||
is-expanded="false"
|
||||
get-vis-click-handler="getVisClickHandler"
|
||||
get-vis-brush-handler="getVisBrushHandler"
|
||||
save-state="saveState"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
create-child-ui-state="createChildUiState">
|
||||
container-api="containerApi"
|
||||
>
|
||||
</dashboard-panel>`)(parentScope);
|
||||
$scope = $el.isolateScope();
|
||||
parentScope.$digest();
|
||||
|
@ -53,27 +52,28 @@ describe('dashboard panel', function () {
|
|||
}
|
||||
|
||||
afterEach(() => {
|
||||
SavedObjectsClient.prototype.get.restore();
|
||||
$scope.$destroy();
|
||||
$el.remove();
|
||||
});
|
||||
|
||||
it('should not visualize the visualization if it does not exist', function () {
|
||||
init({ found: false });
|
||||
return $scope.loadedPanel.then(() => {
|
||||
return $scope.renderPromise.then(() => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should try to visualize the visualization if found', function () {
|
||||
init({ id: 'foo1', type: 'visualization', _version: 2, attributes: {} });
|
||||
return $scope.loadedPanel.then(() => {
|
||||
return $scope.renderPromise.then(() => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -80,16 +80,10 @@
|
|||
on-panel-removed="onPanelRemoved"
|
||||
dashboard-view-mode="dashboardViewMode"
|
||||
panels="panels"
|
||||
get-vis-click-handler="getFilterBarClickHandler"
|
||||
get-vis-brush-handler="getBrushEvent"
|
||||
save-state="saveState"
|
||||
app-state="appState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
create-child-ui-state="createChildUiState"
|
||||
toggle-expand="toggleExpandPanel"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
data-shared-items-count="{{panels.length}}"
|
||||
on-filter="filter"
|
||||
container-api="containerApi"
|
||||
></dashboard-grid>
|
||||
|
||||
<dashboard-panel
|
||||
|
@ -98,12 +92,7 @@
|
|||
is-full-screen-mode="!chrome.getVisible()"
|
||||
is-expanded="true"
|
||||
dashboard-view-mode="dashboardViewMode"
|
||||
get-vis-click-handler="getFilterBarClickHandler"
|
||||
get-vis-brush-handler="getBrushEvent"
|
||||
save-state="saveState"
|
||||
app-state="appState"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
create-child-ui-state="createChildUiState"
|
||||
container-api="containerApi"
|
||||
toggle-expand="toggleExpandPanel(expandedPanel.panelIndex)"
|
||||
></dashboard-panel>
|
||||
|
||||
|
|
|
@ -19,16 +19,14 @@ import { DocTitleProvider } from 'ui/doc_title';
|
|||
import { getTopNavConfig } from './top_nav/get_top_nav_config';
|
||||
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
|
||||
import { VisualizeConstants } from 'plugins/kibana/visualize/visualize_constants';
|
||||
import { UtilsBrushEventProvider } from 'ui/utils/brush_event';
|
||||
import { FilterBarClickHandlerProvider } from 'ui/filter_bar/filter_bar_click_handler';
|
||||
import { DashboardState } from './dashboard_state';
|
||||
import { notify } from 'ui/notify';
|
||||
import './panel/get_object_loaders_for_dashboard';
|
||||
import { documentationLinks } from 'ui/documentation_links/documentation_links';
|
||||
import { showCloneModal } from './top_nav/show_clone_modal';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import { QueryManagerProvider } from 'ui/query_manager';
|
||||
import { ESC_KEY_CODE } from 'ui_framework/services';
|
||||
import { DashboardContainerAPI } from './dashboard_container_api';
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
'elasticsearch',
|
||||
|
@ -86,8 +84,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const confirmModal = $injector.get('confirmModal');
|
||||
const config = $injector.get('config');
|
||||
const Private = $injector.get('Private');
|
||||
const brushEvent = Private(UtilsBrushEventProvider);
|
||||
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
|
@ -103,8 +99,10 @@ app.directive('dashboardApp', function ($injector) {
|
|||
docTitle.change(dash.title);
|
||||
}
|
||||
|
||||
const dashboardState = new DashboardState(dash, AppState, dashboardConfig);
|
||||
const dashboardState = new DashboardState(dash, AppState, dashboardConfig.getHideWriteControls());
|
||||
$scope.appState = dashboardState.getAppState();
|
||||
const queryManager = Private(QueryManagerProvider)(dashboardState.getAppState());
|
||||
$scope.containerApi = new DashboardContainerAPI(dashboardState, queryManager);
|
||||
|
||||
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
|
||||
// normal cross app navigation.
|
||||
|
@ -124,6 +122,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
};
|
||||
$scope.panels = dashboardState.getPanels();
|
||||
$scope.fullScreenMode = dashboardState.getFullScreenMode();
|
||||
$scope.indexPatterns = dashboardState.getPanelIndexPatterns();
|
||||
};
|
||||
|
||||
// Part of the exposed plugin API - do not remove without careful consideration.
|
||||
|
@ -155,11 +154,8 @@ app.directive('dashboardApp', function ($injector) {
|
|||
$scope.timefilter = timefilter;
|
||||
$scope.expandedPanel = null;
|
||||
$scope.dashboardViewMode = dashboardState.getViewMode();
|
||||
$scope.appState = dashboardState.getAppState();
|
||||
|
||||
$scope.landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
|
||||
$scope.getBrushEvent = () => brushEvent(dashboardState.getAppState());
|
||||
$scope.getFilterBarClickHandler = () => filterBarClickHandler(dashboardState.getAppState());
|
||||
$scope.hasExpandedPanel = () => $scope.expandedPanel !== null;
|
||||
$scope.getDashTitle = () => getDashboardTitle(
|
||||
dashboardState.getTitle(),
|
||||
|
@ -212,17 +208,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
notify.info(`Search successfully added to your dashboard`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a child ui state for the panel. It's passed the ui state to use, but needs to
|
||||
* be generated from the parent (why, I don't know yet).
|
||||
* @param path {String} - the unique path for this ui state.
|
||||
* @param uiState {Object} - the uiState for the child.
|
||||
* @returns {Object}
|
||||
*/
|
||||
$scope.createChildUiState = function createChildUiState(path, uiState) {
|
||||
return dashboardState.uiState.createChild(path, uiState, true);
|
||||
};
|
||||
|
||||
$scope.$watch('model.darkTheme', () => {
|
||||
dashboardState.setDarkTheme($scope.model.darkTheme);
|
||||
updateTheme();
|
||||
|
@ -242,12 +227,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
$scope.indexPatterns = dashboardState.getPanelIndexPatterns();
|
||||
};
|
||||
|
||||
$scope.filter = function (field, value, operator, index) {
|
||||
queryManager.add(field, value, operator, index);
|
||||
updateState();
|
||||
};
|
||||
|
||||
|
||||
$scope.$watch('model.query', (newQuery) => {
|
||||
$scope.model.query = migrateLegacyQuery(newQuery);
|
||||
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { ContainerAPI } from 'ui/embeddable';
|
||||
|
||||
export class DashboardContainerAPI extends ContainerAPI {
|
||||
constructor(dashboardState, queryManager) {
|
||||
super();
|
||||
this.dashboardState = dashboardState;
|
||||
this.queryManager = queryManager;
|
||||
}
|
||||
|
||||
addFilter(field, value, operator, index) {
|
||||
this.queryManager.add(field, value, operator, index);
|
||||
}
|
||||
|
||||
updatePanel(panelIndex, panelAttributes) {
|
||||
const panelToUpdate = this.dashboardState.getPanels().find((panel) => panel.panelIndex === panelIndex);
|
||||
Object.assign(panelToUpdate, panelAttributes);
|
||||
this.dashboardState.saveState();
|
||||
return panelToUpdate;
|
||||
}
|
||||
|
||||
getAppState() {
|
||||
return this.dashboardState.appState;
|
||||
}
|
||||
|
||||
createChildUistate(path, initialState) {
|
||||
return this.dashboardState.uiState.createChild(path, initialState, true);
|
||||
}
|
||||
|
||||
registerPanelIndexPattern(panelIndex, pattern) {
|
||||
this.dashboardState.registerPanelIndexPatternMap(panelIndex, pattern);
|
||||
this.dashboardState.saveState();
|
||||
}
|
||||
|
||||
}
|
|
@ -59,13 +59,13 @@ export class DashboardState {
|
|||
*
|
||||
* @param savedDashboard {SavedDashboard}
|
||||
* @param AppState {AppState}
|
||||
* @param dashboardConfig {DashboardConfigProvider}
|
||||
* @param hideWriteControls {boolean} true if write controls should be hidden.
|
||||
*/
|
||||
constructor(savedDashboard, AppState, dashboardConfig) {
|
||||
constructor(savedDashboard, AppState, hideWriteControls) {
|
||||
this.savedDashboard = savedDashboard;
|
||||
this.dashboardConfig = dashboardConfig;
|
||||
this.hideWriteControls = hideWriteControls;
|
||||
|
||||
this.stateDefaults = getStateDefaults(this.savedDashboard, this.dashboardConfig.getHideWriteControls());
|
||||
this.stateDefaults = getStateDefaults(this.savedDashboard, this.hideWriteControls);
|
||||
|
||||
this.appState = new AppState(this.stateDefaults);
|
||||
this.uiState = this.appState.makeStateful('uiState');
|
||||
|
@ -117,7 +117,7 @@ export class DashboardState {
|
|||
// The right way to fix this might be to ensure the defaults object stored on state is a deep
|
||||
// clone, but given how much code uses the state object, I determined that to be too risky of a change for
|
||||
// now. TODO: revisit this!
|
||||
this.stateDefaults = getStateDefaults(this.savedDashboard, this.dashboardConfig.getHideWriteControls());
|
||||
this.stateDefaults = getStateDefaults(this.savedDashboard, this.hideWriteControls);
|
||||
// The original query won't be restored by the above because the query on this.savedDashboard is applied
|
||||
// in place in order for it to affect the visualizations.
|
||||
this.stateDefaults.query = this.lastSavedDashboardFilters.query;
|
||||
|
@ -259,7 +259,7 @@ export class DashboardState {
|
|||
* @returns {DashboardViewMode}
|
||||
*/
|
||||
getViewMode() {
|
||||
return this.dashboardConfig.getHideWriteControls() ? DashboardViewMode.VIEW : this.appState.viewMode;
|
||||
return this.hideWriteControls ? DashboardViewMode.VIEW : this.appState.viewMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,17 +18,6 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
* @type {DashboardViewMode}
|
||||
*/
|
||||
dashboardViewMode: '=',
|
||||
/**
|
||||
* Used to create a child persisted state for the panel from parent state.
|
||||
* @type {function} - Returns a {PersistedState} child uiState for this scope.
|
||||
*/
|
||||
createChildUiState: '=',
|
||||
/**
|
||||
* Registers an index pattern with the dashboard app used by each panel. The index patterns are used by the
|
||||
* filter bar for generating field suggestions.
|
||||
* @type {function(IndexPattern)}
|
||||
*/
|
||||
registerPanelIndexPattern: '=',
|
||||
/**
|
||||
* Trigger after a panel has been removed from the grid.
|
||||
*/
|
||||
|
@ -38,22 +27,11 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
* @type {Array<PanelState>}
|
||||
*/
|
||||
panels: '=',
|
||||
/**
|
||||
* Returns a click handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisClickHandler: '=',
|
||||
/**
|
||||
* Returns a brush event handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisBrushHandler: '=',
|
||||
/**
|
||||
* Call when changes should be propagated to the url and thus saved in state.
|
||||
* @type {function}
|
||||
*/
|
||||
saveState: '=',
|
||||
appState: '=',
|
||||
/**
|
||||
* Expand or collapse a panel, so it either takes up the whole screen or goes back to its
|
||||
* natural size.
|
||||
|
@ -61,10 +39,9 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
*/
|
||||
toggleExpand: '=',
|
||||
/**
|
||||
* Called when a filter action has been triggered by a panel
|
||||
* @type {function}
|
||||
* @type {DashboardContainerApi}
|
||||
*/
|
||||
onFilter: '=',
|
||||
containerApi: '=',
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
const notify = new Notifier();
|
||||
|
@ -222,14 +199,9 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
is-full-screen-mode="isFullScreenMode"
|
||||
is-expanded="false"
|
||||
dashboard-view-mode="dashboardViewMode"
|
||||
get-vis-click-handler="getVisClickHandler"
|
||||
get-vis-brush-handler="getVisBrushHandler"
|
||||
save-state="saveState"
|
||||
app-state="appState"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
container-api="containerApi"
|
||||
toggle-expand="toggleExpand(${panel.panelIndex})"
|
||||
create-child-ui-state="createChildUiState"
|
||||
on-filter="onFilter">
|
||||
>
|
||||
</li>`;
|
||||
const panelElement = $compile(panelHtml)($scope);
|
||||
panelElementMapping[panel.panelIndex] = panelElement;
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('app/dashboard');
|
||||
|
||||
/**
|
||||
* We have more types available than just 'search' and 'visualization' but as of now, they
|
||||
* can't be added to a dashboard.
|
||||
*/
|
||||
module.factory('getObjectLoadersForDashboard', function (savedSearches, savedVisualizations) {
|
||||
return () => [savedSearches, savedVisualizations];
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/**
|
||||
* Retrieves the saved object represented by the panel and returns it, along with the appropriate
|
||||
* edit Url.
|
||||
* @param {Array.<SavedObjectLoader>} loaders - The available loaders for different panel types.
|
||||
* @param {PanelState} panel
|
||||
* @returns {Promise.<{savedObj: SavedObject, editUrl: String}>}
|
||||
*/
|
||||
export function loadSavedObject(loaders, panel) {
|
||||
const loader = loaders.find((loader) => loader.type === panel.type);
|
||||
if (!loader) {
|
||||
throw new Error(`No loader for object of type ${panel.type}`);
|
||||
}
|
||||
return loader.get(panel.id).then(savedObj => {
|
||||
return { savedObj, editUrl: loader.urlFor(panel.id) };
|
||||
});
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
<div class="panel panel-default" data-test-subj="dashboardPanel" ng-class="{'panel--edit-mode': !isViewOnlyMode()}" ng-switch on="panel.type" ng-if="savedObj || error">
|
||||
<div class="panel panel-default" data-test-subj="dashboardPanel" 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
|
||||
|
@ -76,36 +76,5 @@
|
|||
<span ng-bind-html="error | markdown"></span>
|
||||
</div>
|
||||
|
||||
<visualize
|
||||
ng-if="!error"
|
||||
ng-switch-when="visualization"
|
||||
show-spy-panel="!isFullScreenMode"
|
||||
saved-obj="savedObj"
|
||||
app-state="appState"
|
||||
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
|
||||
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 id="embeddedPanel" class="panel-content"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
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 { 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/embeddable/embeddable_handlers_registry';
|
||||
|
||||
uiModules
|
||||
.get('app/dashboard')
|
||||
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector, getObjectLoadersForDashboard) {
|
||||
|
||||
.directive('dashboardPanel', function (Notifier, Private, $injector) {
|
||||
const services = savedObjectManagementRegistry.all().map(function (serviceObj) {
|
||||
const service = $injector.get(serviceObj.service);
|
||||
return {
|
||||
|
@ -38,17 +33,6 @@ uiModules
|
|||
* @type {boolean}
|
||||
*/
|
||||
isFullScreenMode: '=',
|
||||
/**
|
||||
* Used to create a child persisted state for the panel from parent state.
|
||||
* @type {function} - Returns a {PersistedState} child uiState for this scope.
|
||||
*/
|
||||
createChildUiState: '=',
|
||||
/**
|
||||
* Registers an index pattern with the dashboard app used by this panel. Used by the filter bar for
|
||||
* generating field suggestions.
|
||||
* @type {function(IndexPattern)}
|
||||
*/
|
||||
registerPanelIndexPattern: '=',
|
||||
/**
|
||||
* Contains information about this panel.
|
||||
* @type {PanelState}
|
||||
|
@ -70,122 +54,58 @@ uiModules
|
|||
*/
|
||||
isExpanded: '=',
|
||||
/**
|
||||
* Returns a click handler for a visualization.
|
||||
* @type {function}
|
||||
* @type {DashboardContainerApi}
|
||||
*/
|
||||
getVisClickHandler: '=',
|
||||
/**
|
||||
* Returns a brush event handler for a visualization.
|
||||
* @type {function}
|
||||
*/
|
||||
getVisBrushHandler: '=',
|
||||
/**
|
||||
* Call when changes should be propagated to the url and thus saved in state.
|
||||
* @type {function}
|
||||
*/
|
||||
saveState: '=',
|
||||
/**
|
||||
* Called when a filter action has been triggered
|
||||
* @type {function}
|
||||
*/
|
||||
onFilter: '=',
|
||||
appState: '=',
|
||||
containerApi: '='
|
||||
},
|
||||
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();
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
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();
|
||||
$scope.registerPanelIndexPattern($scope.panel.panelIndex, $scope.savedObj.vis.indexPattern);
|
||||
} else if ($scope.panel.type === savedSearches.type) {
|
||||
if ($scope.savedObj.searchSource) {
|
||||
$scope.registerPanelIndexPattern($scope.panel.panelIndex, $scope.savedObj.searchSource.get('index'));
|
||||
}
|
||||
// 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;
|
||||
|
||||
$scope.setSortOrder = function setSortOrder(columnName, direction) {
|
||||
$scope.panel.sort = [columnName, direction];
|
||||
$scope.saveState();
|
||||
};
|
||||
|
||||
$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
|
||||
};
|
||||
|
||||
$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
|
||||
};
|
||||
|
||||
$scope.moveColumn = function moveColumn(columnName, newIndex) {
|
||||
columnActions.moveColumn($scope.panel.columns, columnName, newIndex);
|
||||
$scope.saveState(); // sync to sharing url
|
||||
};
|
||||
}
|
||||
|
||||
$scope.filter = function (field, value, operator) {
|
||||
const index = $scope.savedObj.searchSource.get('index').id;
|
||||
$scope.onFilter(field, value, operator, index);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
$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;
|
||||
};
|
||||
|
||||
const panelId = $scope.panel.id;
|
||||
|
||||
// TODO: This function contains too much internal panel knowledge. Logic should be pushed to embeddable handlers.
|
||||
const handleError = (error) => {
|
||||
$scope.error = error.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 = error.savedObjectType === $scope.panel.type;
|
||||
if (objectItselfDeleted) return;
|
||||
|
||||
const type = $scope.panel.type;
|
||||
const service = services.find(service => service.type === type);
|
||||
if (!service) return;
|
||||
|
||||
$scope.editUrl = '#management/kibana/objects/' + service.name + '/' + panelId + '?notFound=' + error.savedObjectType;
|
||||
};
|
||||
|
||||
const embeddableHandlers = Private(EmbeddableHandlersRegistryProvider);
|
||||
const embeddableHandler = embeddableHandlers.byName[$scope.panel.type];
|
||||
if (!embeddableHandler) {
|
||||
handleError(new Error(`No embeddable handler for panel type ${$scope.panel.type} was found.`));
|
||||
return;
|
||||
}
|
||||
embeddableHandler.getEditPath(panelId).then(path => {
|
||||
$scope.editUrl = path;
|
||||
});
|
||||
embeddableHandler.getTitleFor(panelId).then(title => {
|
||||
$scope.title = title;
|
||||
});
|
||||
$scope.renderPromise = embeddableHandler.render(
|
||||
element.find('#embeddedPanel').get(0),
|
||||
$scope.panel,
|
||||
$scope.containerApi)
|
||||
.catch(handleError);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
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 { EmbeddableHandler } from 'ui/embeddable';
|
||||
|
||||
|
||||
export class SearchEmbeddableHandler extends EmbeddableHandler {
|
||||
|
||||
constructor($compile, $rootScope, searchLoader, Promise) {
|
||||
super();
|
||||
this.$compile = $compile;
|
||||
this.searchLoader = searchLoader;
|
||||
this.$rootScope = $rootScope;
|
||||
this.name = 'search';
|
||||
this.Promise = Promise;
|
||||
}
|
||||
|
||||
getEditPath(panelId) {
|
||||
return this.Promise.resolve(this.searchLoader.urlFor(panelId));
|
||||
}
|
||||
|
||||
getTitleFor(panelId) {
|
||||
return this.searchLoader.get(panelId).then(savedObject => savedObject.title);
|
||||
}
|
||||
|
||||
render(domNode, panel, container) {
|
||||
const searchScope = this.$rootScope.$new();
|
||||
return this.getEditPath(panel.id)
|
||||
.then(editPath => {
|
||||
searchScope.editPath = editPath;
|
||||
return this.searchLoader.get(panel.id);
|
||||
})
|
||||
.then(savedObject => {
|
||||
searchScope.savedObj = savedObject;
|
||||
searchScope.panel = panel;
|
||||
container.registerPanelIndexPattern(panel.panelIndex, savedObject.searchSource.get('index'));
|
||||
|
||||
// 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.
|
||||
searchScope.panel = container.updatePanel(searchScope.panel.panelIndex, {
|
||||
columns: searchScope.panel.columns || searchScope.savedObj.columns,
|
||||
sort: searchScope.panel.sort || searchScope.savedObj.sort
|
||||
});
|
||||
|
||||
const uiState = savedObject.uiStateJSON ? JSON.parse(savedObject.uiStateJSON) : {};
|
||||
searchScope.uiState = container.createChildUistate(getPersistedStateId(panel), uiState);
|
||||
|
||||
searchScope.setSortOrder = function setSortOrder(columnName, direction) {
|
||||
searchScope.panel = container.updatePanel(searchScope.panel.panelIndex, { sort: [columnName, direction] });
|
||||
};
|
||||
|
||||
searchScope.addColumn = function addColumn(columnName) {
|
||||
savedObject.searchSource.get('index').popularizeField(columnName, 1);
|
||||
columnActions.addColumn(searchScope.panel.columns, columnName);
|
||||
searchScope.panel = container.updatePanel(searchScope.panel.panelIndex, { columns: searchScope.panel.columns });
|
||||
};
|
||||
|
||||
searchScope.removeColumn = function removeColumn(columnName) {
|
||||
savedObject.searchSource.get('index').popularizeField(columnName, 1);
|
||||
columnActions.removeColumn(searchScope.panel.columns, columnName);
|
||||
searchScope.panel = container.updatePanel(searchScope.panel.panelIndex, { columns: searchScope.panel.columns });
|
||||
};
|
||||
|
||||
searchScope.moveColumn = function moveColumn(columnName, newIndex) {
|
||||
columnActions.moveColumn(searchScope.panel.columns, columnName, newIndex);
|
||||
searchScope.panel = container.updatePanel(searchScope.panel.panelIndex, { columns: searchScope.panel.columns });
|
||||
};
|
||||
|
||||
searchScope.filter = function (field, value, operator) {
|
||||
const index = savedObject.searchSource.get('index').id;
|
||||
container.addFilter(field, value, operator, index);
|
||||
};
|
||||
|
||||
const searchInstance = this.$compile(searchTemplate)(searchScope);
|
||||
const rootNode = angular.element(domNode);
|
||||
rootNode.append(searchInstance);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { SearchEmbeddableHandler } from './search_embeddable_handler';
|
||||
import { EmbeddableHandlersRegistryProvider } from 'ui/embeddable/embeddable_handlers_registry';
|
||||
|
||||
export function searchEmbeddableHandlerProvider(Private) {
|
||||
const SearchEmbeddableHandlerProvider = ($compile, $rootScope, savedSearches, Promise) => {
|
||||
return new SearchEmbeddableHandler($compile, $rootScope, savedSearches, Promise);
|
||||
};
|
||||
return Private(SearchEmbeddableHandlerProvider);
|
||||
}
|
||||
|
||||
|
||||
EmbeddableHandlersRegistryProvider.register(searchEmbeddableHandlerProvider);
|
|
@ -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>
|
|
@ -0,0 +1,63 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import visualizationTemplate from './visualize_template.html';
|
||||
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
|
||||
import { UtilsBrushEventProvider as utilsBrushEventProvider } from 'ui/utils/brush_event';
|
||||
import { FilterBarClickHandlerProvider as filterBarClickHandlerProvider } from 'ui/filter_bar/filter_bar_click_handler';
|
||||
import { EmbeddableHandler } from 'ui/embeddable';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
export class VisualizeEmbeddableHandler extends EmbeddableHandler {
|
||||
constructor($compile, $rootScope, visualizeLoader, timefilter, Notifier, Promise) {
|
||||
super();
|
||||
this.$compile = $compile;
|
||||
this.visualizeLoader = visualizeLoader;
|
||||
this.$rootScope = $rootScope;
|
||||
this.name = 'visualization';
|
||||
this.Promise = Promise;
|
||||
this.brushEvent = utilsBrushEventProvider(timefilter);
|
||||
this.filterBarClickHandler = filterBarClickHandlerProvider(Notifier);
|
||||
}
|
||||
|
||||
getEditPath(panelId) {
|
||||
return this.Promise.resolve(this.visualizeLoader.urlFor(panelId));
|
||||
}
|
||||
|
||||
getTitleFor(panelId) {
|
||||
return this.visualizeLoader.get(panelId).then(savedObject => savedObject.title);
|
||||
}
|
||||
|
||||
render(domNode, panel, container) {
|
||||
const visualizeScope = this.$rootScope.$new();
|
||||
return this.getEditPath(panel.id)
|
||||
.then(editPath => {
|
||||
visualizeScope.editUrl = editPath;
|
||||
return this.visualizeLoader.get(panel.id);
|
||||
})
|
||||
.then(savedObject => {
|
||||
visualizeScope.savedObj = savedObject;
|
||||
visualizeScope.panel = panel;
|
||||
|
||||
const uiState = savedObject.uiStateJSON ? JSON.parse(savedObject.uiStateJSON) : {};
|
||||
visualizeScope.uiState = container.createChildUistate(getPersistedStateId(panel), uiState);
|
||||
|
||||
visualizeScope.savedObj.vis.setUiState(visualizeScope.uiState);
|
||||
|
||||
visualizeScope.savedObj.vis.listeners.click = this.filterBarClickHandler(container.getAppState());
|
||||
visualizeScope.savedObj.vis.listeners.brush = this.brushEvent(container.getAppState());
|
||||
visualizeScope.isFullScreenMode = !chrome.getVisible();
|
||||
|
||||
container.registerPanelIndexPattern(panel.panelIndex, visualizeScope.savedObj.vis.indexPattern);
|
||||
|
||||
const visualizationInstance = this.$compile(visualizationTemplate)(visualizeScope);
|
||||
const rootNode = angular.element(domNode);
|
||||
rootNode.append(visualizationInstance);
|
||||
|
||||
visualizationInstance.on('$destroy', function () {
|
||||
visualizeScope.savedObj.destroy();
|
||||
visualizeScope.$destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { VisualizeEmbeddableHandler } from './visualize_embeddable_handler';
|
||||
import { EmbeddableHandlersRegistryProvider } from 'ui/embeddable/embeddable_handlers_registry';
|
||||
|
||||
export function visualizeEmbeddableHandlerProvider(Private) {
|
||||
const VisualizeEmbeddableHandlerProvider = (
|
||||
$compile,
|
||||
$rootScope,
|
||||
savedVisualizations,
|
||||
timefilter,
|
||||
Notifier,
|
||||
Promise) => {
|
||||
return new VisualizeEmbeddableHandler($compile, $rootScope, savedVisualizations, timefilter, Notifier, Promise);
|
||||
};
|
||||
return Private(VisualizeEmbeddableHandlerProvider);
|
||||
}
|
||||
|
||||
EmbeddableHandlersRegistryProvider.register(visualizeEmbeddableHandlerProvider);
|
|
@ -0,0 +1,11 @@
|
|||
<visualize
|
||||
show-spy-panel="!isFullScreenMode"
|
||||
saved-obj="savedObj"
|
||||
app-state="appState"
|
||||
ui-state="uiState"
|
||||
data-shared-item
|
||||
data-title="{{savedObj.title}}"
|
||||
data-description="{{savedObj.description}}"
|
||||
render-counter
|
||||
class="panel-content">
|
||||
</visualize>
|
51
src/ui/public/embeddable/container_api.js
Normal file
51
src/ui/public/embeddable/container_api.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* The ContainerAPI is an interface for embeddable objects to interact with the container they are embedded within.
|
||||
*/
|
||||
export class ContainerAPI {
|
||||
/**
|
||||
* Available so the embeddable object can trigger a filter action.
|
||||
* @param field
|
||||
* @param value
|
||||
* @param operator
|
||||
* @param index
|
||||
*/
|
||||
addFilter(/*field, value, operator, index */) {
|
||||
throw new Error('Must implement addFilter.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {AppState}
|
||||
*/
|
||||
getAppState() {
|
||||
throw new Error('Must implement getAppState.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new state for the panel. It's passed the ui state object to use, and is returned
|
||||
* a PersistedState.
|
||||
* @param path {String} - the unique path for this ui state.
|
||||
* @param initialState {Object} - the initial state to use for the child.
|
||||
* @returns {PersistedState}
|
||||
*/
|
||||
createChildUistate(/* path, initialState */) {
|
||||
throw new Error('Must implement getInitalState.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to tell the container that this panel uses a particular index pattern.
|
||||
* @param {string} panelIndex - a unique id that identifies the panel to update.
|
||||
* @param {string} indexPattern - an index pattern the panel uses
|
||||
*/
|
||||
registerPanelIndexPattern(/* panelIndex, indexPattern */) {
|
||||
throw new Error('Must implement registerPanelIndexPattern.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} panelIndex - a unique id that identifies the panel to update.
|
||||
* @param {Object} panelAttributes - the new panel attributes that will be applied to the panel.
|
||||
* @return {Object} - the updated panel.
|
||||
*/
|
||||
updatePanel(/*paneIndex, panelAttributes */) {
|
||||
throw new Error('Must implement updatePanel.');
|
||||
}
|
||||
}
|
33
src/ui/public/embeddable/embeddable_handler.js
Normal file
33
src/ui/public/embeddable/embeddable_handler.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* The EmbeddableHandler defines how to render and embed any object into the Dashboard, or some other
|
||||
* container that supports EmbeddableHandlers.
|
||||
*/
|
||||
export class EmbeddableHandler {
|
||||
/**
|
||||
* @param {string} panelId - the id of the panel to grab the title for.
|
||||
* @return {Promise.<string>} a promise that resolves with the path that dictates where the user will be navigated to
|
||||
* when they click the edit icon.
|
||||
*/
|
||||
getEditPath(/* panelId */) {
|
||||
throw new Error('Must implement getEditPath.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} panelId - the id of the panel to grab the title for.
|
||||
* @return {Promise.<string>} - Promise that resolves with the title to display for the particular panel.
|
||||
*/
|
||||
getTitleFor(/* panelId */) {
|
||||
throw new Error('Must implement getTitleFor.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} domNode - the dom node to mount the rendered embeddable on
|
||||
* @param {PanelState} panel - a panel object which container information about the panel. Can also be modified to
|
||||
* store per panel information.
|
||||
* @property {ContainerApi} containerApi - an id to specify the object that this panel contains.
|
||||
* @param {Promise.<void>} A promise that resolves when the object is finished rendering.
|
||||
*/
|
||||
render(/* domNode, panel, container */) {
|
||||
throw new Error('Must implement render.');
|
||||
}
|
||||
}
|
9
src/ui/public/embeddable/embeddable_handlers_registry.js
Normal file
9
src/ui/public/embeddable/embeddable_handlers_registry.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { uiRegistry } from 'ui/registry/_registry';
|
||||
|
||||
/**
|
||||
* Registry of functions (EmbeddableHandlerProviders) which return an EmbeddableHandler.
|
||||
*/
|
||||
export const EmbeddableHandlersRegistryProvider = uiRegistry({
|
||||
name: 'embeddableHandlers',
|
||||
index: ['name']
|
||||
});
|
3
src/ui/public/embeddable/index.js
Normal file
3
src/ui/public/embeddable/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { EmbeddableHandler } from './embeddable_handler';
|
||||
export { EmbeddableHandlersRegistryProvider } from './embeddable_handlers_registry';
|
||||
export { ContainerAPI } from './container_api';
|
|
@ -21,6 +21,10 @@ export default class UiExports {
|
|||
visEditorTypes: [
|
||||
'ui/vis/editors/default/default',
|
||||
],
|
||||
embeddableHandlers: [
|
||||
'plugins/kibana/visualize/embeddable/visualize_embeddable_handler_provider',
|
||||
'plugins/kibana/discover/embeddable/search_embeddable_handler_provider',
|
||||
],
|
||||
};
|
||||
this.urlBasePath = urlBasePath;
|
||||
this.exportConsumer = _.memoize(this.exportConsumer);
|
||||
|
@ -105,6 +109,7 @@ export default class UiExports {
|
|||
case 'visRequestHandlers':
|
||||
case 'visEditorTypes':
|
||||
case 'savedObjectTypes':
|
||||
case 'embeddableHandlers':
|
||||
case 'fieldFormats':
|
||||
case 'fieldFormatEditors':
|
||||
case 'spyModes':
|
||||
|
|
|
@ -182,7 +182,22 @@ export default function ({ getService, getPageObjects }) {
|
|||
expect(spyToggleExists).to.be(true);
|
||||
});
|
||||
|
||||
// This was an actual bug that appeared, where the spy pane appeared on panels after adding them, but
|
||||
// disappeared when a new dashboard was opened up.
|
||||
it('shows the spy pane toggle directly after opening a dashboard', async () => {
|
||||
await PageObjects.dashboard.saveDashboard('spy pane test');
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard('spy pane test');
|
||||
const panels = await PageObjects.dashboard.getDashboardPanels();
|
||||
// Simulate hover
|
||||
await remote.moveMouseTo(panels[0]);
|
||||
const spyToggleExists = await PageObjects.visualize.getSpyToggleExists();
|
||||
expect(spyToggleExists).to.be(true);
|
||||
});
|
||||
|
||||
it('shows other panels after being minimized', async () => {
|
||||
// Panels are all minimized on a fresh open of a dashboard, so we need to re-expand in order to then minimize.
|
||||
await PageObjects.dashboard.toggleExpandPanel();
|
||||
await PageObjects.dashboard.toggleExpandPanel();
|
||||
const panels = await PageObjects.dashboard.getDashboardPanels();
|
||||
const visualizations = PageObjects.dashboard.getTestVisualizations();
|
||||
|
@ -222,6 +237,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
|
||||
describe('full screen mode', () => {
|
||||
it('option not available in edit mode', async () => {
|
||||
await PageObjects.dashboard.clickEdit();
|
||||
const exists = await PageObjects.dashboard.fullScreenModeMenuItemExists();
|
||||
expect(exists).to.be(false);
|
||||
});
|
||||
|
|
|
@ -310,10 +310,12 @@ export default function ({ getService, getPageObjects }) {
|
|||
description: 'A Saved Search Description'
|
||||
};
|
||||
|
||||
await PageObjects.discover.loadSavedSearch(expected.title);
|
||||
const { title, description } = await PageObjects.common.getSharedItemTitleAndDescription();
|
||||
expect(title).to.eql(expected.title);
|
||||
expect(description).to.eql(expected.description);
|
||||
await retry.try(async () => {
|
||||
await PageObjects.discover.loadSavedSearch(expected.title);
|
||||
const { title, description } = await PageObjects.common.getSharedItemTitleAndDescription();
|
||||
expect(title).to.eql(expected.title);
|
||||
expect(description).to.eql(expected.description);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue