mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
1dc8933b72
commit
f4b918da11
49 changed files with 338 additions and 2932 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -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 || {}) },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -79,6 +79,7 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => {
|
|||
return;
|
||||
}
|
||||
if (precisionChange) {
|
||||
updateGeohashAgg();
|
||||
this.vis.updateState();
|
||||
} else {
|
||||
//when we filter queries by collar
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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';
|
|
@ -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=""
|
||||
/>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -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 }));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}));
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -18,4 +18,3 @@
|
|||
*/
|
||||
|
||||
export { buildPipeline } from './build_pipeline';
|
||||
export { runPipeline } from './run_pipeline';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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'
|
||||
>;
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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 };
|
|
@ -91,6 +91,7 @@ export interface IExpressionLoaderParams {
|
|||
customFunctions?: [];
|
||||
customRenderers?: [];
|
||||
extraHandlers?: Record<string, any>;
|
||||
inspectorAdapters?: Adapters;
|
||||
}
|
||||
|
||||
export interface IInterpreterHandlers {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'),
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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 };
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
|
@ -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'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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 を読み込み中",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue