remove visualize loader (#46910) (#50319)

# Conflicts:
#	src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts
#	src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts
This commit is contained in:
Peter Pisljar 2019-11-12 11:03:25 -05:00 committed by GitHub
parent 1dc8933b72
commit f4b918da11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 338 additions and 2932 deletions

View file

@ -24,7 +24,7 @@ import { wrapInI18nContext } from 'ui/i18n';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { npStart } from 'ui/new_platform';
import { FilterBar, ApplyFiltersPopover } from '../filter';
import { FilterBar } from '../filter';
import { IndexPatterns } from '../index_patterns/index_patterns';
/** @internal */
@ -70,54 +70,7 @@ export const initLegacyModule = once((indexPatterns: IndexPatterns): void => {
['className', { watchDepth: 'reference' }],
['pluginDataStart', { watchDepth: 'reference' }],
]);
})
.directive('applyFiltersPopover', () => {
return {
restrict: 'E',
template: '',
compile: (elem: any) => {
const child = document.createElement('apply-filters-popover-helper');
// Copy attributes to the child directive
for (const attr of elem[0].attributes) {
child.setAttribute(attr.name, attr.value);
}
// Add a key attribute that will force a full rerender every time that
// a filter changes.
child.setAttribute('key', 'key');
// Append helper directive
elem.append(child);
const linkFn = ($scope: any, _: any, $attr: any) => {
// Watch only for filter changes to update key.
$scope.$watch(
() => {
return $scope.$eval($attr.filters) || [];
},
(newVal: any) => {
$scope.key = Date.now();
},
true
);
};
return linkFn;
},
};
})
.directive('applyFiltersPopoverHelper', (reactDirective: any) =>
reactDirective(wrapInI18nContext(ApplyFiltersPopover), [
['filters', { watchDepth: 'collection' }],
['onCancel', { watchDepth: 'reference' }],
['onSubmit', { watchDepth: 'reference' }],
['indexPatterns', { watchDepth: 'collection' }],
// Key is needed to trigger a full rerender of the component
'key',
])
);
});
uiModules.get('kibana/index_patterns').value('indexPatterns', indexPatterns);
});

View file

@ -38,17 +38,18 @@ export class ExpressionDataHandler {
private inspectorAdapters: Adapters;
private promise: Promise<IInterpreterResult>;
public isPending: boolean = true;
constructor(expression: string | ExpressionAST, params: IExpressionLoaderParams) {
if (typeof expression === 'string') {
this.expression = expression;
this.ast = fromExpression(expression) as ExpressionAST;
} else {
this.ast = expression;
this.expression = toExpression(expression);
this.expression = toExpression(this.ast);
}
this.abortController = new AbortController();
this.inspectorAdapters = this.getActiveInspectorAdapters();
this.inspectorAdapters = params.inspectorAdapters || this.getActiveInspectorAdapters();
const getInitialContext = () => ({
type: 'kibana_context',
@ -58,11 +59,21 @@ export class ExpressionDataHandler {
const defaultContext = { type: 'null' };
const interpreter = getInterpreter();
this.promise = interpreter.interpretAst(this.ast, params.context || defaultContext, {
getInitialContext,
inspectorAdapters: this.inspectorAdapters,
abortSignal: this.abortController.signal,
});
this.promise = interpreter
.interpretAst(this.ast, params.context || defaultContext, {
getInitialContext,
inspectorAdapters: this.inspectorAdapters,
abortSignal: this.abortController.signal,
})
.then(
(v: IInterpreterResult) => {
this.isPending = false;
return v;
},
() => {
this.isPending = false;
}
);
}
cancel = () => {

View file

@ -67,7 +67,7 @@ describe('execute helper function', () => {
});
describe('ExpressionLoader', () => {
const expressionString = 'test';
const expressionString = 'demodata';
describe('constructor', () => {
it('accepts expression string', () => {
@ -134,6 +134,8 @@ describe('ExpressionLoader', () => {
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData: () => true,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});
@ -160,10 +162,15 @@ describe('ExpressionLoader', () => {
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});
@ -193,6 +200,8 @@ describe('ExpressionLoader', () => {
(ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({
getData,
cancel: cancelMock,
isPending: () => true,
inspect: () => {},
}));
const expressionLoader = new ExpressionLoader(element, expressionString, {});

View file

@ -38,11 +38,12 @@ export class ExpressionLoader {
private loadingSubject: Subject<void>;
private data: Data;
private params: IExpressionLoaderParams = {};
private ignoreNextResponse = false;
constructor(
element: HTMLElement,
expression: string | ExpressionAST,
params: IExpressionLoaderParams
expression?: string | ExpressionAST,
params?: IExpressionLoaderParams
) {
this.dataSubject = new Subject();
this.data$ = this.dataSubject.asObservable().pipe(share());
@ -65,7 +66,9 @@ export class ExpressionLoader {
this.setParams(params);
this.loadData(expression, this.params);
if (expression) {
this.loadData(expression, this.params);
}
}
destroy() {
@ -117,9 +120,10 @@ export class ExpressionLoader {
update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): void {
this.setParams(params);
this.loadingSubject.next();
if (expression) {
this.loadData(expression, this.params);
} else {
} else if (this.data) {
this.render(this.data);
}
}
@ -128,18 +132,22 @@ export class ExpressionLoader {
expression: string | ExpressionAST,
params: IExpressionLoaderParams
): Promise<void> => {
this.loadingSubject.next();
if (this.dataHandler) {
if (this.dataHandler && this.dataHandler.isPending) {
this.ignoreNextResponse = true;
this.dataHandler.cancel();
}
this.setParams(params);
this.dataHandler = new ExpressionDataHandler(expression, params);
if (!params.inspectorAdapters) params.inspectorAdapters = this.dataHandler.inspect();
const data = await this.dataHandler.getData();
if (this.ignoreNextResponse) {
this.ignoreNextResponse = false;
return;
}
this.dataSubject.next(data);
};
private render(data: Data): void {
this.loadingSubject.next();
this.renderHandler.render(data, this.params.extraHandlers);
}
@ -148,23 +156,16 @@ export class ExpressionLoader {
return;
}
if (params.searchContext && this.params.searchContext) {
if (params.searchContext) {
this.params.searchContext = _.defaults(
{},
params.searchContext,
this.params.searchContext
this.params.searchContext || {}
) as any;
}
if (params.extraHandlers && this.params) {
this.params.extraHandlers = params.extraHandlers;
}
if (!Object.keys(this.params).length) {
this.params = {
...params,
searchContext: { type: 'kibana_context', ...(params.searchContext || {}) },
};
}
}
}

View file

@ -22,9 +22,15 @@ import { i18n } from '@kbn/i18n';
import { AggConfigs } from 'ui/agg_types/agg_configs';
import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
import chrome from 'ui/chrome';
import { TimeRange } from 'src/plugins/data/public';
import { Query, TimeRange, esFilters } from 'src/plugins/data/public';
import { SearchSource } from '../../../../ui/public/courier/search_source';
import { FilterBarQueryFilterProvider } from '../../../../ui/public/filter_manager/query_filter';
// @ts-ignore
import {
FilterBarQueryFilterProvider,
QueryFilter,
} from '../../../../ui/public/filter_manager/query_filter';
import { buildTabularInspectorData } from '../../../../ui/public/inspector/build_tabular_inspector_data';
import {
getRequestInspectorStats,
@ -32,15 +38,30 @@ import {
} from '../../../../ui/public/courier/utils/courier_inspector_utils';
import { calculateObjectHash } from '../../../../ui/public/vis/lib/calculate_object_hash';
import { getTime } from '../../../../ui/public/timefilter';
import { RequestHandlerParams } from '../../../../ui/public/visualize/loader/embedded_visualize_handler';
import { KibanaContext, KibanaDatatable } from '../../common';
import { ExpressionFunction, KibanaDatatableColumn } from '../../types';
import { start as data } from '../../../data/public/legacy';
export interface RequestHandlerParams {
searchSource: SearchSource;
aggs: AggConfigs;
timeRange?: TimeRange;
query?: Query;
filters?: esFilters.Filter[];
forceFetch: boolean;
queryFilter: QueryFilter;
uiState?: PersistedState;
partialRows?: boolean;
inspectorAdapters: Adapters;
metricsAtAllLevels?: boolean;
visParams?: any;
abortSignal?: AbortSignal;
}
// @ts-ignore
import { tabifyAggResponse } from '../../../../ui/public/agg_response/tabify/tabify';
// @ts-ignore
import { SearchSourceProvider } from '../../../../ui/public/courier/search_source';
import { KibanaContext, KibanaDatatable } from '../../common';
import { ExpressionFunction, KibanaDatatableColumn } from '../../types';
import { start as data } from '../../../data/public/legacy';
import { PersistedState } from '../../../../ui/public/persisted_state';
import { Adapters } from '../../../../../plugins/inspector/public';
const name = 'esaggs';

View file

@ -18,9 +18,11 @@
*/
import chrome from 'ui/chrome';
import { visualizationLoader } from 'ui/visualize/loader/visualization_loader';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
// @ts-ignore
import { VisProvider } from 'ui/visualize/loader/vis';
import { VisProvider } from '../../../../ui/public/visualize/loader/vis';
import { Visualization } from '../../../../ui/public/visualize/components';
export const visualization = () => ({
name: 'visualization',
@ -50,17 +52,27 @@ export const visualization = () => ({
type: visType,
params: visConfig,
});
handlers.vis.eventsSubject = handlers.eventsSubject;
}
handlers.vis.eventsSubject = { next: handlers.event };
const uiState = handlers.uiState || handlers.vis.getUiState();
handlers.onDestroy(() => visualizationLoader.destroy());
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
await visualizationLoader
.render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params)
.then(() => {
if (handlers.done) handlers.done();
});
const listenOnChange = params ? params.listenOnChange : false;
render(
<Visualization
vis={handlers.vis}
visData={visData}
visParams={handlers.vis.params}
uiState={uiState}
listenOnChange={listenOnChange}
onInit={handlers.done}
/>,
domNode
);
},
});

View file

@ -42,13 +42,6 @@
index-patterns="indexPatterns"
></filter-bar>
<apply-filters-popover
filters="appState.$newFilters"
on-cancel="onCancelApplyFilters"
on-submit="onApplyFilters"
index-patterns="indexPatterns"
></apply-filters-popover>
<div ng-show="getShouldShowEditHelp() || getShouldShowViewHelp()" class="dshStartScreen">
<div class="euiPanel euiPanel--paddingLarge euiPageContent euiPageContent--horizontalCenter">
<icon type="'dashboardApp'" size="'xxl'" color="'subdued'"></icon>

View file

@ -56,9 +56,7 @@ import { capabilities } from 'ui/capabilities';
import { Subscription } from 'rxjs';
import { npStart } from 'ui/new_platform';
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
import { extractTimeFilter, changeTimeFilter } from '../../../../../plugins/data/public';
import { start as data } from '../../../data/public/legacy';
import { esFilters } from '../../../../../plugins/data/public';
import {
DashboardContainer,
@ -417,31 +415,6 @@ export class DashboardAppController {
queryFilter.setFilters(filters);
};
$scope.onCancelApplyFilters = () => {
$scope.appState.$newFilters = [];
};
$scope.onApplyFilters = filters => {
if (filters.length) {
// All filters originated from one visualization.
const indexPatternId = filters[0].meta.index;
const indexPattern = _.find(
$scope.indexPatterns,
(p: IndexPattern) => p.id === indexPatternId
);
if (indexPattern && indexPattern.timeFieldName) {
const { timeRangeFilter, restOfFilters } = extractTimeFilter(
indexPattern.timeFieldName,
filters
);
queryFilter.addFilters(restOfFilters);
if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter);
}
}
$scope.appState.$newFilters = [];
};
$scope.onQuerySaved = savedQuery => {
$scope.savedQuery = savedQuery;
};
@ -514,12 +487,6 @@ export class DashboardAppController {
}
);
$scope.$watch('appState.$newFilters', (filters: esFilters.Filter[] = []) => {
if (filters.length === 1) {
$scope.onApplyFilters(filters);
}
});
$scope.indexPatterns = [];
$scope.$watch('model.query', (newQuery: Query) => {

View file

@ -31,7 +31,7 @@ import editorTemplate from './editor.html';
import { DashboardConstants } from '../../dashboard/dashboard_constants';
import { VisualizeConstants } from '../visualize_constants';
import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
import { extractTimeFilter, changeTimeFilter } from '../../../../../../plugins/data/public';
import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util';
import {
@ -342,23 +342,6 @@ function VisEditor(
queryFilter.setFilters(filters);
};
$scope.onCancelApplyFilters = () => {
$scope.state.$newFilters = [];
};
$scope.onApplyFilters = filters => {
const { timeRangeFilter, restOfFilters } = extractTimeFilter($scope.indexPattern.timeFieldName, filters);
queryFilter.addFilters(restOfFilters);
if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter);
$scope.state.$newFilters = [];
};
$scope.$watch('state.$newFilters', (filters = []) => {
if (filters.length === 1) {
$scope.onApplyFilters(filters);
}
});
$scope.showSaveQuery = capabilities.visualize.saveQuery;
$scope.$watch(() => capabilities.visualize.saveQuery, (newCapability) => {
@ -457,6 +440,12 @@ function VisEditor(
next: $scope.fetch
}));
subscriptions.add(subscribeWithScope($scope, timefilter.getAutoRefreshFetch$(), {
next: () => {
$scope.vis.forceReload();
}
}));
$scope.$on('$destroy', function () {
if ($scope._handler) {
$scope._handler.destroy();

View file

@ -17,39 +17,53 @@
* under the License.
*/
import _ from 'lodash';
import { EmbeddedVisualizeHandler } from 'ui/visualize/loader/embedded_visualize_handler';
import _, { forEach } from 'lodash';
import { StaticIndexPattern } from 'ui/index_patterns';
import { PersistedState } from 'ui/persisted_state';
import { Subscription } from 'rxjs';
import * as Rx from 'rxjs';
import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers';
import { SavedObject } from 'ui/saved_objects/saved_object';
import { Vis } from 'ui/vis';
import { SearchSource } from 'ui/courier';
import { queryGeohashBounds } from 'ui/visualize/loader/utils';
import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities';
import { AppState } from 'ui/state_management/app_state';
import { npStart } from 'ui/new_platform';
import { IExpressionLoaderParams } from '../../../../expressions/public/np_ready/public/types';
import { start as expressions } from '../../../../expressions/public/legacy';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
import { Query } from '../../../../data/public';
import {
TimeRange,
onlyDisabledFiltersChanged,
esFilters,
} from '../../../../../../plugins/data/public';
import { Query } from '../../../../data/public';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
import {
AppState,
Container,
Embeddable,
EmbeddableInput,
EmbeddableOutput,
PersistedState,
StaticIndexPattern,
VisSavedObject,
VisualizeLoader,
VisualizeLoaderParams,
VisualizeUpdateParams,
} from '../kibana_services';
Embeddable,
Container,
APPLY_FILTER_TRIGGER,
} from '../../../../../../plugins/embeddable/public';
import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public';
import { mapAndFlattenFilters } from '../../../../../../plugins/data/public';
const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;
export interface VisSavedObject extends SavedObject {
vis: Vis;
description?: string;
searchSource: SearchSource;
title: string;
uiStateJSON?: string;
destroy: () => void;
}
export interface VisualizeEmbeddableConfiguration {
savedVisualization: VisSavedObject;
indexPatterns?: StaticIndexPattern[];
editUrl: string;
loader: VisualizeLoader;
editable: boolean;
appState?: AppState;
uiState?: PersistedState;
@ -73,24 +87,28 @@ export interface VisualizeOutput extends EmbeddableOutput {
visTypeName: string;
}
type ExpressionLoader = InstanceType<typeof expressions.ExpressionLoader>;
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
private handler?: ExpressionLoader;
private savedVisualization: VisSavedObject;
private loader: VisualizeLoader;
private appState: AppState | undefined;
private uiState: PersistedState;
private handler?: EmbeddedVisualizeHandler;
private timeRange?: TimeRange;
private query?: Query;
private title?: string;
private filters?: esFilters.Filter[];
private visCustomizations: VisualizeInput['vis'];
private subscription: Subscription;
private subscriptions: Subscription[] = [];
private expression: string = '';
private actions: any = {};
private vis: Vis;
private domNode: any;
public readonly type = VISUALIZE_EMBEDDABLE_TYPE;
constructor(
{
savedVisualization,
loader,
editUrl,
indexPatterns,
editable,
@ -112,8 +130,12 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
},
parent
);
this.appState = appState;
this.savedVisualization = savedVisualization;
this.loader = loader;
this.vis = this.savedVisualization.vis;
this.vis.on('update', this.handleVisUpdate);
this.vis.on('reload', this.reload);
if (uiState) {
this.uiState = uiState;
@ -126,23 +148,33 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
this.uiState.on('change', this.uiStateChangeHandler);
}
this.vis._setUiState(this.uiState);
this.appState = appState;
this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => {
this.handleChanges();
});
this.subscriptions.push(
Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => {
this.handleChanges();
})
);
}
public getVisualizationDescription() {
return this.savedVisualization.description;
}
public getInspectorAdapters() {
public getInspectorAdapters = () => {
if (!this.handler) {
return undefined;
}
return this.handler.inspectorAdapters;
}
return this.handler.inspect();
};
public openInspector = () => {
if (this.handler) {
return this.handler.openInspector(this.getTitle() || '');
}
};
/**
* Transfers all changes in the containerState.customization into
@ -170,87 +202,148 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
}
public handleChanges() {
public async handleChanges() {
this.transferCustomizationsToUiState();
const updatedParams: VisualizeUpdateParams = {};
let dirty = false;
// Check if timerange has changed
if (!_.isEqual(this.input.timeRange, this.timeRange)) {
this.timeRange = _.cloneDeep(this.input.timeRange);
updatedParams.timeRange = this.timeRange;
dirty = true;
}
// Check if filters has changed
if (!onlyDisabledFiltersChanged(this.input.filters, this.filters)) {
updatedParams.filters = this.input.filters;
this.filters = this.input.filters;
dirty = true;
}
// Check if query has changed
if (!_.isEqual(this.input.query, this.query)) {
updatedParams.query = this.input.query;
this.query = this.input.query;
dirty = true;
}
if (this.output.title !== this.title) {
this.title = this.output.title;
updatedParams.dataAttrs = {
title: this.title || '',
};
if (this.domNode) {
this.domNode.setAttribute('data-title', this.title || '');
}
}
if (this.handler && !_.isEmpty(updatedParams)) {
this.handler.update(updatedParams);
this.handler.reload();
if (this.savedVisualization.description && this.domNode) {
this.domNode.setAttribute('data-description', this.savedVisualization.description);
}
if (this.handler && dirty) {
this.updateHandler();
}
}
/**
*
* @param {Element} domNode
* @param {ContainerState} containerState
*/
public render(domNode: HTMLElement) {
public async render(domNode: HTMLElement) {
this.timeRange = _.cloneDeep(this.input.timeRange);
this.query = this.input.query;
this.filters = this.input.filters;
this.transferCustomizationsToUiState();
const dataAttrs: { [key: string]: string } = {
'shared-item': '',
title: this.output.title || '',
this.savedVisualization.vis._setUiState(this.uiState);
this.uiState = this.savedVisualization.vis.getUiState();
// init default actions
forEach(this.vis.type.events, (event, eventName) => {
if (event.disabled || !eventName) {
return;
} else {
this.actions[eventName] = event.defaultAction;
}
});
// This is a hack to give maps visualizations access to data in the
// globalState, since they can no longer access it via searchSource.
// TODO: Remove this as a part of elastic/kibana#30593
this.vis.API.getGeohashBounds = () => {
return queryGeohashBounds(this.savedVisualization.vis, {
filters: this.filters,
query: this.query,
searchSource: this.savedVisualization.searchSource,
});
};
// this is a hack to make editor still work, will be removed once we clean up editor
this.vis.hasInspector = () => {
const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega'];
if (visTypesWithoutInspector.includes(this.vis.type.name)) {
return false;
}
return this.getInspectorAdapters();
};
this.vis.openInspector = this.openInspector;
const div = document.createElement('div');
div.className = `visualize panel-content panel-content--fullWidth`;
domNode.appendChild(div);
this.domNode = div;
this.handler = new expressions.ExpressionLoader(this.domNode);
this.subscriptions.push(
this.handler.events$.subscribe(async event => {
if (this.actions[event.name]) {
event.data.aggConfigs = getTableAggs(this.vis);
const filters: esFilters.Filter[] = this.actions[event.name](event.data) || [];
const mappedFilters = mapAndFlattenFilters(filters);
const timeFieldName = this.vis.indexPattern.timeFieldName;
npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, {
embeddable: this,
filters: mappedFilters,
timeFieldName,
});
}
})
);
div.setAttribute('data-title', this.output.title || '');
if (this.savedVisualization.description) {
dataAttrs.description = this.savedVisualization.description;
div.setAttribute('data-description', this.savedVisualization.description);
}
const handlerParams: VisualizeLoaderParams = {
appState: this.appState,
uiState: this.uiState,
// Append visualization to container instead of replacing its content
append: true,
timeRange: _.cloneDeep(this.input.timeRange),
query: this.query,
filters: this.filters,
cssClass: `panel-content panel-content--fullWidth`,
dataAttrs,
};
div.setAttribute('data-test-subj', 'visualizationLoader');
div.setAttribute('data-shared-item', '');
div.setAttribute('data-rendering-count', '0');
div.setAttribute('data-render-complete', 'false');
this.handler = this.loader.embedVisualizationWithSavedObject(
domNode,
this.savedVisualization,
handlerParams
this.subscriptions.push(
this.handler.loading$.subscribe(() => {
div.setAttribute('data-render-complete', 'false');
div.setAttribute('data-loading', '');
})
);
this.subscriptions.push(
this.handler.render$.subscribe(count => {
div.removeAttribute('data-loading');
div.setAttribute('data-render-complete', 'true');
div.setAttribute('data-rendering-count', count.toString());
dispatchRenderComplete(div);
})
);
this.updateHandler();
}
public destroy() {
super.destroy();
if (this.subscription) {
this.subscription.unsubscribe();
}
this.subscriptions.forEach(s => s.unsubscribe());
this.uiState.off('change', this.uiStateChangeHandler);
this.savedVisualization.vis.removeListener('reload', this.reload);
this.savedVisualization.vis.removeListener('update', this.handleVisUpdate);
this.savedVisualization.destroy();
if (this.handler) {
this.handler.destroy();
@ -258,12 +351,44 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
}
public reload() {
public reload = () => {
this.handleVisUpdate();
};
private async updateHandler() {
const expressionParams: IExpressionLoaderParams = {
searchContext: {
type: 'kibana_context',
timeRange: this.timeRange,
query: this.input.query,
filters: this.input.filters,
},
extraHandlers: {
vis: this.vis,
uiState: this.uiState,
},
};
this.expression = await buildPipeline(this.vis, {
searchSource: this.savedVisualization.searchSource,
timeRange: this.timeRange,
});
this.vis.filters = { timeRange: this.timeRange };
if (this.handler) {
this.handler.reload();
this.handler.update(this.expression, expressionParams);
}
}
private handleVisUpdate = async () => {
if (this.appState) {
this.appState.vis = this.savedVisualization.vis.getState();
this.appState.save();
}
this.updateHandler();
};
private uiStateChangeHandler = () => {
this.updateInput({
...this.uiState.toJSON(),

View file

@ -36,7 +36,6 @@ import {
EmbeddableFactory,
EmbeddableOutput,
ErrorEmbeddable,
getVisualizeLoader,
VisSavedObject,
} from '../kibana_services';
@ -131,7 +130,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
const visId = savedObject.id as string;
const editUrl = visId ? addBasePath(`/app/kibana${savedVisualizations.urlFor(visId)}`) : '';
const loader = await getVisualizeLoader();
const isLabsEnabled = config.get<boolean>('visualize:enableLabs');
if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') {
@ -143,7 +141,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
return new VisualizeEmbeddable(
{
savedVisualization: savedObject,
loader,
indexPatterns,
editUrl,
editable: this.isEditable(),

View file

@ -42,7 +42,7 @@ import { timefilter } from 'ui/timefilter';
// Saved objects
import { SavedObjectsClientProvider } from 'ui/saved_objects';
// @ts-ignore
import { SavedObjectProvider } from 'ui/saved_objects/saved_object';
import { SavedObject, SavedObjectProvider } from 'ui/saved_objects/saved_object';
import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry';
import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public';
@ -105,7 +105,6 @@ export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
export { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
export { getVisualizeLoader } from 'ui/visualize/loader';
export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
export {
Container,
@ -121,12 +120,8 @@ export { METRIC_TYPE };
export { StaticIndexPattern } from 'ui/index_patterns';
export { AppState } from 'ui/state_management/app_state';
export { VisType } from 'ui/vis';
export { VisualizeLoader } from 'ui/visualize/loader';
export {
VisSavedObject,
VisualizeLoaderParams,
VisualizeUpdateParams,
} from 'ui/visualize/loader/types';
// export const
export { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
export { VisSavedObject } from './embeddable/visualize_embeddable';

View file

@ -79,6 +79,7 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => {
return;
}
if (precisionChange) {
updateGeohashAgg();
this.vis.updateState();
} else {
//when we filter queries by collar

View file

@ -75,13 +75,13 @@ class VisEditorVisualizationUI extends Component {
this._handler = await embeddables.getEmbeddableFactory('visualization').createFromObject(savedObj, {
vis: {},
timeRange: timeRange,
filters: appState.filters || [],
filters: appState ? appState.filters || [] : [],
});
this._handler.render(this._visEl.current);
await this._handler.render(this._visEl.current);
this._subscription = this._handler.handler.data$.subscribe(data => {
this.setPanelInterval(data.visData);
onDataChange(data);
this.setPanelInterval(data.value.visData);
onDataChange(data.value);
});
}

View file

@ -11,11 +11,6 @@
<div
class="visEditor__canvas"
data-shared-item
data-shared-items-container
render-complete
data-title="{{vis.title}}"
data-description="{{vis.description}}"
/>
</div>

View file

@ -57,46 +57,10 @@ describe('<VisualizationChart/>', () => {
expect(wrapper.text()).toBe('Test Visualization visualization, not yet accessible');
});
it('should emit render start and render end', async () => {
const renderStart = jest.fn();
const renderComplete = jest.fn();
const domNode = document.createElement('div');
domNode.addEventListener('renderStart', renderStart);
domNode.addEventListener('renderComplete', renderComplete);
mount(<VisualizationChart vis={vis} />, {
attachTo: domNode
});
jest.runAllTimers();
await renderPromise;
expect(renderStart).toHaveBeenCalledTimes(1);
expect(renderComplete).toHaveBeenCalledTimes(1);
});
it('should render visualization', async () => {
const wrapper = mount(<VisualizationChart vis={vis} />);
jest.runAllTimers();
await renderPromise;
expect(wrapper.find('.visChart').text()).toMatch(/markdown/);
});
it('should re-render on param change', async () => {
const renderComplete = jest.fn();
const wrapper = mount(<VisualizationChart vis={vis} />);
const domNode = wrapper.getDOMNode();
domNode.addEventListener('renderComplete', renderComplete);
jest.runAllTimers();
await renderPromise;
expect(renderComplete).toHaveBeenCalledTimes(1);
vis.params.markdown = 'new text';
wrapper.setProps({ vis });
jest.runAllTimers();
await renderPromise;
expect(wrapper.find('.visChart').text()).toBe('new text');
expect(renderComplete).toHaveBeenCalledTimes(2);
});
});

View file

@ -19,13 +19,9 @@
import React from 'react';
import * as Rx from 'rxjs';
import { debounceTime, filter, share, switchMap, tap } from 'rxjs/operators';
import { debounceTime, filter, share, switchMap } from 'rxjs/operators';
import { PersistedState } from '../../persisted_state';
import {
dispatchRenderComplete,
dispatchRenderStart,
} from '../../../../../plugins/kibana_utils/public';
import { ResizeChecker } from '../../resize_checker';
import { Vis, VisualizationController } from '../../vis';
import { getUpdateStatus } from '../../vis/update_status';
@ -59,11 +55,6 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
const render$ = this.renderSubject.asObservable().pipe(share());
const success$ = render$.pipe(
tap(() => {
if (this.chartDiv.current) {
dispatchRenderStart(this.chartDiv.current);
}
}),
filter(
({ vis, visData, container }) => vis && container && (!vis.type.requiresSearch || visData)
),
@ -85,8 +76,8 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
const requestError$ = render$.pipe(filter(({ vis }) => vis.requestError));
this.renderSubscription = Rx.merge(success$, requestError$).subscribe(() => {
if (this.chartDiv.current !== null) {
dispatchRenderComplete(this.chartDiv.current);
if (this.props.onInit) {
this.props.onInit();
}
});
}
@ -111,19 +102,11 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
throw new Error('chartDiv and currentDiv reference should always be present.');
}
const { vis, onInit } = this.props;
const { vis } = this.props;
const Visualization = vis.type.visualization;
this.visualization = new Visualization(this.chartDiv.current, vis);
if (onInit) {
// In case the visualization implementation has an isLoaded function, we
// call that and wait for the result to resolve (in case it was a promise).
const visLoaded =
this.visualization && this.visualization.isLoaded && this.visualization.isLoaded();
Promise.resolve(visLoaded).then(onInit);
}
// We know that containerDiv.current will never be null, since we will always
// have rendered and the div is always rendered into the tree (i.e. not
// inside any condition).

View file

@ -1,20 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './loader';

View file

@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmbeddedVisualizeHandler data$ observable can be used to get response data in the correct format 1`] = `
Object {
"params": Object {},
"visConfig": Object {},
"visData": Object {},
"visType": "histogram",
}
`;
exports[`EmbeddedVisualizeHandler update should add provided data- attributes to the html element 1`] = `
<div
data-foo="bar"
data-loading=""
data-render-complete="false"
data-rendering-count="0"
data-shared-item=""
/>
`;
exports[`EmbeddedVisualizeHandler update should remove null data- attributes from the html element 1`] = `
<div
data-baz="qux"
data-loading=""
data-render-complete="false"
data-rendering-count="0"
data-shared-item=""
/>
`;

View file

@ -1,56 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { setupAndTeardownInjectorStub } from 'test_utils/stub_get_active_injector';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from '../../../vis';
import { visualizationLoader } from '../visualization_loader';
describe('visualization loader', () => {
let vis;
beforeEach(ngMock.module('kibana', 'kibana/directive'));
beforeEach(ngMock.inject((_$rootScope_, savedVisualizations, Private) => {
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
// Create a new Vis object
const Vis = Private(VisProvider);
vis = new Vis(indexPattern, {
type: 'markdown',
params: { markdown: 'this is test' },
});
}));
setupAndTeardownInjectorStub();
it('should render visualization', async () => {
const element = document.createElement('div');
expect(visualizationLoader.render).to.be.a('function');
visualizationLoader.render(element, vis, null, vis.params);
expect($(element).find('.visualization').length).to.be(1);
});
});

View file

@ -1,478 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { cloneDeep } from 'lodash';
import { setupAndTeardownInjectorStub } from 'test_utils/stub_get_active_injector';
import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from '../../../vis';
import { getVisualizeLoader } from '../visualize_loader';
import { EmbeddedVisualizeHandler } from '../embedded_visualize_handler';
import { Inspector } from '../../../inspector/inspector';
import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public';
import { PipelineDataLoader } from '../pipeline_data_loader';
import { PersistedState } from '../../../persisted_state';
import { DataAdapter, RequestAdapter } from '../../../inspector/adapters';
describe('visualize loader', () => {
let DataLoader;
let searchSource;
let vis;
let $rootScope;
let loader;
let mockedSavedObject;
let sandbox;
function createSavedObject() {
return {
vis,
searchSource,
};
}
async function timeout(delay = 0) {
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}
function newContainer() {
return angular.element('<div></div>');
}
function embedWithParams(params) {
const container = newContainer();
loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), params);
$rootScope.$digest();
return container.find('[data-test-subj="visualizationLoader"]');
}
beforeEach(ngMock.module('kibana', 'kibana/directive'));
beforeEach(ngMock.inject((_$rootScope_, savedVisualizations, Private) => {
$rootScope = _$rootScope_;
searchSource = Private(FixturesStubbedSearchSourceProvider);
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
DataLoader = PipelineDataLoader;
// Create a new Vis object
const Vis = Private(VisProvider);
vis = new Vis(indexPattern, {
type: 'pie',
title: 'testVis',
params: {},
aggs: [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 }
]
}
}
]
});
vis.type.requestHandler = 'courier';
vis.type.responseHandler = 'none';
vis.type.requiresSearch = false;
// Setup savedObject
mockedSavedObject = createSavedObject();
sandbox = sinon.sandbox.create();
// Mock savedVisualizations.get to return 'mockedSavedObject' when id is 'exists'
sandbox.stub(savedVisualizations, 'get').callsFake((id) =>
id === 'exists' ? Promise.resolve(mockedSavedObject) : Promise.reject()
);
}));
setupAndTeardownInjectorStub();
beforeEach(async () => {
loader = await getVisualizeLoader();
});
afterEach(() => {
if (sandbox) {
sandbox.restore();
}
});
describe('getVisualizeLoader', () => {
it('should return a promise', () => {
expect(getVisualizeLoader().then).to.be.a('function');
});
it('should resolve to an object', async () => {
const visualizeLoader = await getVisualizeLoader();
expect(visualizeLoader).to.be.an('object');
});
});
describe('service', () => {
describe('getVisualizationList', () => {
it('should be a function', async () => {
expect(loader.getVisualizationList).to.be.a('function');
});
});
describe('embedVisualizationWithSavedObject', () => {
it('should be a function', () => {
expect(loader.embedVisualizationWithSavedObject).to.be.a('function');
});
it('should render the visualize element', () => {
const container = newContainer();
loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1);
});
it('should not mutate vis.params', () => {
const container = newContainer();
const savedObject = createSavedObject();
const paramsBefore = cloneDeep(vis.params);
loader.embedVisualizationWithSavedObject(container[0], savedObject, {});
const paramsAfter = cloneDeep(vis.params);
expect(paramsBefore).to.eql(paramsAfter);
});
it('should replace content of container by default', () => {
const container = angular.element('<div><div id="prevContent"></div></div>');
loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
expect(container.find('#prevContent').length).to.be(0);
});
it('should append content to container when using append parameter', () => {
const container = angular.element('<div><div id="prevContent"></div></div>');
loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {
append: true
});
expect(container.children().length).to.be(2);
expect(container.find('#prevContent').length).to.be(1);
});
it('should apply css classes from parameters', () => {
const vis = embedWithParams({ cssClass: 'my-css-class another-class' });
expect(vis.hasClass('my-css-class')).to.be(true);
expect(vis.hasClass('another-class')).to.be(true);
});
it('should apply data attributes from dataAttrs parameter', () => {
const vis = embedWithParams({
dataAttrs: {
'foo': '',
'with-dash': 'value',
}
});
expect(vis.attr('data-foo')).to.be('');
expect(vis.attr('data-with-dash')).to.be('value');
});
});
describe('embedVisualizationWithId', () => {
it('should be a function', async () => {
expect(loader.embedVisualizationWithId).to.be.a('function');
});
it('should reject if the id was not found', () => {
const resolveSpy = sinon.spy();
const rejectSpy = sinon.spy();
const container = newContainer();
return loader.embedVisualizationWithId(container[0], 'not-existing', {})
.then(resolveSpy, rejectSpy)
.then(() => {
expect(resolveSpy.called).to.be(false);
expect(rejectSpy.calledOnce).to.be(true);
});
});
it('should render a visualize element, if id was found', async () => {
const container = newContainer();
await loader.embedVisualizationWithId(container[0], 'exists', {});
expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1);
});
});
describe('EmbeddedVisualizeHandler', () => {
it('should be returned from embedVisualizationWithId via a promise', async () => {
const handler = await loader.embedVisualizationWithId(newContainer()[0], 'exists', {});
expect(handler instanceof EmbeddedVisualizeHandler).to.be(true);
});
it('should be returned from embedVisualizationWithSavedObject', async () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
expect(handler instanceof EmbeddedVisualizeHandler).to.be(true);
});
it('should give access to the visualize element', () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
expect(handler.getElement()).to.be(container.find('[data-test-subj="visualizationLoader"]')[0]);
});
it('should allow opening the inspector of the visualization and return its session', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
sandbox.spy(Inspector, 'open');
const inspectorSession = handler.openInspector();
expect(Inspector.open.calledOnce).to.be(true);
expect(inspectorSession.close).to.be.a('function');
inspectorSession.close();
});
describe('inspector', () => {
describe('hasInspector()', () => {
it('should forward to inspectors hasInspector', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
sinon.spy(Inspector, 'isAvailable');
handler.hasInspector();
expect(Inspector.isAvailable.calledOnce).to.be(true);
const adapters = Inspector.isAvailable.lastCall.args[0];
expect(adapters.data).to.be.a(DataAdapter);
expect(adapters.requests).to.be.a(RequestAdapter);
});
it('should return hasInspectors result', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
const stub = sinon.stub(Inspector, 'isAvailable');
stub.returns(true);
expect(handler.hasInspector()).to.be(true);
stub.returns(false);
expect(handler.hasInspector()).to.be(false);
});
afterEach(() => {
Inspector.isAvailable.restore();
});
});
describe('openInspector()', () => {
beforeEach(() => {
sinon.stub(Inspector, 'open');
});
it('should call openInspector with all attached inspectors', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
handler.openInspector();
expect(Inspector.open.calledOnce).to.be(true);
const adapters = Inspector.open.lastCall.args[0];
expect(adapters).to.be(handler.inspectorAdapters);
});
it('should pass the vis title to the openInspector call', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
handler.openInspector();
expect(Inspector.open.calledOnce).to.be(true);
const params = Inspector.open.lastCall.args[1];
expect(params.title).to.be('testVis');
});
afterEach(() => {
Inspector.open.restore();
});
});
describe('inspectorAdapters', () => {
it('should register none for none requestHandler', () => {
const savedObj = createSavedObject();
savedObj.vis.type.requestHandler = 'none';
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {});
expect(handler.inspectorAdapters).to.eql({});
});
it('should attach data and request handler for courier', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
expect(handler.inspectorAdapters.data).to.be.a(DataAdapter);
expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter);
});
it('should allow enabling data adapter manually', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
expect(handler.inspectorAdapters.data).to.be.a(DataAdapter);
});
it('should allow enabling requests adapter manually', () => {
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {});
expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter);
});
it('should allow adding custom inspector adapters via the custom key', () => {
const Foodapter = class { };
const Bardapter = class { };
const savedObj = createSavedObject();
savedObj.vis.type.inspectorAdapters = {
custom: { foo: Foodapter, bar: Bardapter }
};
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {});
expect(handler.inspectorAdapters.foo).to.be.a(Foodapter);
expect(handler.inspectorAdapters.bar).to.be.a(Bardapter);
});
it('should not share adapter instances between vis instances', () => {
const Foodapter = class { };
const savedObj1 = createSavedObject();
const savedObj2 = createSavedObject();
savedObj1.vis.type.inspectorAdapters = { custom: { foo: Foodapter } };
savedObj2.vis.type.inspectorAdapters = { custom: { foo: Foodapter } };
const handler1 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj1, {});
const handler2 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj2, {});
expect(handler1.inspectorAdapters.foo).to.be.a(Foodapter);
expect(handler2.inspectorAdapters.foo).to.be.a(Foodapter);
expect(handler1.inspectorAdapters.foo).not.to.be(handler2.inspectorAdapters.foo);
expect(handler1.inspectorAdapters.data).to.be.a(DataAdapter);
expect(handler2.inspectorAdapters.data).to.be.a(DataAdapter);
expect(handler1.inspectorAdapters.data).not.to.be(handler2.inspectorAdapters.data);
});
});
});
it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
const spy = sinon.spy();
handler.whenFirstRenderComplete().then(spy);
expect(spy.notCalled).to.be(true);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
await timeout();
expect(spy.calledOnce).to.be(true);
});
it('should add listeners via addRenderCompleteListener that triggers on renderComplete events', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
await timeout();
expect(spy.calledOnce).to.be(true);
});
it('should call render complete listeners once per renderComplete event', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
expect(spy.callCount).to.be(3);
});
it('should successfully remove listeners from render complete', async () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {});
const spy = sinon.spy();
handler.addRenderCompleteListener(spy);
expect(spy.notCalled).to.be(true);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
expect(spy.calledOnce).to.be(true);
spy.resetHistory();
handler.removeRenderCompleteListener(spy);
dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]);
expect(spy.notCalled).to.be(true);
});
it('should allow updating and deleting data attributes', () => {
const container = newContainer();
const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {
dataAttrs: {
foo: 42
}
});
expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-foo')).to.be('42');
handler.update({
dataAttrs: {
foo: null,
added: 'value',
}
});
expect(container.find('[data-test-subj="visualizationLoader"]')[0].hasAttribute('data-foo')).to.be(false);
expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-added')).to.be('value');
});
it('should allow updating the time range of the visualization', async () => {
const spy = sandbox.spy(DataLoader.prototype, 'fetch');
const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {
timeRange: { from: 'now-7d', to: 'now' }
});
// Wait for the initial fetch and render to happen
await timeout(150);
spy.resetHistory();
handler.update({
timeRange: { from: 'now-10d/d', to: 'now' }
});
// Wait for fetch debounce to happen (as soon as we use lodash 4+ we could use fake timers here for the debounce)
await timeout(150);
sinon.assert.calledOnce(spy);
sinon.assert.calledWith(spy, sinon.match({ timeRange: { from: 'now-10d/d', to: 'now' } }));
});
it('should not set forceFetch on uiState change', async () => {
const spy = sandbox.spy(DataLoader.prototype, 'fetch');
const uiState = new PersistedState();
loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {
timeRange: { from: 'now-7d', to: 'now' },
uiState: uiState,
});
// Wait for the initial fetch and render to happen
await timeout(150);
spy.resetHistory();
uiState.set('property', 'value');
// Wait for fetch debounce to happen (as soon as we use lodash 4+ we could use fake timers here for the debounce)
await timeout(150);
sinon.assert.calledOnce(spy);
sinon.assert.calledWith(spy, sinon.match({ forceFetch: false }));
});
});
});
});

View file

@ -1,73 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.useFakeTimers();
import { Subject } from 'rxjs';
jest.mock('ui/notify', () => ({
toastNotifications: jest.fn(),
}));
jest.mock('./utils', () => ({
queryGeohashBounds: jest.fn(),
}));
jest.mock('./pipeline_helpers/utilities', () => ({
getFormat: jest.fn(),
getTableAggs: jest.fn(),
}));
const autoRefreshFetchSub = new Subject();
export const timefilter = {
_triggerAutoRefresh: () => {
autoRefreshFetchSub.next();
},
getAutoRefreshFetch$: () => {
return autoRefreshFetchSub.asObservable();
},
};
jest.doMock('../../timefilter', () => ({ timefilter }));
jest.mock('../../inspector', () => ({
Inspector: {
open: jest.fn(),
isAvailable: jest.fn(),
},
}));
export const mockDataLoaderFetch = jest.fn().mockReturnValue({
as: 'visualization',
value: {
visType: 'histogram',
visData: {},
visConfig: {},
params: {},
},
});
const MockDataLoader = class {
public async fetch(data: any) {
return await mockDataLoaderFetch(data);
}
};
jest.mock('./pipeline_data_loader', () => ({
PipelineDataLoader: MockDataLoader,
}));

View file

@ -1,299 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('ui/new_platform');
import { searchSourceMock } from '../../courier/search_source/mocks';
import { mockDataLoaderFetch, timefilter } from './embedded_visualize_handler.test.mocks';
import _ from 'lodash';
// @ts-ignore
import MockState from '../../../../../fixtures/mock_state';
import { Vis } from '../../vis';
import { VisResponseData } from './types';
import { Inspector } from '../../inspector';
import { EmbeddedVisualizeHandler, RequestHandlerParams } from './embedded_visualize_handler';
import { AggConfigs } from 'ui/agg_types/agg_configs';
jest.mock('plugins/interpreter/interpreter', () => ({
getInterpreter: () => {
return Promise.resolve();
},
}));
jest.mock('../../../../core_plugins/interpreter/public/registries', () => ({
registries: {
renderers: {
get: (name: string) => {
return {
render: async () => {
return {};
},
};
},
},
},
}));
describe('EmbeddedVisualizeHandler', () => {
let handler: any;
let div: HTMLElement;
let dataLoaderParams: RequestHandlerParams;
const mockVis: Vis = {
title: 'My Vis',
// @ts-ignore
type: 'foo',
getAggConfig: () => [],
_setUiState: () => ({}),
getUiState: () => new MockState(),
on: () => ({}),
off: () => ({}),
removeListener: jest.fn(),
API: {},
};
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(_, 'debounce').mockImplementation(
// @ts-ignore
(f: Function) => {
// @ts-ignore
f.cancel = () => {};
return f;
}
);
dataLoaderParams = {
aggs: ([] as any) as AggConfigs,
filters: undefined,
forceFetch: false,
inspectorAdapters: {},
query: undefined,
queryFilter: null,
searchSource: searchSourceMock,
timeRange: undefined,
uiState: undefined,
};
div = document.createElement('div');
handler = new EmbeddedVisualizeHandler(
div,
{
vis: mockVis,
title: 'My Vis',
searchSource: searchSourceMock,
destroy: () => ({}),
copyOnSave: false,
save: () => Promise.resolve('123'),
},
{
autoFetch: true,
Private: <T>(provider: () => T) => provider(),
queryFilter: null,
}
);
});
afterEach(() => {
handler.destroy();
});
describe('autoFetch', () => {
it('should trigger a reload when autoFetch=true and auto refresh happens', () => {
const spy = jest.spyOn(handler, 'fetchAndRender');
timefilter._triggerAutoRefresh();
jest.runAllTimers();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(true);
});
it('should not trigger a reload when autoFetch=false and auto refresh happens', () => {
handler = new EmbeddedVisualizeHandler(
div,
{
vis: mockVis,
title: 'My Vis',
searchSource: searchSourceMock,
destroy: () => ({}),
copyOnSave: false,
save: () => Promise.resolve('123'),
},
{
autoFetch: false,
Private: <T>(provider: () => T) => provider(),
queryFilter: null,
}
);
const spy = jest.spyOn(handler, 'fetchAndRender');
timefilter._triggerAutoRefresh();
jest.runAllTimers();
expect(spy).not.toHaveBeenCalled();
});
});
describe('getElement', () => {
it('should return the provided html element', () => {
expect(handler.getElement()).toBe(div);
});
});
describe('update', () => {
it('should add provided data- attributes to the html element', () => {
const spy = jest.spyOn(handler, 'fetchAndRender');
const params = {
dataAttrs: { foo: 'bar' },
};
handler.update(params);
expect(spy).not.toHaveBeenCalled();
expect(handler.getElement()).toMatchSnapshot();
});
it('should remove null data- attributes from the html element', () => {
const spy = jest.spyOn(handler, 'fetchAndRender');
handler.update({
dataAttrs: { foo: 'bar' },
});
const params = {
dataAttrs: {
foo: null,
baz: 'qux',
},
};
handler.update(params);
expect(spy).not.toHaveBeenCalled();
expect(handler.getElement()).toMatchSnapshot();
});
it('should call dataLoader.render with updated timeRange', () => {
const params = { timeRange: { foo: 'bar' } };
handler.update(params);
expect(mockDataLoaderFetch).toHaveBeenCalled();
const callIndex = mockDataLoaderFetch.mock.calls.length - 1;
const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0];
expect(abortSignal).toBeInstanceOf(AbortSignal);
expect(otherParams).toEqual({ ...dataLoaderParams, ...params });
});
it('should call dataLoader.render with updated filters', () => {
const params = { filters: [{ meta: { disabled: false } }] };
handler.update(params);
expect(mockDataLoaderFetch).toHaveBeenCalled();
const callIndex = mockDataLoaderFetch.mock.calls.length - 1;
const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0];
expect(abortSignal).toBeInstanceOf(AbortSignal);
expect(otherParams).toEqual({ ...dataLoaderParams, ...params });
});
it('should call dataLoader.render with updated query', () => {
const params = { query: { foo: 'bar' } };
handler.update(params);
expect(mockDataLoaderFetch).toHaveBeenCalled();
const callIndex = mockDataLoaderFetch.mock.calls.length - 1;
const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0];
expect(abortSignal).toBeInstanceOf(AbortSignal);
expect(otherParams).toEqual({ ...dataLoaderParams, ...params });
});
});
describe('destroy', () => {
it('should remove vis event listeners', () => {
const spy = jest.spyOn(mockVis, 'removeListener');
handler.destroy();
expect(spy).toHaveBeenCalledTimes(2);
expect(spy.mock.calls[0][0]).toBe('reload');
expect(spy.mock.calls[1][0]).toBe('update');
});
it('should remove element event listeners', () => {
const spy = jest.spyOn(handler.getElement(), 'removeEventListener');
handler.destroy();
expect(spy).toHaveBeenCalled();
});
it('should prevent subsequent renders', () => {
const spy = jest.spyOn(handler, 'fetchAndRender');
handler.destroy();
expect(spy).not.toHaveBeenCalled();
});
it('should cancel debounced fetchAndRender', () => {
const spy = jest.spyOn(handler.debouncedFetchAndRender, 'cancel');
handler.destroy();
expect(spy).toHaveBeenCalledTimes(1);
});
it('should call abort on controller', () => {
handler.abortController = new AbortController();
const spy = jest.spyOn(handler.abortController, 'abort');
handler.destroy();
expect(spy).toHaveBeenCalled();
});
});
describe('openInspector', () => {
it('calls Inspector.open()', () => {
handler.openInspector();
expect(Inspector.open).toHaveBeenCalledTimes(1);
expect(Inspector.open).toHaveBeenCalledWith({}, { title: 'My Vis' });
});
});
describe('hasInspector', () => {
it('calls Inspector.isAvailable()', () => {
handler.hasInspector();
expect(Inspector.isAvailable).toHaveBeenCalledTimes(1);
expect(Inspector.isAvailable).toHaveBeenCalledWith({});
});
});
describe('reload', () => {
it('should force fetch and render', () => {
const spy = jest.spyOn(handler, 'fetchAndRender');
handler.reload();
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(true);
});
});
describe('data$', () => {
it('observable can be used to get response data in the correct format', async () => {
let response;
handler.data$.subscribe((data: VisResponseData) => (response = data));
await handler.fetch(true);
jest.runAllTimers();
expect(response).toMatchSnapshot();
});
});
describe('render', () => {
// TODO
});
describe('whenFirstRenderComplete', () => {
// TODO
});
describe('addRenderCompleteListener', () => {
// TODO
});
describe('removeRenderCompleteListener', () => {
// TODO
});
});

View file

@ -1,553 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EventEmitter } from 'events';
import { debounce, forEach, get, isEqual } from 'lodash';
import * as Rx from 'rxjs';
import { share } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
// @ts-ignore untyped dependency
import { AggConfigs } from 'ui/agg_types/agg_configs';
import { SearchSource } from 'ui/courier';
import { QueryFilter } from 'ui/filter_manager/query_filter';
import { TimeRange, onlyDisabledFiltersChanged } from '../../../../../plugins/data/public';
import { registries } from '../../../../core_plugins/interpreter/public/registries';
import { Inspector } from '../../inspector';
import { Adapters } from '../../inspector/types';
import { PersistedState } from '../../persisted_state';
import { IPrivate } from '../../private';
import { RenderCompleteHelper } from '../../../../../plugins/kibana_utils/public';
import { AppState } from '../../state_management/app_state';
import { timefilter } from '../../timefilter';
import { Vis } from '../../vis';
// @ts-ignore untyped dependency
import { VisFiltersProvider } from '../../vis/vis_filters';
import { PipelineDataLoader } from './pipeline_data_loader';
import { visualizationLoader } from './visualization_loader';
import { Query } from '../../../../core_plugins/data/public';
import { esFilters } from '../../../../../plugins/data/public';
import { DataAdapter, RequestAdapter } from '../../inspector/adapters';
import { getTableAggs } from './pipeline_helpers/utilities';
import {
VisResponseData,
VisSavedObject,
VisualizeLoaderParams,
VisualizeUpdateParams,
} from './types';
import { queryGeohashBounds } from './utils';
interface EmbeddedVisualizeHandlerParams extends VisualizeLoaderParams {
Private: IPrivate;
queryFilter: any;
autoFetch?: boolean;
}
export interface RequestHandlerParams {
searchSource: SearchSource;
aggs: AggConfigs;
timeRange?: TimeRange;
query?: Query;
filters?: esFilters.Filter[];
forceFetch: boolean;
queryFilter: QueryFilter;
uiState?: PersistedState;
partialRows?: boolean;
inspectorAdapters: Adapters;
metricsAtAllLevels?: boolean;
visParams?: any;
abortSignal?: AbortSignal;
}
const RENDER_COMPLETE_EVENT = 'render_complete';
const DATA_SHARED_ITEM = 'data-shared-item';
const LOADING_ATTRIBUTE = 'data-loading';
const RENDERING_COUNT_ATTRIBUTE = 'data-rendering-count';
/**
* A handler to the embedded visualization. It offers several methods to interact
* with the visualization.
*/
export class EmbeddedVisualizeHandler {
/**
* This observable will emit every time new data is loaded for the
* visualization. The emitted value is the loaded data after it has
* been transformed by the visualization's response handler.
* This should not be used by any plugin.
* @ignore
*/
public readonly data$: Rx.Observable<any>;
public readonly inspectorAdapters: Adapters = {};
private vis: Vis;
private handlers: any;
private loaded: boolean = false;
private destroyed: boolean = false;
private listeners = new EventEmitter();
private firstRenderComplete: Promise<void>;
private renderCompleteHelper: RenderCompleteHelper;
private shouldForceNextFetch: boolean = false;
private debouncedFetchAndRender = debounce(() => {
if (this.destroyed) {
return;
}
const forceFetch = this.shouldForceNextFetch;
this.shouldForceNextFetch = false;
this.fetch(forceFetch).then(this.render);
}, 100);
private dataLoaderParams: RequestHandlerParams;
private readonly appState?: AppState;
private uiState: PersistedState;
private dataLoader: PipelineDataLoader;
private dataSubject: Rx.Subject<any>;
private actions: any = {};
private events$: Rx.Observable<any>;
private autoFetch: boolean;
private abortController?: AbortController;
private autoRefreshFetchSubscription: Rx.Subscription | undefined;
constructor(
private readonly element: HTMLElement,
savedObject: VisSavedObject,
params: EmbeddedVisualizeHandlerParams
) {
const { searchSource, vis } = savedObject;
const {
appState,
uiState,
queryFilter,
timeRange,
filters,
query,
autoFetch = true,
Private,
} = params;
this.dataLoaderParams = {
searchSource,
timeRange,
query,
queryFilter,
filters,
uiState,
aggs: vis.getAggConfig(),
forceFetch: false,
inspectorAdapters: this.inspectorAdapters,
};
// Listen to the first RENDER_COMPLETE_EVENT to resolve this promise
this.firstRenderComplete = new Promise(resolve => {
this.listeners.once(RENDER_COMPLETE_EVENT, resolve);
});
element.setAttribute(LOADING_ATTRIBUTE, '');
element.setAttribute(DATA_SHARED_ITEM, '');
element.setAttribute(RENDERING_COUNT_ATTRIBUTE, '0');
element.addEventListener('renderComplete', this.onRenderCompleteListener);
this.autoFetch = autoFetch;
this.appState = appState;
this.vis = vis;
if (uiState) {
vis._setUiState(uiState);
}
this.uiState = this.vis.getUiState();
this.handlers = {
vis: this.vis,
uiState: this.uiState,
onDestroy: (fn: () => never) => (this.handlers.destroyFn = fn),
};
this.vis.on('update', this.handleVisUpdate);
this.vis.on('reload', this.reload);
this.uiState.on('change', this.onUiStateChange);
if (autoFetch) {
this.autoRefreshFetchSubscription = timefilter.getAutoRefreshFetch$().subscribe(this.reload);
}
// This is a hack to give maps visualizations access to data in the
// globalState, since they can no longer access it via searchSource.
// TODO: Remove this as a part of elastic/kibana#30593
this.vis.API.getGeohashBounds = () => {
return queryGeohashBounds(this.vis, {
filters: this.dataLoaderParams.filters,
query: this.dataLoaderParams.query,
});
};
this.dataLoader = new PipelineDataLoader(vis);
const visFilters: any = Private(VisFiltersProvider);
this.renderCompleteHelper = new RenderCompleteHelper(element);
this.inspectorAdapters = this.getActiveInspectorAdapters();
this.vis.openInspector = this.openInspector;
this.vis.hasInspector = this.hasInspector;
// init default actions
forEach(this.vis.type.events, (event, eventName) => {
if (event.disabled || !eventName) {
return;
} else {
this.actions[eventName] = event.defaultAction;
}
});
this.handlers.eventsSubject = new Rx.Subject();
this.vis.eventsSubject = this.handlers.eventsSubject;
this.events$ = this.handlers.eventsSubject.asObservable().pipe(share());
this.events$.subscribe(event => {
if (this.actions[event.name]) {
event.data.aggConfigs = getTableAggs(this.vis);
const newFilters = this.actions[event.name](event.data) || [];
if (event.name === 'brush') {
const fieldName = newFilters[0].meta.key;
const $state = this.vis.API.getAppState();
const existingFilter = $state.filters.find(
(filter: any) => filter.meta && filter.meta.key === fieldName
);
if (existingFilter) {
Object.assign(existingFilter, newFilters[0]);
}
}
visFilters.pushFilters(newFilters);
}
});
this.dataSubject = new Rx.Subject();
this.data$ = this.dataSubject.asObservable().pipe(share());
this.render();
}
/**
* Update properties of the embedded visualization. This method does not allow
* updating all initial parameters, but only a subset of the ones allowed
* in {@link VisualizeUpdateParams}.
*
* @param params The parameters that should be updated.
*/
public update(params: VisualizeUpdateParams = {}) {
// Apply data- attributes to the element if specified
const dataAttrs = params.dataAttrs;
if (dataAttrs) {
Object.keys(dataAttrs).forEach(key => {
if (dataAttrs[key] === null) {
this.element.removeAttribute(`data-${key}`);
return;
}
this.element.setAttribute(`data-${key}`, dataAttrs[key]);
});
}
let fetchRequired = false;
if (
params.hasOwnProperty('timeRange') &&
!isEqual(this.dataLoaderParams.timeRange, params.timeRange)
) {
fetchRequired = true;
this.dataLoaderParams.timeRange = params.timeRange;
}
if (
params.hasOwnProperty('filters') &&
!onlyDisabledFiltersChanged(this.dataLoaderParams.filters, params.filters)
) {
fetchRequired = true;
this.dataLoaderParams.filters = params.filters;
}
if (params.hasOwnProperty('query') && !isEqual(this.dataLoaderParams.query, params.query)) {
fetchRequired = true;
this.dataLoaderParams.query = params.query;
}
if (fetchRequired) {
this.fetchAndRender();
}
}
/**
* Destroy the underlying Angular scope of the visualization. This should be
* called whenever you remove the visualization.
*/
public destroy(): void {
this.destroyed = true;
this.cancel();
this.debouncedFetchAndRender.cancel();
if (this.autoFetch) {
if (this.autoRefreshFetchSubscription) this.autoRefreshFetchSubscription.unsubscribe();
}
this.vis.removeListener('reload', this.reload);
this.vis.removeListener('update', this.handleVisUpdate);
this.element.removeEventListener('renderComplete', this.onRenderCompleteListener);
this.uiState.off('change', this.onUiStateChange);
visualizationLoader.destroy(this.element);
this.renderCompleteHelper.destroy();
if (this.handlers.destroyFn) {
this.handlers.destroyFn();
}
}
/**
* Return the actual DOM element (wrapped in jQuery) of the rendered visualization.
* This is especially useful if you used `append: true` in the parameters where
* the visualization will be appended to the specified container.
*/
public getElement(): HTMLElement {
return this.element;
}
/**
* renders visualization with provided data
* @param response: visualization data
*/
public render = (response: VisResponseData | null = null): void => {
const executeRenderer = this.rendererProvider(response);
if (!executeRenderer) {
return;
}
// TODO: we have this weird situation when we need to render first,
// and then we call fetch and render... we need to get rid of that.
executeRenderer().then(() => {
if (!this.loaded) {
this.loaded = true;
if (this.autoFetch) {
this.fetchAndRender();
}
}
});
};
/**
* Opens the inspector for the embedded visualization. This will return an
* handler to the inspector to close and interact with it.
* @return An inspector session to interact with the opened inspector.
*/
public openInspector = () => {
return Inspector.open(this.inspectorAdapters, {
title: this.vis.title,
});
};
public hasInspector = () => {
return Inspector.isAvailable(this.inspectorAdapters);
};
/**
* Returns a promise, that will resolve (without a value) once the first rendering of
* the visualization has finished. If you want to listen to consecutive rendering
* events, look into the `addRenderCompleteListener` method.
*
* @returns Promise, that resolves as soon as the visualization is done rendering
* for the first time.
*/
public whenFirstRenderComplete(): Promise<void> {
return this.firstRenderComplete;
}
/**
* Adds a listener to be called whenever the visualization finished rendering.
* This can be called multiple times, when the visualization rerenders, e.g. due
* to new data.
*
* @param {function} listener The listener to be notified about complete renders.
*/
public addRenderCompleteListener(listener: () => void) {
this.listeners.addListener(RENDER_COMPLETE_EVENT, listener);
}
/**
* Removes a previously registered render complete listener from this handler.
* This listener will no longer be called when the visualization finished rendering.
*
* @param {function} listener The listener to remove from this handler.
*/
public removeRenderCompleteListener(listener: () => void) {
this.listeners.removeListener(RENDER_COMPLETE_EVENT, listener);
}
/**
* Force the fetch of new data and renders the chart again.
*/
public reload = () => {
this.fetchAndRender(true);
};
private incrementRenderingCount = () => {
const renderingCount = Number(this.element.getAttribute(RENDERING_COUNT_ATTRIBUTE) || 0);
this.element.setAttribute(RENDERING_COUNT_ATTRIBUTE, `${renderingCount + 1}`);
};
private onRenderCompleteListener = () => {
this.listeners.emit(RENDER_COMPLETE_EVENT);
this.element.removeAttribute(LOADING_ATTRIBUTE);
this.incrementRenderingCount();
};
private onUiStateChange = () => {
this.fetchAndRender();
};
/**
* Returns an object of all inspectors for this vis object.
* This must only be called after this.type has properly be initialized,
* since we need to read out data from the the vis type to check which
* inspectors are available.
*/
private getActiveInspectorAdapters = (): Adapters => {
const adapters: Adapters = {};
const { inspectorAdapters: typeAdapters } = this.vis.type;
// Add the requests inspector adapters if the vis type explicitly requested it via
// inspectorAdapters.requests: true in its definition or if it's using the courier
// request handler, since that will automatically log its requests.
if ((typeAdapters && typeAdapters.requests) || this.vis.type.requestHandler === 'courier') {
adapters.requests = new RequestAdapter();
}
// Add the data inspector adapter if the vis type requested it or if the
// vis is using courier, since we know that courier supports logging
// its data.
if ((typeAdapters && typeAdapters.data) || this.vis.type.requestHandler === 'courier') {
adapters.data = new DataAdapter();
}
// Add all inspectors, that are explicitly registered with this vis type
if (typeAdapters && typeAdapters.custom) {
Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => {
adapters[key] = new (Adapter as any)();
});
}
return adapters;
};
/**
* Fetches new data and renders the chart. This will happen debounced for a couple
* of milliseconds, to bundle fast successive calls into one fetch and render,
* e.g. while resizing the window, this will be triggered constantly on the resize
* event.
*
* @param forceFetch=false Whether the request handler should be signaled to forceFetch
* (i.e. ignore caching in case it supports it). If at least one call to this
* passed `true` the debounced fetch and render will be a force fetch.
*/
private fetchAndRender = (forceFetch = false): void => {
this.shouldForceNextFetch = forceFetch || this.shouldForceNextFetch;
this.element.setAttribute(LOADING_ATTRIBUTE, '');
this.debouncedFetchAndRender();
};
private handleVisUpdate = () => {
if (this.appState) {
this.appState.vis = this.vis.getState();
this.appState.save();
}
this.fetchAndRender();
};
private cancel = () => {
if (this.abortController) this.abortController.abort();
};
private fetch = (forceFetch: boolean = false) => {
this.cancel();
this.abortController = new AbortController();
this.dataLoaderParams.abortSignal = this.abortController.signal;
this.dataLoaderParams.aggs = this.vis.getAggConfig();
this.dataLoaderParams.forceFetch = forceFetch;
this.dataLoaderParams.inspectorAdapters = this.inspectorAdapters;
this.vis.filters = { timeRange: this.dataLoaderParams.timeRange };
this.vis.requestError = undefined;
this.vis.showRequestError = false;
return (
this.dataLoader
// Don't pass in this.dataLoaderParams directly because it may be modified async in another
// call to fetch before the previous one has completed
.fetch({ ...this.dataLoaderParams })
.then(data => {
// Pipeline responses never throw errors, so we need to check for
// `type: 'error'`, and then throw so it can be caught below.
// TODO: We should revisit this after we have fully migrated
// to the new expression pipeline infrastructure.
if (data && data.type === 'error') {
throw data.error;
}
if (data && data.value) {
this.dataSubject.next(data.value);
}
return data;
})
.catch(this.handleDataLoaderError)
);
};
/**
* When dataLoader returns an error, we need to make sure it surfaces in the UI.
*
* TODO: Eventually we should add some custom error messages for issues that are
* frequently encountered by users.
*/
private handleDataLoaderError = (error: any): void => {
// If the data loader was aborted then no need to surface this error in the UI
if (error && error.name === 'AbortError') return;
// Cancel execution of pipeline expressions
if (this.abortController) {
this.abortController.abort();
}
this.vis.requestError = error;
this.vis.showRequestError =
error.type && ['NO_OP_SEARCH_STRATEGY', 'UNSUPPORTED_QUERY'].includes(error.type);
toastNotifications.addDanger({
title: i18n.translate('common.ui.visualize.dataLoaderError', {
defaultMessage: 'Error in visualization',
}),
text: error.message,
});
};
private rendererProvider = (response: VisResponseData | null) => {
const renderer = registries.renderers.get(get(response || {}, 'as', 'visualization'));
if (!renderer) {
return null;
}
return () =>
renderer.render(
this.element,
get(response, 'value', { visType: this.vis.type.name }),
this.handlers
);
};
}

View file

@ -1,20 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './visualize_loader';

View file

@ -1,47 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Vis } from '../../vis';
import { buildPipeline, runPipeline } from './pipeline_helpers';
import { RequestHandlerParams } from './embedded_visualize_handler';
export class PipelineDataLoader {
constructor(private readonly vis: Vis) {}
public async fetch(params: RequestHandlerParams): Promise<any> {
this.vis.pipelineExpression = await buildPipeline(this.vis, params);
return runPipeline(
this.vis.pipelineExpression,
{ type: 'null' },
{
getInitialContext: () => ({
type: 'kibana_context',
query: params.query,
timeRange: params.timeRange,
filters: params.filters
? params.filters.filter(filter => !filter.meta.disabled)
: undefined,
}),
inspectorAdapters: params.inspectorAdapters,
abortSignal: params.abortSignal,
}
);
}
}

View file

@ -18,4 +18,3 @@
*/
export { buildPipeline } from './build_pipeline';
export { runPipeline } from './run_pipeline';

View file

@ -1,43 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import { fromExpression } from '@kbn/interpreter/common';
import { Adapters } from 'ui/inspector';
import { getInterpreter } from '../../../../../core_plugins/interpreter/public/interpreter';
import { KibanaContext } from '../../../../../core_plugins/interpreter/public';
type getInitialContextFunction = () => KibanaContext;
export interface RunPipelineHandlers {
getInitialContext: getInitialContextFunction;
inspectorAdapters?: Adapters;
abortSignal?: AbortSignal;
}
export const runPipeline = async (
expression: string,
context: any,
handlers: RunPipelineHandlers
) => {
const ast = fromExpression(expression);
const { interpreter } = await getInterpreter();
const pipelineResponse = await interpreter.interpretAst(ast, context, handlers as any);
return pipelineResponse;
};

View file

@ -1,104 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { TimeRange } from 'src/plugins/data/public';
import { Query } from 'src/legacy/core_plugins/data/public';
import { SavedObject } from 'ui/saved_objects/saved_object';
import { VisResponseValue } from 'src/plugins/visualizations/public';
import { SearchSource } from '../../courier';
import { PersistedState } from '../../persisted_state';
import { AppState } from '../../state_management/app_state';
import { Vis } from '../../vis';
import { esFilters } from '../../../../../plugins/data/public';
export interface VisSavedObject extends SavedObject {
vis: Vis;
description?: string;
searchSource: SearchSource;
title: string;
uiStateJSON?: string;
destroy: () => void;
}
export interface VisResponseData {
as: string;
value: VisResponseValue;
}
/**
* The parameters accepted by the embedVisualize calls.
*/
export interface VisualizeLoaderParams {
/**
* An object with a from/to key, that must be either a date in ISO format, or a
* valid datetime Elasticsearch expression, e.g.: { from: 'now-7d/d', to: 'now' }
*/
timeRange?: TimeRange;
/**
* If set to true, the visualization will be appended to the passed element instead
* of replacing all its content. (default: false)
*/
append?: boolean;
/**
* If specified this CSS class (or classes with space separated) will be set to
* the root visualize element.
*/
cssClass?: string;
/**
* An object of key-value pairs, that will be set as data-{key}="{value}" attributes
* on the visualization element.
*/
dataAttrs?: { [key: string]: string };
/**
* Specifies the filters that should be applied to that visualization.
*/
filters?: esFilters.Filter[];
/**
* The query that should apply to that visualization.
*/
query?: Query;
/**
* The current uiState of the application. If you don't pass a uiState, the
* visualization will creates it's own uiState to store information like whether
* the legend is open or closed, but you don't have access to it from the outside.
* Pass one in if you need that access, e.g. for saving that state.
*/
uiState?: PersistedState;
/**
* The appState this visualization should use. If you don't specify it, the
* global AppState (that is decoded in the URL) will be used. Usually you don't
* need to overwrite this, unless you don't want the visualization to use the
* global AppState.
*/
appState?: AppState;
/**
* Whether or not the visualization should fetch its data automatically. If this is
* set to `false` the loader won't trigger a fetch on embedding or when an auto refresh
* cycle happens. Default value: `true`
*/
autoFetch?: boolean;
}
/**
* The subset of properties allowed to update on an already embedded visualization.
*/
export type VisualizeUpdateParams = Pick<
VisualizeLoaderParams,
'timeRange' | 'dataAttrs' | 'filters' | 'query'
>;

View file

@ -25,11 +25,13 @@ import { AggConfig } from 'ui/vis';
import { Query } from 'src/legacy/core_plugins/data/public';
import { timefilter } from 'ui/timefilter';
import { Vis } from '../../../vis';
import { SearchSource } from '../../../courier';
import { esFilters } from '../../../../../../plugins/data/public';
interface QueryGeohashBoundsParams {
filters?: esFilters.Filter[];
query?: Query;
searchSource?: SearchSource;
}
/**
@ -47,7 +49,9 @@ export async function queryGeohashBounds(vis: Vis, params: QueryGeohashBoundsPar
});
if (agg) {
const searchSource = vis.searchSource.createChild();
const searchSource = params.searchSource
? params.searchSource.createChild()
: new SearchSource();
searchSource.setField('size', 0);
searchSource.setField('aggs', () => {
const geoBoundsAgg = vis.getAggConfig().createAggConfig(

View file

@ -33,8 +33,7 @@ import { PersistedState } from '../../persisted_state';
import { start as visualizations } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy';
export function VisProvider(indexPatterns, getAppState) {
export function VisProvider(getAppState) {
const visTypes = visualizations.types;
class Vis extends EventEmitter {

View file

@ -1,65 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { PersistedState } from '../../persisted_state';
import { Vis } from '../../vis';
import { Visualization } from '../components/visualization';
interface VisualizationLoaderParams {
listenOnChange?: boolean;
}
function renderVisualization(
element: HTMLElement,
vis: Vis,
visData: any,
visParams: any,
uiState: PersistedState,
params: VisualizationLoaderParams
) {
return new Promise(resolve => {
const listenOnChange = _.get(params, 'listenOnChange', false);
render(
<Visualization
vis={vis}
visData={visData}
visParams={visParams}
uiState={uiState}
listenOnChange={listenOnChange}
onInit={resolve}
/>,
element
);
});
}
function destroy(element?: HTMLElement) {
if (element) {
unmountComponentAtNode(element);
}
}
export const visualizationLoader = {
render: renderVisualization,
destroy,
};

View file

@ -1,159 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* IMPORTANT: If you make changes to this API, please make sure to check that
* the docs (docs/development/visualize/development-create-visualization.asciidoc)
* are up to date.
*/
import chrome from '../../chrome';
import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter';
import { IPrivate } from '../../private';
import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
import { VisSavedObject, VisualizeLoaderParams } from './types';
export class VisualizeLoader {
constructor(private readonly savedVisualizations: any, private readonly Private: IPrivate) {}
/**
* Renders a saved visualization specified by its id into a DOM element.
*
* @param element The DOM element to render the visualization into.
* You can alternatively pass a jQuery element instead.
* @param id The id of the saved visualization. This is the id of the
* saved object that is stored in the .kibana index.
* @param params A list of parameters that will influence rendering.
*
* @return A promise that resolves to the
* handler for this visualization as soon as the saved object could be found.
*/
public async embedVisualizationWithId(
element: HTMLElement,
savedVisualizationId: string,
params: VisualizeLoaderParams
) {
return new Promise((resolve, reject) => {
this.savedVisualizations.get(savedVisualizationId).then((savedObj: VisSavedObject) => {
const handler = this.renderVis(element, savedObj, params);
resolve(handler);
}, reject);
});
}
/**
* Renders a saved visualization specified by its savedObject into a DOM element.
* In most of the cases you will need this method, since it allows you to specify
* filters, handlers, queries, etc. on the savedObject before rendering.
*
* We do not encourage you to use this method, since it will most likely be changed
* or removed in a future version of Kibana. Rather embed a visualization by its id
* via the {@link #embedVisualizationWithId} method.
*
* @deprecated You should rather embed by id, since this method will be removed in the future.
* @param element The DOM element to render the visualization into.
* You can alternatively pass a jQuery element instead.
* @param savedObj The savedObject as it could be retrieved by the
* `savedVisualizations` service.
* @param params A list of parameters that will influence rendering.
*
* @return The handler to the visualization.
*/
public embedVisualizationWithSavedObject(
el: HTMLElement,
savedObj: VisSavedObject,
params: VisualizeLoaderParams
) {
return this.renderVis(el, savedObj, params);
}
/**
* Returns a promise, that resolves to a list of all saved visualizations.
*
* @return Resolves with a list of all saved visualizations as
* returned by the `savedVisualizations` service in Kibana.
*/
public getVisualizationList(): Promise<any[]> {
return this.savedVisualizations.find().then((result: any) => result.hits);
}
private renderVis(
container: HTMLElement,
savedObj: VisSavedObject,
params: VisualizeLoaderParams
) {
const { vis, description, searchSource } = savedObj;
vis.description = description;
vis.searchSource = searchSource;
if (!params.append) {
container.innerHTML = '';
}
const element = document.createElement('div');
element.className = 'visualize';
element.setAttribute('data-test-subj', 'visualizationLoader');
container.appendChild(element);
// We need the container to have display: flex so visualization will render correctly
container.style.display = 'flex';
// If params specified cssClass, we will set this to the element.
if (params.cssClass) {
params.cssClass.split(' ').forEach(cssClass => {
element.classList.add(cssClass);
});
}
// Apply data- attributes to the element if specified
const dataAttrs = params.dataAttrs;
if (dataAttrs) {
Object.keys(dataAttrs).forEach(key => {
element.setAttribute(`data-${key}`, dataAttrs[key]);
});
}
const handlerParams = {
...params,
// lets add query filter angular service to the params
queryFilter: this.Private(FilterBarQueryFilterProvider),
// lets add Private to the params, we'll need to pass it to visualize later
Private: this.Private,
};
return new EmbeddedVisualizeHandler(element, savedObj, handlerParams);
}
}
function VisualizeLoaderProvider(savedVisualizations: any, Private: IPrivate) {
return new VisualizeLoader(savedVisualizations, Private);
}
/**
* Returns a promise, that resolves with the visualize loader, once it's ready.
* @return A promise, that resolves to the visualize loader.
*/
function getVisualizeLoader(): Promise<VisualizeLoader> {
return chrome.dangerouslyGetActiveInjector().then($injector => {
const Private: IPrivate = $injector.get('Private');
return Private(VisualizeLoaderProvider);
});
}
export { getVisualizeLoader, VisualizeLoaderProvider };

View file

@ -91,6 +91,7 @@ export interface IExpressionLoaderParams {
customFunctions?: [];
customRenderers?: [];
extraHandlers?: Record<string, any>;
inspectorAdapters?: Adapters;
}
export interface IInterpreterHandlers {

View file

@ -221,7 +221,7 @@ export default function ({ getService, getPageObjects }) {
it('when not checked does not add filters to aggregation', async () => {
await PageObjects.visualize.toggleOpenEditor(2);
await PageObjects.visualize.toggleIsFilteredByCollarCheckbox();
await PageObjects.visualize.setIsFilteredByCollarCheckbox(false);
await PageObjects.visualize.clickGo();
await inspector.open();
await inspector.expectTableHeaders(['geohash_grid', 'Count', 'Geo Centroid']);
@ -229,7 +229,7 @@ export default function ({ getService, getPageObjects }) {
});
after(async () => {
await PageObjects.visualize.toggleIsFilteredByCollarCheckbox();
await PageObjects.visualize.setIsFilteredByCollarCheckbox(true);
await PageObjects.visualize.clickGo();
});
});

View file

@ -1007,6 +1007,16 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
await testSubjects.click('isFilteredByCollarCheckbox');
}
async setIsFilteredByCollarCheckbox(value = true) {
await retry.try(async () => {
const isChecked = await this.isChecked('isFilteredByCollarCheckbox');
if (isChecked !== value) {
await testSubjects.click('isFilteredByCollarCheckbox');
throw new Error('isFilteredByCollar not set correctly');
}
});
}
async getMarkdownData() {
const markdown = await retry.try(async () => find.byCssSelector('visualize'));
return await markdown.getVisibleText();

View file

@ -24,9 +24,6 @@ import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters';
import { runPipeline } from 'ui/visualize/loader/pipeline_helpers';
import { visualizationLoader } from 'ui/visualize/loader/visualization_loader';
import { registries } from 'plugins/interpreter/registries';
// This is required so some default styles and required scripts/Angular modules are loaded,
@ -58,6 +55,17 @@ app.config(stateManagementConfigProvider =>
stateManagementConfigProvider.disable()
);
import { fromExpression } from '@kbn/interpreter/common';
import { getInterpreter } from '../../../../../src/legacy/core_plugins/interpreter/public/interpreter';
const runPipeline = async (expression, context, handlers) => {
const ast = fromExpression(expression);
const { interpreter } = await getInterpreter();
const pipelineResponse = await interpreter.interpretAst(ast, context, handlers);
return pipelineResponse;
};
function RootController($scope, $element) {
const domNode = $element[0];
@ -67,7 +75,6 @@ function RootController($scope, $element) {
DataAdapter={DataAdapter}
runPipeline={runPipeline}
registries={registries}
visualizationLoader={visualizationLoader}
/>, domNode);
// unmount react on controller destroy

View file

@ -64,7 +64,6 @@ class Main extends React.Component {
this.setState({ expression: 'Renderer was not found in registry!\n\n' + JSON.stringify(context) });
return resolve();
}
props.visualizationLoader.destroy(this.chartDiv);
const renderCompleteHandler = () => {
resolve('render complete');
this.chartDiv.removeEventListener('renderComplete', renderCompleteHandler);

View file

@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'header']);
describe('runPipeline', function () {
describe.skip('runPipeline', function () {
this.tags(['skipFirefox']);
before(async () => {

View file

@ -32,7 +32,6 @@ export default async function ({ readConfigFile }) {
testFiles: [
require.resolve('./test_suites/app_plugins'),
require.resolve('./test_suites/custom_visualizations'),
require.resolve('./test_suites/embedding_visualizations'),
require.resolve('./test_suites/panel_actions'),
require.resolve('./test_suites/search'),

View file

@ -1,39 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default function (kibana) {
return new kibana.Plugin({
uiExports: {
app: {
title: 'Embedding Vis',
description: 'This is a sample plugin to test embedding of visualizations',
main: 'plugins/kbn_tp_visualize_embedding/app',
}
},
init(server) {
// The following lines copy over some configuration variables from Kibana
// to this plugin. This will be needed when embedding visualizations, so that e.g.
// region map is able to get its configuration.
server.injectUiAppVars('kbn_tp_visualize_embedding', async () => {
return await server.getInjectedUiAppVars('kibana');
});
}
});
}

View file

@ -1,14 +0,0 @@
{
"name": "kbn_tp_visualize_embedding",
"version": "1.0.0",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"dependencies": {
"@elastic/eui": "14.8.0",
"react": "^16.8.0",
"react-dom": "^16.8.0"
}
}

View file

@ -1,67 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
// This is required so some default styles and required scripts/Angular modules are loaded,
// or the timezone setting is correctly applied.
import 'ui/autoload/all';
// These are all the required uiExports you need to import in case you want to embed visualizations.
import 'uiExports/visTypes';
import 'uiExports/visResponseHandlers';
import 'uiExports/visRequestHandlers';
import 'uiExports/visEditorTypes';
import 'uiExports/visualize';
import 'uiExports/savedObjectTypes';
import 'uiExports/fieldFormats';
import 'uiExports/search';
import { Main } from './components/main';
const app = uiModules.get('apps/firewallDemoPlugin', ['kibana']);
app.config($locationProvider => {
$locationProvider.html5Mode({
enabled: false,
requireBase: false,
rewriteLinks: false,
});
});
app.config(stateManagementConfigProvider =>
stateManagementConfigProvider.disable()
);
function RootController($scope, $element) {
const domNode = $element[0];
// render react to DOM
render(<Main />, domNode);
// unmount react on controller destroy
$scope.$on('$destroy', () => {
unmountComponentAtNode(domNode);
});
}
chrome.setRootController('firewallDemoPlugin', RootController);

View file

@ -1,140 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiLoadingChart,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiSelect,
} from '@elastic/eui';
import { embeddingSamples } from '../embedding';
const VISUALIZATION_OPTIONS = [
{ value: '', text: '' },
{ value: 'timebased', text: 'Time based' },
{ value: 'timebased_with-filters', text: 'Time based (with filters)' },
{ value: 'timebased_no-datehistogram', text: 'Time based data without date histogram' }
];
class Main extends React.Component {
chartDiv = React.createRef();
state = {
loading: false,
selectedParams: null,
selectedVis: null,
};
embedVisualization = async () => {
if (this.handler) {
// Whenever a visualization is about to be removed from DOM that you embedded,
// you need to call `destroy` on the handler to make sure the visualization is
// teared down correctly.
this.handler.destroy();
this.chartDiv.current.innerHTML = '';
}
const { selectedParams, selectedVis } = this.state;
if (selectedParams && selectedVis) {
this.setState({ loading: true });
const sample = embeddingSamples.find(el => el.id === selectedParams);
this.handler = await sample.run(this.chartDiv.current, selectedVis);
// handler.whenFirstRenderComplete() will return a promise that resolves once the first
// rendering after embedding has finished.
await this.handler.whenFirstRenderComplete();
this.setState({ loading: false });
}
}
onChangeVisualization = async (ev) => {
this.setState({
selectedVis: ev.target.value,
}, this.embedVisualization);
};
onSelectSample = async (ev) => {
this.setState({
selectedParams: ev.target.value,
}, this.embedVisualization);
};
render() {
const samples = [
{ value: '', text: '' },
...embeddingSamples.map(({ id, title }) => ({
value: id,
text: title,
}))
];
return (
<EuiPage>
<EuiPageBody>
<EuiPageContent>
<EuiPageContentHeader>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiFormRow label="Select visualizations">
<EuiSelect
data-test-subj="visSelect"
options={VISUALIZATION_OPTIONS}
onChange={this.onChangeVisualization}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="Select embedding parameters">
<EuiSelect
data-test-subj="embeddingParamsSelect"
options={samples}
onChange={this.onSelectSample}
/>
</EuiFormRow>
</EuiFlexItem>
{ this.state.loading &&
<EuiFlexItem>
<EuiLoadingChart size="xl" data-test-subj="visLoadingIndicator" />
</EuiFlexItem>
}
</EuiFlexGroup>
</EuiPageContentHeader>
<EuiPageContentBody>
{/*
The element you want to render into should have its dimension set (via a fixed height, flexbox, absolute positioning, etc.),
since the visualization will render with exactly the size of that element, i.e. the container size determines the
visualization size.
*/}
<div ref={this.chartDiv} style={{ height: '500px' }} />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}
export { Main };

View file

@ -1,187 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* This files shows a couple of examples how to use the visualize loader API
* to embed visualizations.
*/
import { getVisualizeLoader } from 'ui/visualize';
import chrome from 'ui/chrome';
export const embeddingSamples = [
{
id: 'none',
title: 'No parameters',
async run(domNode, id) {
// You always need to retrieve the visualize loader for embedding visualizations.
const loader = await getVisualizeLoader();
// Use the embedVisualizationWithId method to embed a visualization by its id. The id is the id of the
// saved object in the .kibana index (you can find the id via Management -> Saved Objects).
//
// Pass in a DOM node that you want to embed that visualization into. Note: the loader will
// use the size of that DOM node.
//
// The call will return a handler for the visualization with methods to interact with it.
// Check the components/main.js file to see how this handler is used. Most important: you need to call
// `destroy` on the handler once you are about to remove the visualization from the DOM.
//
// Note: If the visualization you want to embed contains date histograms with an auto interval, you need
// to specify the timeRange parameter (see below).
return loader.embedVisualizationWithId(domNode, id, {});
}
}, {
id: 'timerange',
title: 'timeRange',
async run(domNode, id) {
const loader = await getVisualizeLoader();
// If you want to filter down the data to a specific time range, you can specify a
// timeRange in the parameters to the embedding call.
// You can either use an absolute time range as seen below. You can also specify
// a datemath string, like "now-7d", "now-1w/w" for the from or to key.
// You can also directly assign a moment JS or regular JavaScript Date object.
return loader.embedVisualizationWithId(domNode, id, {
timeRange: {
from: '2015-09-20 20:00:00.000',
to: '2015-09-21 20:00:00.000',
}
});
}
}, {
id: 'query',
title: 'query',
async run(domNode, id) {
const loader = await getVisualizeLoader();
// You can specify a query that should filter down the data via the query parameter.
// It must have a language key which must be one of the supported query languages of Kibana,
// which are at the moment: 'lucene' or 'kquery'.
// The query key must then hold the actual query in the specified language for filtering.
return loader.embedVisualizationWithId(domNode, id, {
query: {
language: 'lucene',
query: 'extension.raw:jpg',
}
});
}
}, {
id: 'filters',
title: 'filters',
async run(domNode, id) {
const loader = await getVisualizeLoader();
// You can specify an array of filters that should apply to the query.
// The format of a filter must match the format the filter bar is using internally.
// This has a query key, which holds the query part of an Elasticsearch query
// and a meta key allowing to set some meta values, most important for this API
// the `negate` option to negate the filter.
return loader.embedVisualizationWithId(domNode, id, {
filters: [
{
query: {
bool: {
should: [
{ match_phrase: { 'extension.raw': 'jpg' } },
{ match_phrase: { 'extension.raw': 'png' } },
]
}
},
meta: {
negate: true
}
}
]
});
}
}, {
id: 'filters_query_timerange',
title: 'filters & query & timeRange',
async run(domNode, id) {
const loader = await getVisualizeLoader();
// You an of course combine timeRange, query and filters options all together
// to filter the data in the embedded visualization.
return loader.embedVisualizationWithId(domNode, id, {
timeRange: {
from: '2015-09-20 20:00:00.000',
to: '2015-09-21 20:00:00.000',
},
query: {
language: 'lucene',
query: 'bytes:>2000'
},
filters: [
{
query: {
bool: {
should: [
{ match_phrase: { 'extension.raw': 'jpg' } },
{ match_phrase: { 'extension.raw': 'png' } },
]
}
},
meta: {
negate: true
}
}
]
});
}
}, {
id: 'savedobject_filter_query_timerange',
title: 'filters & query & time (use saved object)',
async run(domNode, id) {
const loader = await getVisualizeLoader();
// Besides embedding via the id of the visualizataion, the API offers the possibility to
// embed via the saved visualization object.
//
// WE ADVISE YOU NOT TO USE THIS INSIDE ANY PLUGIN!
//
// Since the format of the saved visualization object will change in the future and because
// this still requires you to talk to old Angular code, we do not encourage you to use this
// way of embedding in any plugin. It's likely it will be removed or changed in a future version.
const $injector = await chrome.dangerouslyGetActiveInjector();
const savedVisualizations = $injector.get('savedVisualizations');
const savedVis = await savedVisualizations.get(id);
return loader.embedVisualizationWithSavedObject(domNode, savedVis, {
timeRange: {
from: '2015-09-20 20:00:00.000',
to: '2015-09-21 20:00:00.000',
},
query: {
language: 'lucene',
query: 'bytes:>2000'
},
filters: [
{
query: {
bool: {
should: [
{ match_phrase: { 'extension.raw': 'jpg' } },
{ match_phrase: { 'extension.raw': 'png' } },
]
}
},
meta: {
negate: true
}
}
]
});
}
}
];

View file

@ -1,189 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
import { delay } from 'bluebird';
export default function ({ getService }) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const table = getService('table');
const retry = getService('retry');
async function selectVis(id) {
await testSubjects.click('visSelect');
await find.clickByCssSelector(`option[value="${id}"]`);
}
async function selectParams(id) {
await testSubjects.click('embeddingParamsSelect');
await find.clickByCssSelector(`option[value="${id}"]`);
await retry.try(async () => {
await testSubjects.waitForDeleted('visLoadingIndicator');
});
await delay(1000);
}
async function getTableData() {
const data = await table.getDataFromTestSubj('paginated-table-body');
// Strip away empty rows (at the bottom)
return data.filter(row => !row.every(cell => !cell.trim()));
}
describe('embed by id', function describeIndexTests() {
describe('vis on timebased data without date histogram', () => {
before(async () => {
await selectVis('timebased_no-datehistogram');
});
it('should correctly embed', async () => {
await selectParams('none');
const data = await getTableData();
expect(data).to.be.eql([
['jpg', '9,109'],
['css', '2,159'],
['png', '1,373'],
['gif', '918'],
['php', '445'],
]);
});
it('should correctly embed specifying a timeRange', async () => {
await selectParams('timerange');
const data = await getTableData();
expect(data).to.be.eql([
['jpg', '3,005'],
['css', '720'],
['png', '455'],
['gif', '300'],
['php', '142'],
]);
});
it('should correctly embed specifying a query', async () => {
await selectParams('query');
const data = await getTableData();
expect(data).to.be.eql([
['jpg', '9,109'],
]);
});
it('should correctly embed specifying filters', async () => {
await selectParams('filters');
const data = await getTableData();
expect(data).to.be.eql([
['css', '2,159'],
['gif', '918'],
['php', '445'],
]);
});
it('should correctly embed specifying filters and query and timeRange', async () => {
await selectParams('filters_query_timerange');
const data = await getTableData();
expect(data).to.be.eql([
['css', '678'],
['php', '110'],
]);
});
});
describe('vis on timebased data with date histogram with interval auto', () => {
before(async () => {
await selectVis('timebased');
});
it('should correctly embed specifying a timeRange', async () => {
await selectParams('timerange');
const data = await getTableData();
expect(data).to.be.eql([
['2015-09-20 20:00', '45.159KB', '5.65KB'],
['2015-09-21 00:00', '42.428KB', '5.345KB'],
['2015-09-21 04:00', '43.717KB', '5.35KB'],
['2015-09-21 08:00', '43.228KB', '5.538KB'],
['2015-09-21 12:00', '42.83KB', '5.669KB'],
['2015-09-21 16:00', '44.908KB', '5.673KB'],
]);
});
it('should correctly embed specifying filters and query and timeRange', async () => {
await selectParams('filters_query_timerange');
const data = await getTableData();
expect(data).to.be.eql([
['2015-09-20 20:00', '45.391KB', '5.692KB'],
['2015-09-21 00:00', '46.57KB', '5.953KB'],
['2015-09-21 04:00', '47.339KB', '6.636KB'],
['2015-09-21 08:00', '40.5KB', '6.133KB'],
['2015-09-21 12:00', '41.31KB', '5.84KB'],
['2015-09-21 16:00', '48.012KB', '6.003KB'],
]);
});
});
describe('vis on timebased data with date histogram with interval auto and saved filters', () => {
before(async () => {
await selectVis('timebased_with-filters');
});
it('should correctly embed specifying a timeRange', async () => {
await selectParams('timerange');
const data = await getTableData();
expect(data).to.be.eql([
['2015-09-20 20:00', '21.221KB', '2.66KB'],
['2015-09-21 00:00', '22.054KB', '2.63KB'],
['2015-09-21 04:00', '15.592KB', '2.547KB'],
['2015-09-21 08:00', '4.656KB', '2.328KB'],
['2015-09-21 12:00', '17.887KB', '2.694KB'],
['2015-09-21 16:00', '20.533KB', '2.529KB'],
]);
});
it('should correctly embed specifying filters and query and timeRange', async () => {
await selectParams('filters_query_timerange');
const data = await getTableData();
expect(data).to.be.eql([
['2015-09-20 20:00', '24.567KB', '3.498KB'],
['2015-09-21 00:00', '25.984KB', '3.589KB'],
['2015-09-21 04:00', '2.543KB', '2.543KB'],
['2015-09-21 12:00', '5.783KB', '2.927KB'],
['2015-09-21 16:00', '21.107KB', '3.44KB'],
]);
});
});
describe('vis visa saved object on timebased data with date histogram with interval auto and saved filters', () => {
before(async () => {
await selectVis('timebased_with-filters');
});
it('should correctly embed specifying filters and query and timeRange', async () => {
await selectParams('savedobject_filter_query_timerange');
const data = await getTableData();
expect(data).to.be.eql([
['2015-09-20 20:00', '24.567KB', '3.498KB'],
['2015-09-21 00:00', '25.984KB', '3.589KB'],
['2015-09-21 04:00', '2.543KB', '2.543KB'],
['2015-09-21 12:00', '5.783KB', '2.927KB'],
['2015-09-21 16:00', '21.107KB', '3.44KB'],
]);
});
});
});
}

View file

@ -1,43 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export default function ({ getService, getPageObjects, loadTestFile }) {
const browser = getService('browser');
const appsMenu = getService('appsMenu');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['common', 'header']);
describe('embedding visualizations', function () {
before(async () => {
await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional');
await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding');
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'Australia/North',
'defaultIndex': 'logstash-*',
'format:bytes:defaultPattern': '0,0.[000]b'
});
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('settings');
await appsMenu.clickLink('Embedding Vis');
});
loadTestFile(require.resolve('./embed_by_id'));
});
}

View file

@ -556,7 +556,6 @@
"common.ui.vislib.colormaps.greysText": "グレー",
"common.ui.vislib.colormaps.redsText": "赤",
"common.ui.vislib.colormaps.yellowToRedText": "黄色から赤",
"common.ui.visualize.dataLoaderError": "ビジュアライゼーションエラー",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした",
"common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。",
"common.ui.welcomeMessage": "Kibana を読み込み中",

View file

@ -557,7 +557,6 @@
"common.ui.vislib.colormaps.greysText": "灰色",
"common.ui.vislib.colormaps.redsText": "红色",
"common.ui.vislib.colormaps.yellowToRedText": "黄到红",
"common.ui.visualize.dataLoaderError": "可视化错误",
"common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界",
"common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。",
"common.ui.welcomeMessage": "正在加载 Kibana",