cleanup visualizations api (#59958) (#60959)

This commit is contained in:
Peter Pisljar 2020-03-24 08:29:03 +01:00 committed by GitHub
parent a524c001f5
commit 80a7fc3d71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 1245 additions and 1495 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md)
## AggConfigOptions.enabled property
<b>Signature:</b>
```typescript
enabled?: boolean;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md)
## AggConfigOptions.id property
<b>Signature:</b>
```typescript
id?: string;
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md)
## AggConfigOptions interface
<b>Signature:</b>
```typescript
export interface AggConfigOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | <code>boolean</code> | |
| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | <code>string</code> | |
| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | <code>Record&lt;string, any&gt;</code> | |
| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | <code>string</code> | |
| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | <code>IAggType</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md)
## AggConfigOptions.params property
<b>Signature:</b>
```typescript
params?: Record<string, any>;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md)
## AggConfigOptions.schema property
<b>Signature:</b>
```typescript
schema?: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) &gt; [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md)
## AggConfigOptions.type property
<b>Signature:</b>
```typescript
type: IAggType;
```

View file

@ -48,6 +48,7 @@
| Interface | Description |
| --- | --- |
| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | |
| [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | |
| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | |
| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | |

View file

@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n';
import { createInputControlVisController } from './vis_controller';
import { getControlsTab } from './components/editor/controls_tab';
import { OptionsTab } from './components/editor/options_tab';
import { Status } from '../../visualizations/public';
import { InputControlVisDependencies } from './plugin';
import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common';
@ -40,7 +39,6 @@ export function createInputControlVisTypeDefinition(deps: InputControlVisDepende
defaultMessage: 'Create interactive controls for easy dashboard manipulation.',
}),
stage: 'experimental',
requiresUpdateStatus: [Status.PARAMS, Status.TIME],
feedbackMessage: defaultFeedbackMessage,
visualization: InputControlVisController,
visConfig: {

View file

@ -54,7 +54,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie
.subscribe(this.queryBarUpdateHandler);
}
async render(visData: any, visParams: VisParams, status: any) {
async render(visData: any, visParams: VisParams) {
this.visParams = visParams;
this.controls = [];
this.controls = await this.initControls();

View file

@ -672,7 +672,7 @@ function discoverController(
// no timefield, no vis, nothing to update
if (!getTimeField() || !$scope.vis) return;
const buckets = $scope.vis.getAggConfig().byTypeName('buckets');
const buckets = $scope.vis.data.aggs.byTypeName('buckets');
if (buckets && buckets.length === 1) {
$scope.bucketInterval = buckets[0].buckets.getInterval();
@ -876,11 +876,11 @@ function discoverController(
inspectorRequest.stats(getResponseInspectorStats($scope.searchSource, resp)).ok({ json: resp });
if (getTimeField()) {
const tabifiedData = tabifyAggResponse($scope.vis.aggs, resp);
const tabifiedData = tabifyAggResponse($scope.vis.data.aggs, resp);
$scope.searchSource.rawResponse = resp;
$scope.histogramData = discoverResponseHandler(
tabifiedData,
getDimensions($scope.vis.aggs.aggs, $scope.timeRange)
getDimensions($scope.vis.data.aggs.aggs, $scope.timeRange)
);
}
@ -1023,41 +1023,27 @@ function discoverController(
},
];
if ($scope.vis) {
const visState = $scope.vis.getEnabledState();
visState.aggs = visStateAggs;
$scope.vis.setState(visState);
return;
}
const visSavedObject = {
indexPattern: $scope.indexPattern.id,
visState: {
type: 'histogram',
title: savedSearch.title,
params: {
addLegend: false,
addTimeMarker: true,
},
aggs: visStateAggs,
$scope.vis = visualizations.createVis('histogram', {
title: savedSearch.title,
params: {
addLegend: false,
addTimeMarker: true,
},
};
$scope.vis = visualizations.createVis(
$scope.searchSource.getField('index'),
visSavedObject.visState
);
visSavedObject.vis = $scope.vis;
data: {
aggs: visStateAggs,
indexPattern: $scope.searchSource.getField('index').id,
searchSource: $scope.searchSource,
},
});
$scope.searchSource.onRequestStart((searchSource, options) => {
if (!$scope.vis) return;
return $scope.vis.getAggConfig().onSearchRequestStart(searchSource, options);
return $scope.vis.data.aggs.onSearchRequestStart(searchSource, options);
});
$scope.searchSource.setField('aggs', function() {
if (!$scope.vis) return;
return $scope.vis.getAggConfig().toDsl();
return $scope.vis.data.aggs.toDsl();
});
}

View file

@ -69,7 +69,7 @@ export function getEditBreadcrumbs($route: any) {
return [
...getLandingBreadcrumbs(),
{
text: $route.current.locals.savedVis.title,
text: $route.current.locals.resolved.savedVis.title,
},
];
}

View file

@ -69,7 +69,8 @@
<visualization-embedded
ng-if="!isVisible"
class="visualize"
saved-obj="savedVis"
vis="vis"
embeddable-handler="embeddableHandler"
ui-state="uiState"
time-range="timeRange"
filters="filters"
@ -89,13 +90,15 @@
</h1>
<visualization-editor
ng-if="isVisible"
saved-obj="savedVis"
vis="vis"
saved-search="savedSearch"
embeddable-handler="embeddableHandler"
event-emitter="eventEmitter"
ui-state="uiState"
time-range="timeRange"
filters="filters"
query="query"
class="visEditor__content"
app-state="appState"
/>
</visualize-app>

View file

@ -22,6 +22,7 @@ import _ from 'lodash';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { EventEmitter } from 'events';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
@ -84,6 +85,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
uiSettings,
I18nContext,
setActiveUrl,
visualizations,
} = getServices();
const {
@ -98,27 +100,63 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
);
// Retrieve the resolved SavedVis instance.
const savedVis = $route.current.locals.savedVis;
const { vis, savedVis, savedSearch, embeddableHandler } = $route.current.locals.resolved;
$scope.eventEmitter = new EventEmitter();
const _applyVis = () => {
$scope.$apply();
};
// This will trigger a digest cycle. This is needed when vis is updated from a global angular like in visualize_embeddable.js.
savedVis.vis.on('apply', _applyVis);
$scope.eventEmitter.on('apply', _applyVis);
// vis is instance of src/legacy/ui/public/vis/vis.js.
// SearchSource is a promise-based stream of search results that can inherit from other search sources.
const { vis, searchSource, savedSearch } = savedVis;
const searchSource = vis.data.searchSource;
$scope.vis = vis;
$scope.savedSearch = savedSearch;
const $appStatus = {
dirty: !savedVis.id,
};
vis.on('dirtyStateChange', ({ isDirty }) => {
vis.dirty = isDirty;
$scope.$digest();
const defaultQuery = {
query: '',
language:
localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'),
};
const visStateToEditorState = () => {
const savedVisState = visualizations.convertFromSerializedVis(vis.serialize());
return {
uiState: vis.uiState.toJSON(),
query: vis.data.searchSource.getOwnField('query') || defaultQuery,
filters: vis.data.searchSource.getOwnField('filter') || [],
vis: { ...savedVisState.visState, title: vis.title },
linked: !!savedVis.savedSearchId,
};
};
const stateDefaults = visStateToEditorState();
const { stateContainer, stopStateSync } = useVisualizeAppState({
stateDefaults,
kbnUrlStateStorage,
});
$scope.eventEmitter.on('dirtyStateChange', ({ isDirty }) => {
if (!isDirty) {
stateContainer.transitions.updateVisState(visStateToEditorState().vis);
}
$timeout(() => {
$scope.dirty = isDirty;
});
});
$scope.eventEmitter.on('updateVis', () => {
embeddableHandler.reload();
});
$scope.embeddableHandler = embeddableHandler;
$scope.topNavMenu = [
...(visualizeCapabilities.save
? [
@ -135,10 +173,10 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
),
testId: 'visualizeSaveButton',
disableButton() {
return Boolean(vis.dirty);
return Boolean($scope.dirty);
},
tooltip() {
if (vis.dirty) {
if ($scope.dirty) {
return i18n.translate(
'kbn.visualize.topNavMenu.saveVisualizationDisabledButtonTooltip',
{
@ -207,7 +245,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
}),
testId: 'shareTopNavButton',
run: anchorElement => {
const hasUnappliedChanges = vis.dirty;
const hasUnappliedChanges = $scope.dirty;
const hasUnsavedChanges = $appStatus.dirty;
share.toggleShareContextMenu({
anchorElement,
@ -233,17 +271,17 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
}),
testId: 'openInspectorButton',
disableButton() {
return !vis.hasInspector || !vis.hasInspector();
return !embeddableHandler.hasInspector || !embeddableHandler.hasInspector();
},
run() {
const inspectorSession = vis.openInspector();
const inspectorSession = embeddableHandler.openInspector();
// Close the inspector if this scope is destroyed (e.g. because the user navigates away).
const removeWatch = $scope.$on('$destroy', () => inspectorSession.close());
// Remove that watch in case the user closes the inspector session herself.
inspectorSession.onClose.finally(removeWatch);
},
tooltip() {
if (!vis.hasInspector || !vis.hasInspector()) {
if (!embeddableHandler.hasInspector || !embeddableHandler.hasInspector()) {
return i18n.translate('kbn.visualize.topNavMenu.openInspectorDisabledButtonTooltip', {
defaultMessage: `This visualization doesn't support any inspectors.`,
});
@ -257,7 +295,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
defaultMessage: 'Refresh',
}),
run: function() {
vis.forceReload();
embeddableHandler.reload();
},
testId: 'visualizeRefreshButton',
},
@ -267,28 +305,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
chrome.docTitle.change(savedVis.title);
}
const defaultQuery = {
query: '',
language:
localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'),
};
// Extract visualization state with filtered aggs. You can see these filtered aggs in the URL.
// Consists of things like aggs, params, listeners, title, type, etc.
const savedVisState = vis.getState();
const stateDefaults = {
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
query: searchSource.getOwnField('query') || defaultQuery,
filters: searchSource.getOwnField('filter') || [],
vis: savedVisState,
linked: !!savedVis.savedSearchId,
};
const { stateContainer, stopStateSync } = useVisualizeAppState({
stateDefaults,
kbnUrlStateStorage,
});
// sync initial app filters from state to filterManager
filterManager.setAppFilters(_.cloneDeep(stateContainer.getState().filters));
// setup syncing of app filters between appState and filterManager
@ -315,7 +331,8 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
// appState then they won't be equal.
if (!_.isEqual(stateContainer.getState().vis, stateDefaults.vis)) {
try {
vis.setState(stateContainer.getState().vis);
const { aggs, ...visState } = stateContainer.getState().vis;
vis.setState({ ...visState, data: { aggs } });
} catch (error) {
// stop syncing url updtes with the state to prevent extra syncing
stopAllSyncing();
@ -369,8 +386,8 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
};
function init() {
if (vis.indexPattern) {
$scope.indexPattern = vis.indexPattern;
if (vis.data.indexPattern) {
$scope.indexPattern = vis.data.indexPattern;
} else {
indexPatterns.getDefault().then(defaultIndexPattern => {
$scope.indexPattern = defaultIndexPattern;
@ -379,22 +396,14 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
const initialState = stateContainer.getState();
$scope.appState = {
// mock implementation of the legacy appState.save()
// this could be even replaced by passing only "updateAppState" callback
save() {
stateContainer.transitions.updateVisState(vis.getState());
},
};
const handleLinkedSearch = linked => {
if (linked && !savedVis.savedSearchId && savedSearch) {
savedVis.savedSearchId = savedSearch.id;
vis.savedSearchId = savedSearch.id;
vis.data.savedSearchId = savedSearch.id;
searchSource.setParent(savedSearch.searchSource);
} else if (!linked && savedVis.savedSearchId) {
delete savedVis.savedSearchId;
delete vis.savedSearchId;
delete vis.data.savedSearchId;
}
};
@ -403,6 +412,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
'uiState',
stateContainer
);
vis.uiState = persistedState;
$scope.uiState = persistedState;
$scope.savedVis = savedVis;
$scope.query = initialState.query;
@ -427,7 +437,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
$scope.showQueryBarTimePicker = () => {
// tsvb loads without an indexPattern initially (TODO investigate).
// hide timefilter only if timeFieldName is explicitly undefined.
const hasTimeField = vis.indexPattern ? !!vis.indexPattern.timeFieldName : true;
const hasTimeField = vis.data.indexPattern ? !!vis.data.indexPattern.timeFieldName : true;
return vis.type.options.showTimePicker && hasTimeField;
};
@ -442,10 +452,24 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
updateSavedQueryFromUrl(state.savedQuery);
// if the browser history was changed manually we need to reflect changes in the editor
if (!_.isEqual(vis.getState(), state.vis)) {
vis.setState(state.vis);
vis.forceReload();
vis.emit('updateEditor');
if (
!_.isEqual(
{
...visualizations.convertFromSerializedVis(vis.serialize()).visState,
title: vis.title,
},
state.vis
)
) {
const { aggs, ...visState } = state.vis;
vis.setState({
...visState,
data: {
aggs,
},
});
embeddableHandler.reload();
$scope.eventEmitter.emit('updateEditor');
}
$appStatus.dirty = true;
@ -498,8 +522,8 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
const { query, linked, filters } = stateContainer.getState();
$scope.query = query;
handleLinkedSearch(linked);
savedVis.searchSource.setField('query', query);
savedVis.searchSource.setField('filter', filters);
vis.data.searchSource.setField('query', query);
vis.data.searchSource.setField('filter', filters);
$scope.$broadcast('render');
};
@ -533,7 +557,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
}
savedVis.destroy();
subscriptions.unsubscribe();
$scope.vis.off('apply', _applyVis);
$scope.eventEmitter.off('apply', _applyVis);
unsubscribePersisted();
unsubscribeStateUpdates();
@ -556,7 +580,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
// If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes
if (!isUpdate) {
$scope.vis.forceReload();
embeddableHandler.reload();
}
};
@ -605,8 +629,10 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
title: savedVis.title,
type: savedVis.type || stateContainer.getState().vis.type,
});
savedVis.searchSource.setField('query', stateContainer.getState().query);
savedVis.searchSource.setField('filter', stateContainer.getState().filters);
savedVis.visState = stateContainer.getState().vis;
savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges());
savedVis.uiStateJSON = angular.toJson($scope.uiState.toJSON());
$appStatus.dirty = false;
return savedVis.save(saveOptions).then(
@ -720,7 +746,7 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
);
};
vis.on('unlinkFromSavedSearch', unlinkFromSavedSearch);
$scope.eventEmitter.on('unlinkFromSavedSearch', unlinkFromSavedSearch);
addHelpMenuToAppChrome(chrome, docLinks);

View file

@ -17,12 +17,12 @@
* under the License.
*/
export function initVisualizationDirective(app, deps) {
export function initVisualizationDirective(app) {
app.directive('visualizationEmbedded', function($timeout) {
return {
restrict: 'E',
scope: {
savedObj: '=',
embeddableHandler: '=',
uiState: '=?',
timeRange: '=',
filters: '=',
@ -31,24 +31,16 @@ export function initVisualizationDirective(app, deps) {
},
link: function($scope, element) {
$scope.renderFunction = async () => {
if (!$scope._handler) {
$scope._handler = await deps.embeddable
.getEmbeddableFactory('visualization')
.createFromObject($scope.savedObj, {
timeRange: $scope.timeRange,
filters: $scope.filters || [],
query: $scope.query,
appState: $scope.appState,
uiState: $scope.uiState,
});
$scope._handler.render(element[0]);
} else {
$scope._handler.updateInput({
timeRange: $scope.timeRange,
filters: $scope.filters || [],
query: $scope.query,
});
if (!$scope.rendered) {
$scope.embeddableHandler.render(element[0]);
$scope.rendered = true;
}
$scope.embeddableHandler.updateInput({
timeRange: $scope.timeRange,
filters: $scope.filters || [],
query: $scope.query,
});
};
$scope.$on('render', event => {
@ -59,8 +51,8 @@ export function initVisualizationDirective(app, deps) {
});
$scope.$on('$destroy', () => {
if ($scope._handler) {
$scope._handler.destroy();
if ($scope.embeddableHandler) {
$scope.embeddableHandler.destroy();
}
});
},

View file

@ -22,16 +22,23 @@ export function initVisEditorDirective(app, deps) {
return {
restrict: 'E',
scope: {
savedObj: '=',
vis: '=',
uiState: '=?',
timeRange: '=',
filters: '=',
query: '=',
appState: '=',
savedSearch: '=',
embeddableHandler: '=',
eventEmitter: '=',
},
link: function($scope, element) {
const Editor = $scope.savedObj.vis.type.editor || deps.DefaultVisualizationEditor;
const editor = new Editor(element[0], $scope.savedObj);
const Editor = $scope.vis.type.editor || deps.DefaultVisualizationEditor;
const editor = new Editor(
element[0],
$scope.vis,
$scope.eventEmitter,
$scope.embeddableHandler
);
$scope.renderFunction = () => {
editor.render({
@ -42,8 +49,8 @@ export function initVisEditorDirective(app, deps) {
timeRange: $scope.timeRange,
filters: $scope.filters,
query: $scope.query,
appState: $scope.appState,
linked: !!$scope.savedObj.savedSearchId,
linked: !!$scope.vis.data.savedSearchId,
savedSearch: $scope.savedSearch,
});
};

View file

@ -40,6 +40,50 @@ import {
getCreateBreadcrumbs,
getEditBreadcrumbs,
} from './breadcrumbs';
import { createSavedSearchesLoader } from '../../../../../../plugins/discover/public';
const getResolvedResults = deps => {
const { core, data, visualizations } = deps;
const results = {};
return savedVis => {
results.savedVis = savedVis;
return visualizations
.convertToSerializedVis(savedVis)
.then(serializedVis => visualizations.createVis(serializedVis.type, serializedVis))
.then(vis => {
if (vis.type.setup) {
return vis.type.setup(vis).catch(() => vis);
}
return vis;
})
.then(vis => {
results.vis = vis;
return deps.embeddable.getEmbeddableFactory('visualization').createFromObject(results.vis, {
timeRange: data.query.timefilter.timefilter.getTime(),
filters: data.query.filterManager.getFilters(),
});
})
.then(embeddableHandler => {
results.embeddableHandler = embeddableHandler;
if (results.vis.data.savedSearchId) {
return createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
chrome: core.chrome,
overlays: core.overlays,
}).get(results.vis.data.savedSearchId);
}
})
.then(savedSearch => {
if (savedSearch) {
results.savedSearch = savedSearch;
}
return results;
});
};
};
export function initVisualizeApp(app, deps) {
initVisualizeAppDirective(app, deps);
@ -101,7 +145,7 @@ export function initVisualizeApp(app, deps) {
template: editorTemplate,
k7Breadcrumbs: getCreateBreadcrumbs,
resolve: {
savedVis: function($route, history) {
resolved: function($route, history) {
const { core, data, savedVisualizations, visualizations, toastNotifications } = deps;
const visTypes = visualizations.all();
const visType = find(visTypes, { name: $route.current.params.type });
@ -121,12 +165,7 @@ export function initVisualizeApp(app, deps) {
return ensureDefaultIndexPattern(core, data, history)
.then(() => savedVisualizations.get($route.current.params))
.then(savedVis => {
if (savedVis.vis.type.setup) {
return savedVis.vis.type.setup(savedVis).catch(() => savedVis);
}
return savedVis;
})
.then(getResolvedResults(deps))
.catch(
redirectWhenMissing({
history,
@ -142,20 +181,16 @@ export function initVisualizeApp(app, deps) {
template: editorTemplate,
k7Breadcrumbs: getEditBreadcrumbs,
resolve: {
savedVis: function($route, history) {
resolved: function($route, history) {
const { chrome, core, data, savedVisualizations, toastNotifications } = deps;
return ensureDefaultIndexPattern(core, data, history)
.then(() => savedVisualizations.get($route.current.params.id))
.then(savedVis => {
chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id);
return savedVis;
})
.then(savedVis => {
if (savedVis.vis.type.setup) {
return savedVis.vis.type.setup(savedVis).catch(() => savedVis);
}
return savedVis;
})
.then(getResolvedResults(deps))
.catch(
redirectWhenMissing({
history,

View file

@ -29,8 +29,10 @@ import { PersistedState } from 'src/plugins/visualizations/public';
import { LegacyCoreStart } from 'kibana/public';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { VisSavedObject } from '../legacy_imports';
import { SavedVisState } from '../../../../visualizations/public/np_ready/public/types';
import { SavedSearch } from '../../../../../../plugins/discover/public';
export type PureVisState = ReturnType<Vis['getCurrentState']>;
export type PureVisState = SavedVisState;
export interface VisualizeAppState {
filters: Filter[];
@ -58,14 +60,13 @@ export interface VisualizeAppStateTransitions {
}
export interface EditorRenderProps {
appState: { save(): void };
core: LegacyCoreStart;
data: DataPublicPluginStart;
embeddable: EmbeddableStart;
filters: Filter[];
uiState: PersistedState;
timeRange: TimeRange;
query?: Query;
savedSearch?: SavedSearch;
uiState: PersistedState;
/**
* Flag to determine if visualiztion is linked to the saved search
*/

View file

@ -21,7 +21,6 @@ import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import _ from 'lodash';
import ChoroplethLayer from '../choropleth_layer';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import { ImageComparator } from 'test_utils/image_comparator';
import worldJson from './world.json';
import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json';
@ -38,13 +37,11 @@ import afterdatachangePng from './afterdatachange.png';
import afterdatachangeandresizePng from './afterdatachangeandresize.png';
import aftercolorchangePng from './aftercolorchange.png';
import changestartupPng from './changestartup.png';
import {
setup as visualizationsSetup,
start as visualizationsStart,
} from '../../../visualizations/public/np_ready/public/legacy';
import { setup as visualizationsSetup } from '../../../visualizations/public/np_ready/public/legacy';
import { createRegionMapVisualization } from '../region_map_visualization';
import { createRegionMapTypeDefinition } from '../region_map_type';
import { ExprVis } from '../../../visualizations/public/np_ready/public/expressions/vis';
const THRESHOLD = 0.45;
const PIXEL_DIFF = 96;
@ -52,7 +49,6 @@ const PIXEL_DIFF = 96;
describe('RegionMapsVisualizationTests', function() {
let domNode;
let RegionMapsVisualization;
let indexPattern;
let vis;
let dependencies;
@ -115,7 +111,6 @@ describe('RegionMapsVisualizationTests', function() {
}
RegionMapsVisualization = createRegionMapVisualization(dependencies);
indexPattern = Private(LogstashIndexPatternStubProvider);
ChoroplethLayer.prototype._makeJsonAjaxCall = async function() {
//simulate network call
@ -158,7 +153,7 @@ describe('RegionMapsVisualizationTests', function() {
imageComparator = new ImageComparator();
vis = visualizationsStart.createVis(indexPattern, {
vis = new ExprVis({
type: 'region_map',
});

View file

@ -20,7 +20,6 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { mapToLayerWithId } from './util';
import { createRegionMapVisualization } from './region_map_visualization';
import { Status } from '../../visualizations/public';
import { RegionMapOptions } from './components/region_map_options';
import { truncatedColorSchemas } from '../../../../plugins/charts/public';
import { Schemas } from '../../vis_default_editor/public';
@ -55,7 +54,6 @@ provided base maps, or add your own. Darker colors represent higher values.',
showAllShapes: true, //still under consideration
},
},
requiresUpdateStatus: [Status.AGGS, Status.PARAMS, Status.RESIZE, Status.DATA, Status.UI_STATE],
visualization,
editorConfig: {
optionsTemplate: props => <RegionMapOptions {...props} serviceSettings={serviceSettings} />,
@ -100,9 +98,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
},
]),
},
setup: async savedVis => {
const vis = savedVis.vis;
setup: async vis => {
const tmsLayers = await serviceSettings.getTMSServices();
vis.type.editorConfig.collections.tmsLayers = tmsLayers;
if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) {
@ -146,7 +142,7 @@ provided base maps, or add your own. Darker colors represent higher values.',
vis.params.selectedJoinField = selectedJoinField;
}
return savedVis;
return vis;
},
};
}

View file

@ -39,8 +39,8 @@ export function createRegionMapVisualization({ serviceSettings, $injector, uiSet
this._choroplethLayer = null;
}
async render(esResponse, visParams, status) {
await super.render(esResponse, visParams, status);
async render(esResponse, visParams) {
await super.render(esResponse, visParams);
if (this._choroplethLayer) {
await this._choroplethLayer.whenDataLoaded();
}

View file

@ -19,7 +19,6 @@
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import { ImageComparator } from 'test_utils/image_comparator';
import dummyESResponse from './dummy_es_response.json';
import initial from './initial.png';
@ -32,13 +31,11 @@ import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_
import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright';
import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated';
import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark';
import {
setup as visualizationsSetup,
start as visualizationsStart,
} from '../../../visualizations/public/np_ready/public/legacy';
import { setup as visualizationsSetup } from '../../../visualizations/public/np_ready/public/legacy';
import { createTileMapVisualization } from '../tile_map_visualization';
import { createTileMapTypeDefinition } from '../tile_map_type';
import { ExprVis } from '../../../visualizations/public/np_ready/public/expressions/vis';
function mockRawData() {
const stack = [dummyESResponse];
@ -67,7 +64,6 @@ let visRegComplete = false;
describe('CoordinateMapsVisualizationTest', function() {
let domNode;
let CoordinateMapsVisualization;
let indexPattern;
let vis;
let dependencies;
@ -92,7 +88,6 @@ describe('CoordinateMapsVisualizationTest', function() {
}
CoordinateMapsVisualization = createTileMapVisualization(dependencies);
indexPattern = Private(LogstashIndexPatternStubProvider);
getManifestStub = serviceSettings.__debugStubManifestCalls(async url => {
//simulate network calls
@ -124,7 +119,7 @@ describe('CoordinateMapsVisualizationTest', function() {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = visualizationsStart.createVis(indexPattern, {
vis = new ExprVis({
type: 'tile_map',
});
vis.params = {

View file

@ -63,28 +63,21 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
* @param status
* @return {Promise}
*/
async render(esResponse, visParams, status) {
async render(esResponse, visParams) {
if (!this._kibanaMap) {
//the visualization has been destroyed;
return;
}
await this._mapIsLoaded;
if (status.resize) {
this._kibanaMap.resize();
}
if (status.params || status.aggs) {
this._params = visParams;
await this._updateParams();
}
this._kibanaMap.resize();
this._params = visParams;
await this._updateParams();
if (this._hasESResponseChanged(esResponse)) {
await this._updateData(esResponse);
}
if (status.uiState) {
this._kibanaMap.useUiStateFromVisualization(this.vis);
}
this._kibanaMap.useUiStateFromVisualization(this.vis);
await this._whenBaseLayerIsLoaded();
}

View file

@ -23,7 +23,6 @@ import { i18n } from '@kbn/i18n';
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
import { Schemas } from '../../vis_default_editor/public';
import { Status } from '../../visualizations/public';
import { createTileMapVisualization } from './tile_map_visualization';
import { TileMapOptions } from './components/tile_map_options';
import { MapTypes } from './map_types';
@ -57,7 +56,6 @@ export function createTileMapTypeDefinition(dependencies) {
wms: uiSettings.get('visualization:tileMap:WMSdefaults'),
},
},
requiresUpdateStatus: [Status.AGGS, Status.PARAMS, Status.RESIZE, Status.UI_STATE],
requiresPartialRows: true,
visualization: CoordinateMapsVisualization,
responseHandler: convertToGeoJson,
@ -143,21 +141,20 @@ export function createTileMapTypeDefinition(dependencies) {
},
]),
},
setup: async savedVis => {
const vis = savedVis.vis;
setup: async vis => {
let tmsLayers;
try {
tmsLayers = await serviceSettings.getTMSServices();
} catch (e) {
return savedVis;
return vis;
}
vis.type.editorConfig.collections.tmsLayers = tmsLayers;
if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) {
vis.params.wms.selectedTmsLayer = tmsLayers[0];
}
return savedVis;
return vis;
},
};
}

View file

@ -22,12 +22,12 @@ import { mount, shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { IndexPattern, IAggType, AggGroupNames } from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { DefaultEditorAgg, DefaultEditorAggProps } from './agg';
import { DefaultEditorAggParams } from './agg_params';
import { AGGS_ACTION_KEYS } from './agg_group_state';
import { Schema } from '../schemas';
import { EditorVisState } from './sidebar/state/reducers';
jest.mock('./agg_params', () => ({
DefaultEditorAggParams: () => null,
@ -67,7 +67,7 @@ describe('DefaultEditorAgg component', () => {
isLastBucket: false,
isRemovable: false,
metricAggs: [],
state: { params: {} } as VisState,
state: { params: {} } as EditorVisState,
setAggParamValue,
setStateParamValue,
onAggTypeChange: () => {},

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { VisState, VisParams } from 'src/legacy/core_plugins/visualizations/public';
import { VisParams } from 'src/legacy/core_plugins/visualizations/public';
import { IAggType, IAggConfig, IAggGroupNames } from 'src/plugins/data/public';
import { Schema } from '../schemas';
import { EditorVisState } from './sidebar/state/reducers';
type AggId = IAggConfig['id'];
type AggParams = IAggConfig['params'];
@ -31,7 +32,7 @@ export interface DefaultEditorCommonProps {
formIsTouched: boolean;
groupName: IAggGroupNames;
metricAggs: IAggConfig[];
state: VisState;
state: EditorVisState;
setAggParamValue: <T extends keyof AggParams>(
aggId: AggId,
paramName: T,

View file

@ -20,12 +20,12 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { IAggConfigs, IAggConfig } from 'src/plugins/data/public';
import { DefaultEditorAggGroup, DefaultEditorAggGroupProps } from './agg_group';
import { DefaultEditorAgg } from './agg';
import { DefaultEditorAggAdd } from './agg_add';
import { Schema } from '../schemas';
import { EditorVisState } from './sidebar/state/reducers';
jest.mock('@elastic/eui', () => ({
EuiTitle: 'eui-title',
@ -93,8 +93,8 @@ describe('DefaultEditorAgg component', () => {
metricAggs: [],
groupName: 'metrics',
state: {
aggs,
} as VisState,
data: { aggs },
} as EditorVisState,
schemas: [
{
name: 'metrics',
@ -147,8 +147,8 @@ describe('DefaultEditorAgg component', () => {
});
expect(reorderAggs).toHaveBeenCalledWith(
defaultProps.state.aggs.aggs[0],
defaultProps.state.aggs.aggs[1]
defaultProps.state.data.aggs!.aggs[0],
defaultProps.state.data.aggs!.aggs[1]
);
});

View file

@ -73,9 +73,10 @@ function DefaultEditorAggGroup({
const schemaNames = getSchemasByGroup(schemas, groupName).map(s => s.name);
const group: IAggConfig[] = useMemo(
() =>
state.aggs.aggs.filter((agg: IAggConfig) => agg.schema && schemaNames.includes(agg.schema)) ||
[],
[state.aggs.aggs, schemaNames]
state.data.aggs!.aggs.filter(
(agg: IAggConfig) => agg.schema && schemaNames.includes(agg.schema)
) || [],
[state.data.aggs, schemaNames]
);
const stats = {

View file

@ -18,10 +18,10 @@
*/
import { IAggConfig, AggParam, IndexPatternField } from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { ComboBoxGroupedOptions } from '../utils';
import { EditorConfig } from './utils';
import { Schema } from '../schemas';
import { EditorVisState } from './sidebar/state/reducers';
// NOTE: we cannot export the interface with export { InterfaceName }
// as there is currently a bug on babel typescript transform plugin for it
@ -35,7 +35,7 @@ export interface AggParamCommonProps<T, P = AggParam> {
formIsTouched: boolean;
indexedFields?: ComboBoxGroupedOptions<IndexPatternField>;
showValidation: boolean;
state: VisState;
state: EditorVisState;
value?: T;
metricAggs: IAggConfig[];
schemas: Schema[];

View file

@ -20,7 +20,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { IndexPattern, IAggConfig, AggGroupNames } from 'src/plugins/data/public';
import {
DefaultEditorAggParams as PureDefaultEditorAggParams,
@ -28,6 +27,7 @@ import {
} from './agg_params';
import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
import { dataPluginMock } from '../../../../../plugins/data/public/mocks';
import { EditorVisState } from './sidebar/state/reducers';
const mockEditorConfig = {
useNormalizedEsInterval: { hidden: false, fixedValue: false },
@ -108,7 +108,7 @@ describe('DefaultEditorAggParams component', () => {
formIsTouched: false,
indexPattern: {} as IndexPattern,
metricAggs: [],
state: {} as VisState,
state: {} as EditorVisState,
setAggParamValue,
onAggTypeChange,
setTouched,

View file

@ -25,7 +25,6 @@ import {
IndexPattern,
IndexPatternField,
} from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import {
getAggParamsToRender,
getAggTypeOptions,
@ -34,6 +33,7 @@ import {
import { FieldParamEditor, OrderByParamEditor } from './controls';
import { EditorConfig } from './utils';
import { Schema } from '../schemas';
import { EditorVisState } from './sidebar/state/reducers';
jest.mock('../utils', () => ({
groupAndSortBy: jest.fn(() => ['indexedFields']),
@ -58,7 +58,7 @@ describe('DefaultEditorAggParams helpers', () => {
hideCustomLabel: true,
} as Schema,
];
const state = {} as VisState;
const state = {} as EditorVisState;
const metricAggs: IAggConfig[] = [];
const emptyParams = {
basic: [],

View file

@ -28,7 +28,6 @@ import {
IndexPattern,
IndexPatternField,
} from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils';
import { AggTypeState, AggParamsState } from './agg_params_state';
import { AggParamEditorProps } from './agg_param_props';
@ -36,12 +35,13 @@ import { aggParamsMap } from './agg_params_map';
import { EditorConfig } from './utils';
import { Schema, getSchemaByName } from '../schemas';
import { search } from '../../../../../plugins/data/public';
import { EditorVisState } from './sidebar/state/reducers';
interface ParamInstanceBase {
agg: IAggConfig;
editorConfig: EditorConfig;
metricAggs: IAggConfig[];
state: VisState;
state: EditorVisState;
schemas: Schema[];
hideCustomLabel?: boolean;
}

View file

@ -23,9 +23,9 @@ import { mount, shallow, ReactWrapper } from 'enzyme';
import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui';
import { IAggConfig, IndexPatternField } from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { ComboBoxGroupedOptions } from '../../utils';
import { FieldParamEditor, FieldParamEditorProps } from './field';
import { EditorVisState } from '../sidebar/state/reducers';
function callComboBoxOnChange(comp: ReactWrapper, value: any = []) {
const comboBoxProps = comp.find(EuiComboBox).props() as EuiComboBoxProps<any>;
@ -78,7 +78,7 @@ describe('FieldParamEditor component', () => {
setValue,
setValidity,
setTouched,
state: {} as VisState,
state: {} as EditorVisState,
metricAggs: [] as IAggConfig[],
schemas: [],
};

View file

@ -20,9 +20,9 @@
import React from 'react';
import { AggParamEditorProps } from '../agg_param_props';
import { IAggConfig } from 'src/plugins/data/public';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { mount } from 'enzyme';
import { PercentilesEditor } from './percentiles';
import { EditorVisState } from '../sidebar/state/reducers';
describe('PercentilesEditor component', () => {
let setValue: jest.Mock;
@ -45,7 +45,7 @@ describe('PercentilesEditor component', () => {
setValue,
setValidity,
setTouched,
state: {} as VisState,
state: {} as EditorVisState,
metricAggs: [] as IAggConfig[],
schemas: [],
};

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import { IAggConfig, AggParam } from 'src/plugins/data/public';
import { EditorConfig } from '../utils';
import { EditorVisState } from '../sidebar/state/reducers';
export const aggParamCommonPropsMock = {
agg: {} as IAggConfig,
@ -27,7 +27,7 @@ export const aggParamCommonPropsMock = {
editorConfig: {} as EditorConfig,
formIsTouched: false,
metricAggs: [] as IAggConfig[],
state: {} as VisState,
state: {} as EditorVisState,
showValidation: false,
schemas: [],
};

View file

@ -21,7 +21,6 @@ import React, { useMemo, useCallback } from 'react';
import { findLast } from 'lodash';
import { EuiSpacer } from '@elastic/eui';
import { VisState } from 'src/legacy/core_plugins/visualizations/public';
import {
AggGroupNames,
IAggConfig,
@ -40,6 +39,7 @@ import {
} from './state';
import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from '../agg_common_props';
import { ISchemas } from '../../schemas';
import { EditorVisState } from './state/reducers';
export interface DefaultEditorDataTabProps {
dispatch: React.Dispatch<EditorAction>;
@ -47,7 +47,7 @@ export interface DefaultEditorDataTabProps {
isTabSelected: boolean;
metricAggs: IAggConfig[];
schemas: ISchemas;
state: VisState;
state: EditorVisState;
setTouched(isTouched: boolean): void;
setValidity(modelName: string, value: boolean): void;
setStateValue: DefaultEditorAggCommonProps['setStateParamValue'];

View file

@ -21,6 +21,7 @@ import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect
import { get, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EventEmitter } from 'events';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { DefaultEditorNavBar, OptionTab } from './navbar';
@ -40,6 +41,7 @@ interface DefaultEditorSideBarProps {
uiState: PersistedState;
vis: Vis;
isLinkedSearch: boolean;
eventEmitter: EventEmitter;
savedSearch?: SavedSearch;
}
@ -50,14 +52,17 @@ function DefaultEditorSideBar({
uiState,
vis,
isLinkedSearch,
eventEmitter,
savedSearch,
}: DefaultEditorSideBarProps) {
const [selectedTab, setSelectedTab] = useState(optionTabs[0].name);
const [isDirty, setDirty] = useState(false);
const [state, dispatch] = useEditorReducer(vis);
const [state, dispatch] = useEditorReducer(vis, eventEmitter);
const { formState, setTouched, setValidity, resetValidity } = useEditorFormState();
const responseAggs = useMemo(() => state.aggs.getResponseAggs(), [state.aggs]);
const responseAggs = useMemo(() => (state.data.aggs ? state.data.aggs.getResponseAggs() : []), [
state.data.aggs,
]);
const metricSchemas = getSchemasByGroup(vis.type.schemas.all || [], AggGroupNames.Metrics).map(
s => s.name
);
@ -90,17 +95,20 @@ function DefaultEditorSideBar({
const applyChanges = useCallback(() => {
if (formState.invalid || !isDirty) {
setTouched(true);
return;
}
vis.setCurrentState(state);
vis.updateState();
vis.emit('dirtyStateChange', {
vis.setState({
...vis.serialize(),
params: state.params,
data: { aggs: state.data.aggs ? (state.data.aggs.aggs.map(agg => agg.toJSON()) as any) : [] },
});
eventEmitter.emit('updateVis');
eventEmitter.emit('dirtyStateChange', {
isDirty: false,
});
setTouched(false);
}, [vis, state, formState.invalid, setTouched, isDirty]);
}, [vis, state, formState.invalid, setTouched, isDirty, eventEmitter]);
const onSubmit: KeyboardEventHandler<HTMLFormElement> = useCallback(
event => {
@ -122,18 +130,22 @@ function DefaultEditorSideBar({
resetValidity();
}
};
vis.on('dirtyStateChange', changeHandler);
eventEmitter.on('dirtyStateChange', changeHandler);
return () => vis.off('dirtyStateChange', changeHandler);
}, [resetValidity, vis]);
return () => {
eventEmitter.off('dirtyStateChange', changeHandler);
};
}, [resetValidity, eventEmitter]);
// subscribe on external vis changes using browser history, for example press back button
useEffect(() => {
const resetHandler = () => dispatch(discardChanges(vis));
vis.on('updateEditor', resetHandler);
eventEmitter.on('updateEditor', resetHandler);
return () => vis.off('updateEditor', resetHandler);
}, [dispatch, vis]);
return () => {
eventEmitter.off('updateEditor', resetHandler);
};
}, [dispatch, vis, eventEmitter]);
const dataTabProps = {
dispatch,
@ -147,7 +159,7 @@ function DefaultEditorSideBar({
};
const optionTabProps = {
aggs: state.aggs,
aggs: state.data.aggs!,
hasHistogramAgg,
stateParams: state.params,
vis,
@ -173,7 +185,12 @@ function DefaultEditorSideBar({
onKeyDownCapture={onSubmit}
>
{vis.type.requiresSearch && (
<SidebarTitle isLinkedSearch={isLinkedSearch} savedSearch={savedSearch} vis={vis} />
<SidebarTitle
isLinkedSearch={isLinkedSearch}
savedSearch={savedSearch}
vis={vis}
eventEmitter={eventEmitter}
/>
)}
{optionTabs.length > 1 && (

View file

@ -18,6 +18,7 @@
*/
import React, { useCallback, useState } from 'react';
import { EventEmitter } from 'events';
import {
EuiButton,
EuiButtonEmpty,
@ -39,23 +40,24 @@ import { SavedSearch } from '../../../../../../plugins/discover/public';
interface LinkedSearchProps {
savedSearch: SavedSearch;
vis: Vis;
eventEmitter: EventEmitter;
}
interface SidebarTitleProps {
isLinkedSearch: boolean;
savedSearch?: SavedSearch;
vis: Vis;
eventEmitter: EventEmitter;
}
export function LinkedSearch({ savedSearch, vis }: LinkedSearchProps) {
export function LinkedSearch({ savedSearch, eventEmitter }: LinkedSearchProps) {
const [showPopover, setShowPopover] = useState(false);
const closePopover = useCallback(() => setShowPopover(false), []);
const onClickButtonLink = useCallback(() => setShowPopover(v => !v), []);
const onClickUnlikFromSavedSearch = useCallback(() => {
setShowPopover(false);
vis.emit('unlinkFromSavedSearch');
}, [vis]);
eventEmitter.emit('unlinkFromSavedSearch');
}, [eventEmitter]);
const linkButtonAriaLabel = i18n.translate(
'visDefaultEditor.sidebar.savedSearch.linkButtonAriaLabel',
@ -151,20 +153,20 @@ export function LinkedSearch({ savedSearch, vis }: LinkedSearchProps) {
);
}
function SidebarTitle({ savedSearch, vis, isLinkedSearch }: SidebarTitleProps) {
function SidebarTitle({ savedSearch, vis, isLinkedSearch, eventEmitter }: SidebarTitleProps) {
return isLinkedSearch && savedSearch ? (
<LinkedSearch savedSearch={savedSearch} vis={vis} />
<LinkedSearch savedSearch={savedSearch} eventEmitter={eventEmitter} />
) : vis.type.options.showIndexSelection ? (
<EuiTitle size="xs" className="visEditorSidebar__titleContainer eui-textTruncate">
<h2
title={i18n.translate('visDefaultEditor.sidebar.indexPatternAriaLabel', {
defaultMessage: 'Index pattern: {title}',
values: {
title: vis.indexPattern.title,
title: vis.data.indexPattern!.title,
},
})}
>
{vis.indexPattern.title}
{vis.data.indexPattern!.title}
</h2>
</EuiTitle>
) : (

View file

@ -17,20 +17,23 @@
* under the License.
*/
import { useEffect, useReducer, useCallback } from 'react';
import { isEqual } from 'lodash';
import { useReducer, useCallback } from 'react';
import { EventEmitter } from 'events';
import { Vis, VisState, VisParams } from 'src/legacy/core_plugins/visualizations/public';
import { createEditorStateReducer, initEditorState } from './reducers';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { createEditorStateReducer, initEditorState, EditorVisState } from './reducers';
import { EditorStateActionTypes } from './constants';
import { EditorAction, updateStateParams } from './actions';
import { EditorAction } from './actions';
import { useKibana } from '../../../../../../../plugins/kibana_react/public';
import { VisDefaultEditorKibanaServices } from '../../../types';
export * from './editor_form_state';
export * from './actions';
export function useEditorReducer(vis: Vis): [VisState, React.Dispatch<EditorAction>] {
export function useEditorReducer(
vis: Vis,
eventEmitter: EventEmitter
): [EditorVisState, React.Dispatch<EditorAction>] {
const { services } = useKibana<VisDefaultEditorKibanaServices>();
const [state, dispatch] = useReducer(
createEditorStateReducer(services.data.search),
@ -38,28 +41,15 @@ export function useEditorReducer(vis: Vis): [VisState, React.Dispatch<EditorActi
initEditorState
);
useEffect(() => {
const handleVisUpdate = (params: VisParams) => {
if (!isEqual(params, state.params)) {
dispatch(updateStateParams(params));
}
};
// fires when visualization state changes, and we need to copy changes to editorState
vis.on('updateEditorStateParams', handleVisUpdate);
return () => vis.off('updateEditorStateParams', handleVisUpdate);
}, [vis, state.params]);
const wrappedDispatch = useCallback(
(action: EditorAction) => {
dispatch(action);
vis.emit('dirtyStateChange', {
eventEmitter.emit('dirtyStateChange', {
isDirty: action.type !== EditorStateActionTypes.DISCARD_CHANGES,
});
},
[vis]
[eventEmitter]
);
return [state, wrappedDispatch];

View file

@ -19,35 +19,45 @@
import { cloneDeep } from 'lodash';
import { Vis, VisState } from 'src/legacy/core_plugins/visualizations/public';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { AggGroupNames, DataPublicPluginStart } from '../../../../../../../plugins/data/public';
import { EditorStateActionTypes } from './constants';
import { getEnabledMetricAggsCount } from '../../agg_group_helper';
import { EditorAction } from './actions';
function initEditorState(vis: Vis) {
return vis.copyCurrentState(true);
return {
...vis.clone(),
};
}
export type EditorVisState = Pick<Vis, 'title' | 'description' | 'type' | 'params' | 'data'>;
const createEditorStateReducer = ({
aggs: { createAggConfigs },
}: DataPublicPluginStart['search']) => (state: VisState, action: EditorAction): VisState => {
}: DataPublicPluginStart['search']) => (
state: EditorVisState,
action: EditorAction
): EditorVisState => {
switch (action.type) {
case EditorStateActionTypes.ADD_NEW_AGG: {
const { schema } = action.payload;
const defaultConfig =
!state.aggs.aggs.find(agg => agg.schema === schema.name) && schema.defaults
!state.data.aggs!.aggs.find(agg => agg.schema === schema.name) && schema.defaults
? (schema as any).defaults.slice(0, schema.max)
: { schema: schema.name };
const aggConfig = state.aggs.createAggConfig(defaultConfig, {
const aggConfig = state.data.aggs!.createAggConfig(defaultConfig, {
addToAggConfigs: false,
});
aggConfig.brandNew = true;
const newAggs = [...state.aggs.aggs, aggConfig];
const newAggs = [...state.data.aggs!.aggs, aggConfig];
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}
@ -58,7 +68,7 @@ const createEditorStateReducer = ({
case EditorStateActionTypes.CHANGE_AGG_TYPE: {
const { aggId, value } = action.payload;
const newAggs = state.aggs.aggs.map(agg => {
const newAggs = state.data.aggs!.aggs.map(agg => {
if (agg.id === aggId) {
agg.type = value;
@ -70,14 +80,17 @@ const createEditorStateReducer = ({
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}
case EditorStateActionTypes.SET_AGG_PARAM_VALUE: {
const { aggId, paramName, value } = action.payload;
const newAggs = state.aggs.aggs.map(agg => {
const newAggs = state.data.aggs!.aggs.map(agg => {
if (agg.id === aggId) {
const parsedAgg = agg.toJSON();
@ -95,7 +108,10 @@ const createEditorStateReducer = ({
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}
@ -113,7 +129,7 @@ const createEditorStateReducer = ({
case EditorStateActionTypes.REMOVE_AGG: {
let isMetric = false;
const newAggs = state.aggs.aggs.filter(({ id, schema }) => {
const newAggs = state.data.aggs!.aggs.filter(({ id, schema }) => {
if (id === action.payload.aggId) {
const schemaDef = action.payload.schemas.find(s => s.name === schema);
if (schemaDef && schemaDef.group === AggGroupNames.Metrics) {
@ -136,26 +152,36 @@ const createEditorStateReducer = ({
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}
case EditorStateActionTypes.REORDER_AGGS: {
const { sourceAgg, destinationAgg } = action.payload;
const destinationIndex = state.aggs.aggs.indexOf(destinationAgg);
const newAggs = [...state.aggs.aggs];
newAggs.splice(destinationIndex, 0, newAggs.splice(state.aggs.aggs.indexOf(sourceAgg), 1)[0]);
const destinationIndex = state.data.aggs!.aggs.indexOf(destinationAgg);
const newAggs = [...state.data.aggs!.aggs];
newAggs.splice(
destinationIndex,
0,
newAggs.splice(state.data.aggs!.aggs.indexOf(sourceAgg), 1)[0]
);
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}
case EditorStateActionTypes.TOGGLE_ENABLED_AGG: {
const { aggId, enabled } = action.payload;
const newAggs = state.aggs.aggs.map(agg => {
const newAggs = state.data.aggs!.aggs.map(agg => {
if (agg.id === aggId) {
const parsedAgg = agg.toJSON();
@ -170,7 +196,10 @@ const createEditorStateReducer = ({
return {
...state,
aggs: createAggConfigs(state.aggs.indexPattern, newAggs),
data: {
...state.data,
aggs: createAggConfigs(state.data.indexPattern!, newAggs),
},
};
}

View file

@ -20,10 +20,6 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { EditorRenderProps } from '../../kibana/public/visualize/np_ready/types';
import {
VisualizeEmbeddableContract as VisualizeEmbeddable,
VisualizeEmbeddableFactoryContract as VisualizeEmbeddableFactory,
} from '../../visualizations/public/';
import { PanelsContainer, Panel } from '../../../../plugins/kibana_react/public';
import './vis_type_agg_filter';
@ -32,68 +28,44 @@ import { DefaultEditorControllerState } from './default_editor_controller';
import { getInitialWidth } from './editor_size';
function DefaultEditor({
embeddable,
savedObj,
vis,
uiState,
timeRange,
filters,
appState,
optionTabs,
query,
embeddableHandler,
eventEmitter,
linked,
savedSearch,
}: DefaultEditorControllerState & Omit<EditorRenderProps, 'data' | 'core'>) {
const visRef = useRef<HTMLDivElement>(null);
const visHandler = useRef<VisualizeEmbeddable | null>(null);
const [isCollapsed, setIsCollapsed] = useState(false);
const [factory, setFactory] = useState<VisualizeEmbeddableFactory | null>(null);
const { vis, savedSearch } = savedObj;
const onClickCollapse = useCallback(() => {
setIsCollapsed(value => !value);
}, []);
useEffect(() => {
async function visualize() {
if (!visRef.current || (!visHandler.current && factory)) {
return;
}
if (!visHandler.current) {
const embeddableFactory = embeddable.getEmbeddableFactory(
'visualization'
) as VisualizeEmbeddableFactory;
setFactory(embeddableFactory);
visHandler.current = (await embeddableFactory.createFromObject(savedObj, {
// should be look through createFromObject interface again because of "id" param
id: '',
uiState,
appState,
timeRange,
filters,
query,
})) as VisualizeEmbeddable;
visHandler.current.render(visRef.current);
} else {
visHandler.current.updateInput({
timeRange,
filters,
query,
});
}
if (!visRef.current) {
return;
}
visualize();
}, [uiState, savedObj, timeRange, filters, appState, query, factory, embeddable]);
embeddableHandler.render(visRef.current);
setTimeout(() => {
eventEmitter.emit('apply');
});
return () => embeddableHandler.destroy();
}, [embeddableHandler, eventEmitter]);
useEffect(() => {
return () => {
if (visHandler.current) {
visHandler.current.destroy();
}
};
}, []);
embeddableHandler.updateInput({
timeRange,
filters,
query,
});
}, [embeddableHandler, timeRange, filters, query]);
const editorInitialWidth = getInitialWidth(vis.type.editorConfig.defaultSize);
@ -120,6 +92,7 @@ function DefaultEditor({
uiState={uiState}
isLinkedSearch={linked}
savedSearch={savedSearch}
eventEmitter={eventEmitter}
/>
</Panel>
</PanelsContainer>

View file

@ -21,18 +21,22 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { EventEmitter } from 'events';
import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types';
import { VisSavedObject } from 'src/legacy/core_plugins/visualizations/public/';
import { Vis } from 'src/legacy/core_plugins/visualizations/public/';
import { Storage } from '../../../../plugins/kibana_utils/public';
import { KibanaContextProvider } from '../../../../plugins/kibana_react/public';
import { DefaultEditor } from './default_editor';
import { DefaultEditorDataTab, OptionTab } from './components/sidebar';
import { VisualizeEmbeddable } from '../../visualizations/public/np_ready/public/embeddable';
const localStorage = new Storage(window.localStorage);
export interface DefaultEditorControllerState {
savedObj: VisSavedObject;
vis: Vis;
eventEmitter: EventEmitter;
embeddableHandler: VisualizeEmbeddable;
optionTabs: OptionTab[];
}
@ -40,9 +44,9 @@ class DefaultEditorController {
private el: HTMLElement;
private state: DefaultEditorControllerState;
constructor(el: HTMLElement, savedObj: VisSavedObject) {
constructor(el: HTMLElement, vis: Vis, eventEmitter: EventEmitter, embeddableHandler: any) {
this.el = el;
const { type: visType } = savedObj.vis;
const { type: visType } = vis;
const optionTabs = [
...(visType.schemas.buckets || visType.schemas.metrics
@ -71,8 +75,10 @@ class DefaultEditorController {
];
this.state = {
savedObj,
vis,
optionTabs,
eventEmitter,
embeddableHandler,
};
}

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { PersistedState } from 'src/plugins/visualizations/public';
import { IAggConfigs } from 'src/plugins/data/public';
import { PersistedState } from '../../../../plugins/visualizations/public';
import { Vis } from '../../visualizations/public';
export interface VisOptionsProps<VisParamType = unknown> {

View file

@ -20,12 +20,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { MetricVisComponent, MetricVisComponentProps } from './metric_vis_component';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { npStart } from 'ui/new_platform';
import { fieldFormats } from '../../../../../plugins/data/public';
import { identity } from 'lodash';
import { ExprVis } from '../../../visualizations/public/np_ready/public/expressions/vis';
jest.mock('ui/new_platform');
@ -37,7 +37,7 @@ const baseVisData = {
} as any;
describe('MetricVisComponent', function() {
const vis: Vis = {
const vis: ExprVis = {
params: {
metric: {
colorSchema: 'Green to Red',
@ -57,7 +57,7 @@ describe('MetricVisComponent', function() {
const getComponent = (propOverrides: Partial<Props> = {} as Partial<Props>) => {
const props: Props = {
vis,
visParams: vis.params,
visParams: vis.params as any,
visData: baseVisData,
renderComplete: jest.fn(),
...propOverrides,

View file

@ -27,12 +27,13 @@ import { FieldFormatsContentType, IFieldFormat } from '../../../../../plugins/da
import { KibanaDatatable } from '../../../../../plugins/expressions/public';
import { getHeatmapColors } from '../../../../../plugins/charts/public';
import { VisParams, MetricVisMetric } from '../types';
import { SchemaConfig, Vis } from '../../../visualizations/public';
import { SchemaConfig } from '../../../visualizations/public';
import { ExprVis } from '../../../visualizations/public/np_ready/public/expressions/vis';
export interface MetricVisComponentProps {
visParams: VisParams;
visData: Input;
vis: Vis;
vis: ExprVis;
renderComplete: () => void;
}

View file

@ -61,11 +61,22 @@ describe('metric_vis - createMetricVisTypeDefinition', () => {
labelTemplate: 'ip[{{value}}]',
});
const searchSource = {
getField: (name: string) => {
if (name === 'index') {
return stubIndexPattern;
}
},
};
// TODO: remove when Vis is converted to typescript. Only importing Vis as type
// @ts-ignore
vis = visualizationsStart.createVis(stubIndexPattern, {
vis = visualizationsStart.createVis('metric', {
type: 'metric',
aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }],
data: {
searchSource,
aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }],
},
});
vis.params.dimensions = {

View file

@ -50,57 +50,73 @@ describe('Table Vis - AggTable Directive', function() {
const tabifiedData = {};
const init = () => {
const vis1 = visualizationsStart.createVis(indexPattern, 'table');
tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly);
const searchSource = {
getField: name => {
if (name === 'index') {
return indexPattern;
}
},
};
const vis1 = visualizationsStart.createVis('table', {
type: 'table',
data: { searchSource, aggs: [] },
});
tabifiedData.metricOnly = tabifyAggResponse(vis1.data.aggs, metricOnly);
const vis2 = visualizationsStart.createVis(indexPattern, {
const vis2 = visualizationsStart.createVis('table', {
type: 'table',
params: {
showMetricsAtAllLevels: true,
},
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{ type: 'terms', schema: 'bucket', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'bucket', params: { field: 'machine.os' } },
],
data: {
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{ type: 'terms', schema: 'bucket', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'bucket', params: { field: 'machine.os' } },
],
searchSource,
},
});
vis2.aggs.aggs.forEach(function(agg, i) {
vis2.data.aggs.aggs.forEach(function(agg, i) {
agg.id = 'agg_' + (i + 1);
});
tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets, {
tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.data.aggs, threeTermBuckets, {
metricsAtAllLevels: true,
});
const vis3 = visualizationsStart.createVis(indexPattern, {
const vis3 = visualizationsStart.createVis('table', {
type: 'table',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'min', schema: 'metric', params: { field: '@timestamp' } },
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{
type: 'date_histogram',
schema: 'bucket',
params: { field: '@timestamp', interval: 'd' },
},
{
type: 'derivative',
schema: 'metric',
params: { metricAgg: 'custom', customMetric: { id: '5-orderAgg', type: 'count' } },
},
{
type: 'top_hits',
schema: 'metric',
params: { field: 'bytes', aggregate: { val: 'min' }, size: 1 },
},
],
data: {
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'min', schema: 'metric', params: { field: '@timestamp' } },
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{
type: 'date_histogram',
schema: 'bucket',
params: { field: '@timestamp', interval: 'd' },
},
{
type: 'derivative',
schema: 'metric',
params: { metricAgg: 'custom', customMetric: { id: '5-orderAgg', type: 'count' } },
},
{
type: 'top_hits',
schema: 'metric',
params: { field: 'bytes', aggregate: { val: 'min' }, size: 1 },
},
],
searchSource,
},
});
vis3.aggs.aggs.forEach(function(agg, i) {
vis3.data.aggs.aggs.forEach(function(agg, i) {
agg.id = 'agg_' + (i + 1);
});
tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = tabifyAggResponse(
vis3.aggs,
vis3.data.aggs,
oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative
);
};

View file

@ -38,22 +38,35 @@ describe('Table Vis - AggTableGroup Directive', function() {
const tabifiedData = {};
const init = () => {
const vis1 = visualizationsStart.createVis(indexPattern, 'table');
tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly);
const vis2 = visualizationsStart.createVis(indexPattern, {
type: 'pie',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'split', params: { field: 'extension' } },
{ type: 'terms', schema: 'segment', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'segment', params: { field: 'machine.os' } },
],
const searchSource = {
getField: name => {
if (name === 'index') {
return indexPattern;
}
},
};
const vis1 = visualizationsStart.createVis('table', {
type: 'table',
data: { searchSource, aggs: [] },
});
vis2.aggs.aggs.forEach(function(agg, i) {
tabifiedData.metricOnly = tabifyAggResponse(vis1.data.aggs, metricOnly);
const vis2 = visualizationsStart.createVis('pie', {
type: 'pie',
data: {
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'split', params: { field: 'extension' } },
{ type: 'terms', schema: 'segment', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'segment', params: { field: 'machine.os' } },
],
searchSource,
},
});
vis2.data.aggs.aggs.forEach(function(agg, i) {
agg.id = 'agg_' + (i + 1);
});
tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, threeTermBuckets);
tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.data.aggs, threeTermBuckets);
};
const initLocalAngular = () => {

View file

@ -118,20 +118,22 @@ describe('Table Vis - Controller', () => {
return ({
type: tableVisTypeDefinition,
params: Object.assign({}, tableVisTypeDefinition.visConfig.defaults, params),
aggs: createAggConfigs(stubIndexPattern, [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 },
],
data: {
aggs: createAggConfigs(stubIndexPattern, [
{ type: 'count', schema: 'metric' },
{
type: 'range',
schema: 'bucket',
params: {
field: 'bytes',
ranges: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 },
],
},
},
},
]),
]),
},
} as unknown) as Vis;
}
@ -151,11 +153,11 @@ describe('Table Vis - Controller', () => {
// basically a parameterized beforeEach
function initController(vis: Vis) {
vis.aggs.aggs.forEach((agg: IAggConfig, i: number) => {
vis.data.aggs!.aggs.forEach((agg: IAggConfig, i: number) => {
agg.id = 'agg_' + (i + 1);
});
tabifiedResponse = tabifyAggResponse(vis.aggs, oneRangeBucket);
tabifiedResponse = tabifyAggResponse(vis.data.aggs!, oneRangeBucket);
$rootScope.vis = vis;
$rootScope.visParams = vis.params;
$rootScope.uiState = {

View file

@ -19,12 +19,12 @@
import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular';
import $ from 'jquery';
import { isEqual } from 'lodash';
import { Vis, VisParams } from '../../visualizations/public';
import { VisParams } from '../../visualizations/public';
import { npStart } from './legacy_imports';
import { getAngularModule } from './get_inner_angular';
import { initTableVisLegacyModule } from './table_vis_legacy_module';
import { ExprVis } from '../../visualizations/public/np_ready/public/expressions/vis';
const innerAngularName = 'kibana/table_vis';
@ -32,12 +32,12 @@ export class TableVisualizationController {
private tableVisModule: IModule | undefined;
private injector: auto.IInjectorService | undefined;
el: JQuery<Element>;
vis: Vis;
vis: ExprVis;
$rootScope: IRootScopeService | null = null;
$scope: (IScope & { [key: string]: any }) | undefined;
$compile: ICompileService | undefined;
constructor(domeElement: Element, vis: Vis) {
constructor(domeElement: Element, vis: ExprVis) {
this.el = $(domeElement);
this.vis = vis;
}
@ -60,7 +60,7 @@ export class TableVisualizationController {
}
}
async render(esResponse: object, visParams: VisParams, status: { [key: string]: boolean }) {
async render(esResponse: object, visParams: VisParams) {
this.initLocalAngular();
return new Promise(async (resolve, reject) => {
@ -77,15 +77,10 @@ export class TableVisualizationController {
this.$scope.visState = { params: visParams };
this.$scope.esResponse = esResponse;
if (!isEqual(this.$scope.visParams, visParams)) {
this.vis.emit('updateEditorStateParams', visParams);
}
this.$scope.visParams = visParams;
this.$scope.renderComplete = resolve;
this.$scope.renderFailed = reject;
this.$scope.resize = Date.now();
this.$scope.updateStatus = status;
this.$scope.$apply();
};
@ -93,7 +88,7 @@ export class TableVisualizationController {
this.$scope = this.$rootScope.$new();
this.$scope.uiState = this.vis.getUiState();
updateScope();
this.el.find('div').append(this.$compile(this.vis.type.visConfig.template)(this.$scope));
this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope));
this.$scope.$apply();
} else {
updateScope();

View file

@ -19,7 +19,6 @@
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import { start as visualizationsStart } from '../../../../../core_plugins/visualizations/public/np_ready/public/legacy';
import { ImageComparator } from 'test_utils/image_comparator';
import { createTagCloudVisualization } from '../tag_cloud_visualization';
@ -36,7 +35,6 @@ const PIXEL_DIFF = 64;
describe('TagCloudVisualizationTest', function() {
let domNode;
let indexPattern;
let vis;
let imageComparator;
@ -66,22 +64,18 @@ describe('TagCloudVisualizationTest', function() {
});
beforeEach(ngMock.module('kibana'));
beforeEach(
ngMock.inject(Private => {
indexPattern = Private(LogstashIndexPatternStubProvider);
})
);
describe('TagCloudVisualization - basics', function() {
beforeEach(async function() {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = visualizationsStart.createVis(indexPattern, {
vis = visualizationsStart.createVis('tagcloud', {
type: 'tagcloud',
params: {
bucket: { accessor: 0, format: {} },
metric: { accessor: 0, format: {} },
},
data: {},
});
});

View file

@ -79,17 +79,10 @@ export function createTagCloudVisualization({ colors }) {
render(<Label ref={this._label} />, this._labelNode);
}
async render(data, visParams, status) {
if (!(status.resize || status.data || status.params)) return;
if (status.params || status.data) {
this._updateParams(visParams);
this._updateData(data);
}
if (status.resize) {
this._resize();
}
async render(data, visParams) {
this._updateParams(visParams);
this._updateData(data);
this._resize();
await this._renderComplete$.pipe(take(1)).toPromise();

View file

@ -20,7 +20,6 @@
import { i18n } from '@kbn/i18n';
import { Schemas } from '../../vis_default_editor/public';
import { Status } from '../../visualizations/public';
import { TagCloudOptions } from './components/tag_cloud_options';
@ -44,7 +43,6 @@ export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) =
showLabel: true,
},
},
requiresUpdateStatus: [Status.PARAMS, Status.RESIZE, Status.DATA],
visualization: createTagCloudVisualization(deps),
editorConfig: {
collections: {

View file

@ -20,16 +20,16 @@
import React from 'react';
import { IUiSettingsClient } from 'kibana/public';
import { Vis } from 'src/legacy/core_plugins/visualizations/public';
import { ChartComponent } from './chart';
import { VisParams } from '../timelion_vis_fn';
import { TimelionSuccessResponse } from '../helpers/timelion_request_handler';
import { ExprVis } from '../../../visualizations/public/np_ready/public/expressions/vis';
export interface TimelionVisComponentProp {
config: IUiSettingsClient;
renderComplete(): void;
updateStatus: object;
vis: Vis;
vis: ExprVis;
visData: TimelionSuccessResponse;
visParams: VisParams;
}

View file

@ -65,7 +65,7 @@ export class VisEditor extends Component {
}
get uiState() {
return this.props.vis.getUiState();
return this.props.vis.uiState;
}
getConfig = (...args) => {
@ -73,17 +73,15 @@ export class VisEditor extends Component {
};
handleUiState = (field, value) => {
this.props.vis.uiStateVal(field, value);
this.props.vis.uiState.set(field, value);
};
updateVisState = debounce(() => {
this.props.vis.params = this.state.model;
this.props.vis.updateState();
// This check should be redundant, since this method should only be called when we're in editor
// mode where there's also an appState passed into us.
if (this.props.appState) {
this.props.appState.save();
}
this.props.eventEmitter.emit('updateVis');
this.props.eventEmitter.emit('dirtyStateChange', {
isDirty: false,
});
}, VIS_STATE_DEBOUNCE_DELAY);
isValidKueryQuery = filterQuery => {
@ -184,7 +182,8 @@ export class VisEditor extends Component {
dirty={this.state.dirty}
autoApply={this.state.autoApply}
model={model}
savedObj={this.props.savedObj}
embeddableHandler={this.props.embeddableHandler}
vis={this.props.vis}
timeRange={this.props.timeRange}
uiState={this.uiState}
onCommit={this.handleCommit}

View file

@ -29,7 +29,6 @@ import {
AUTO_INTERVAL,
} from './lib/get_interval';
import { PANEL_TYPES } from '../../../../../plugins/vis_type_timeseries/common/panel_types';
import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy';
const MIN_CHART_HEIGHT = 300;
@ -70,15 +69,9 @@ class VisEditorVisualizationUI extends Component {
return;
}
const { timeRange, savedObj, onDataChange } = this.props;
const { onDataChange, embeddableHandler } = this.props;
this._handler = await embeddables
.getEmbeddableFactory('visualization')
.createFromObject(savedObj, {
vis: {},
timeRange: timeRange,
filters: [],
});
this._handler = embeddableHandler;
await this._handler.render(this._visEl.current);
this._subscription = this._handler.handler.data$.subscribe(data => {
@ -285,7 +278,7 @@ VisEditorVisualizationUI.propTypes = {
onCommit: PropTypes.func,
uiState: PropTypes.object,
onToggleAutoApply: PropTypes.func,
savedObj: PropTypes.object,
embeddableHandler: PropTypes.object,
timeRange: PropTypes.object,
dirty: PropTypes.bool,
autoApply: PropTypes.bool,

View file

@ -23,12 +23,15 @@ import { fetchIndexPatternFields } from './lib/fetch_fields';
import { getSavedObjectsClient, getUISettings, getI18n } from './services';
export class EditorController {
constructor(el, savedObj) {
constructor(el, vis, eventEmitter, embeddableHandler) {
this.el = el;
this.embeddableHandler = embeddableHandler;
this.eventEmitter = eventEmitter;
this.state = {
savedObj: savedObj,
vis: savedObj.vis,
fields: [],
vis: vis,
isLoaded: false,
};
}
@ -47,7 +50,7 @@ export class EditorController {
this.state.vis.params.default_index_pattern = title;
this.state.vis.params.default_timefield = timeFieldName;
this.state.vis.fields = await fetchIndexPatternFields(this.state.vis);
this.state.fields = await fetchIndexPatternFields(this.state.vis);
this.state.isLoaded = true;
};
@ -67,13 +70,14 @@ export class EditorController {
<Component
config={getUISettings()}
vis={this.state.vis}
visFields={this.state.vis.fields}
visFields={this.state.fields}
visParams={this.state.vis.params}
savedObj={this.state.savedObj}
timeRange={params.timeRange}
renderComplete={() => {}}
isEditorMode={true}
appState={params.appState}
embeddableHandler={this.embeddableHandler}
eventEmitter={this.eventEmitter}
/>
</I18nContext>,
this.el

View file

@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import colorJS from 'color';
import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts';

View file

@ -22,7 +22,6 @@ import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import $ from 'jquery';
import { createVegaVisualization } from '../vega_visualization';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import { ImageComparator } from 'test_utils/image_comparator';
import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson';
@ -57,7 +56,6 @@ const PIXEL_DIFF = 30;
describe('VegaVisualizations', () => {
let domNode;
let VegaVisualization;
let indexPattern;
let vis;
let imageComparator;
let vegaVisualizationDependencies;
@ -71,7 +69,7 @@ describe('VegaVisualizations', () => {
beforeEach(ngMock.module('kibana'));
beforeEach(
ngMock.inject((Private, $injector) => {
ngMock.inject($injector => {
vegaVisualizationDependencies = {
serviceSettings: $injector.get('serviceSettings'),
core: {
@ -99,7 +97,6 @@ describe('VegaVisualizations', () => {
}
VegaVisualization = createVegaVisualization(vegaVisualizationDependencies);
indexPattern = Private(LogstashIndexPatternStubProvider);
})
);
@ -108,7 +105,7 @@ describe('VegaVisualizations', () => {
setupDOM('512px', '512px');
imageComparator = new ImageComparator();
vis = visualizationsStart.createVis(indexPattern, { type: 'vega' });
vis = visualizationsStart.createVis('vega', { type: 'vega' });
});
afterEach(function() {

View file

@ -19,7 +19,6 @@
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { Status } from '../../visualizations/public';
import { DefaultEditorSize } from '../../vis_default_editor/public';
import { VegaVisualizationDependencies } from './plugin';
import { VegaVisEditor } from './components';
@ -51,7 +50,6 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen
},
visualization,
requestHandler,
requiresUpdateStatus: [Status.DATA, Status.RESIZE],
responseHandler: 'none',
options: {
showIndexSelection: false,

View file

@ -69,7 +69,7 @@ export const createVegaVisualization = ({ serviceSettings }) =>
* @param {*} status
* @returns {Promise<void>}
*/
async render(visData, visParams, status) {
async render(visData) {
const { toasts } = getNotifications();
if (!visData && !this._vegaView) {
@ -82,7 +82,7 @@ export const createVegaVisualization = ({ serviceSettings }) =>
}
try {
await this._render(visData, status);
await this._render(visData);
} catch (error) {
if (this._vegaView) {
this._vegaView.onError(error);
@ -96,8 +96,8 @@ export const createVegaVisualization = ({ serviceSettings }) =>
}
}
async _render(vegaParser, status) {
if (vegaParser && (status.data || !this._vegaView)) {
async _render(vegaParser) {
if (vegaParser) {
// New data received, rebuild the graph
if (this._vegaView) {
await this._vegaView.destroy();
@ -121,9 +121,6 @@ export const createVegaVisualization = ({ serviceSettings }) =>
this._vegaView = new VegaView(vegaViewParams);
}
await this._vegaView.init();
} else if (status.resize) {
// the graph has been resized
await this._vegaView.resize();
}
}

View file

@ -54,7 +54,7 @@ exports[`MetricsAxisOptions component should init with the default set of props
}
vis={
Object {
"setVisType": [MockFunction],
"setState": [MockFunction],
"type": Object {
"schemas": Object {
"metrics": Array [
@ -126,7 +126,7 @@ exports[`MetricsAxisOptions component should init with the default set of props
}
vis={
Object {
"setVisType": [MockFunction],
"setState": [MockFunction],
"type": Object {
"schemas": Object {
"metrics": Array [
@ -169,7 +169,7 @@ exports[`MetricsAxisOptions component should init with the default set of props
setCategoryAxis={[Function]}
vis={
Object {
"setVisType": [MockFunction],
"setState": [MockFunction],
"type": Object {
"schemas": Object {
"metrics": Array [

View file

@ -94,7 +94,7 @@ describe('MetricsAxisOptions component', () => {
type: ChartTypes.AREA,
schemas: { metrics: [{ name: 'metric' }] },
},
setVisType: jest.fn(),
setState: jest.fn(),
},
stateParams: {
valueAxes: [axis],
@ -145,7 +145,7 @@ describe('MetricsAxisOptions component', () => {
},
});
expect(defaultProps.vis.setVisType).toHaveBeenLastCalledWith(ChartTypes.LINE);
expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.LINE });
});
it('should set histogram visType when multiple seriesParam', () => {
@ -159,7 +159,7 @@ describe('MetricsAxisOptions component', () => {
},
});
expect(defaultProps.vis.setVisType).toHaveBeenLastCalledWith(ChartTypes.HISTOGRAM);
expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.HISTOGRAM });
});
});

View file

@ -299,7 +299,7 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps<BasicVislibParams>)
}, [stateParams.seriesParams]);
useEffect(() => {
vis.setVisType(visType);
vis.setState({ type: visType } as any);
}, [vis, visType]);
return isTabSelected ? (

View file

@ -44,7 +44,9 @@ function PointSeriesOptions(props: ValidationVisOptionsProps<BasicVislibParams>)
<BasicOptions {...props} />
{vis.hasSchemaAgg('segment', 'date_histogram') ? (
{vis.data.aggs!.aggs.some(
agg => agg.schema === 'segment' && agg.type.name === 'date_histogram'
) ? (
<SwitchOption
label={i18n.translate('visTypeVislib.editors.pointSeries.currentTimeMarkerLabel', {
defaultMessage: 'Current time marker',

View file

@ -26,7 +26,8 @@ import { Positions } from './utils/collections';
import { VisTypeVislibDependencies } from './plugin';
import { mountReactNode } from '../../../../core/public/utils';
import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend';
import { VisParams, Vis } from '../../visualizations/public';
import { VisParams } from '../../visualizations/public';
import { ExprVis } from '../../visualizations/public/np_ready/public/expressions/vis';
const legendClassName = {
top: 'visLib--legend-top',
@ -45,7 +46,7 @@ export const createVislibVisController = (deps: VisTypeVislibDependencies) => {
legendEl: HTMLDivElement;
vislibVis: any;
constructor(public el: Element, public vis: Vis) {
constructor(public el: Element, public vis: ExprVis) {
this.el = el;
this.vis = vis;
this.unmount = null;

View file

@ -133,21 +133,30 @@ describe('No global chart settings', function() {
responseHandler = vislibSlicesResponseHandler;
let id1 = 1;
stubVis1 = visualizationsStart.createVis(indexPattern, {
stubVis1 = visualizationsStart.createVis('pie', {
type: 'pie',
aggs: rowAgg,
data: {
aggs: rowAgg,
searchSource: {
getField: name => {
if (name === 'index') {
return indexPattern;
}
},
},
},
});
stubVis1.isHierarchical = () => true;
// We need to set the aggs to a known value.
_.each(stubVis1.aggs.aggs, function(agg) {
_.each(stubVis1.data.aggs.aggs, function(agg) {
agg.id = 'agg_' + id1++;
});
});
beforeEach(async () => {
const table1 = tabifyAggResponse(stubVis1.aggs, threeTermBuckets, {
const table1 = tabifyAggResponse(stubVis1.data.aggs, threeTermBuckets, {
metricsAtAllLevels: true,
});
data1 = await responseHandler(table1, rowAggDimensions);
@ -222,19 +231,28 @@ describe('Vislib PieChart Class Test Suite', function() {
responseHandler = vislibSlicesResponseHandler;
let id = 1;
stubVis = visualizationsStart.createVis(indexPattern, {
stubVis = visualizationsStart.createVis('pie', {
type: 'pie',
aggs: dataAgg,
data: {
aggs: dataAgg,
searchSource: {
getField: name => {
if (name === 'index') {
return indexPattern;
}
},
},
},
});
// We need to set the aggs to a known value.
_.each(stubVis.aggs.aggs, function(agg) {
_.each(stubVis.data.aggs.aggs, function(agg) {
agg.id = 'agg_' + id++;
});
});
beforeEach(async () => {
const table = tabifyAggResponse(stubVis.aggs, threeTermBuckets, {
const table = tabifyAggResponse(stubVis.data.aggs, threeTermBuckets, {
metricsAtAllLevels: true,
});
data = await responseHandler(table, dataDimensions);

View file

@ -23,22 +23,11 @@ import { compact, uniq, map, every, isUndefined } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui';
import { IAggConfig } from '../../../../../../../plugins/data/public';
import { createFiltersFromEvent } from '../../../legacy_imports';
import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models';
import { VisLegendItem } from './legend_item';
import { getPieNames } from './pie_utils';
import { Vis } from '../../../../../visualizations/public';
import { createFiltersFromEvent, tabifyGetColumns } from '../../../legacy_imports';
const getTableAggs = (vis: Vis): IAggConfig[] => {
if (!vis.aggs || !vis.aggs.getResponseAggs) {
return [];
}
const columns = tabifyGetColumns(vis.aggs.getResponseAggs(), !vis.isHierarchical());
return columns.map(c => c.aggConfig);
};
export interface VisLegendProps {
vis: any;
vislibVis: any;
@ -50,7 +39,6 @@ export interface VisLegendProps {
export interface VisLegendState {
open: boolean;
labels: any[];
tableAggs: any[];
filterableLabels: Set<string>;
selectedLabel: string | null;
}
@ -66,7 +54,6 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
this.state = {
open,
labels: [],
tableAggs: [],
filterableLabels: new Set(),
selectedLabel: null,
};
@ -200,7 +187,6 @@ export class VisLegend extends PureComponent<VisLegendProps, VisLegendState> {
this.getColor = this.props.vislibVis.visConfig.data.getColorFunc();
}
this.setState({ tableAggs: getTableAggs(this.props.vis) });
this.setLabels(this.props.visData, vislibVis.visConfigArgs.type);
};

View file

@ -55,7 +55,7 @@ describe('<Visualization/>', () => {
beforeEach(() => {
vis = {
_setUiState: function(uiState) {
setUiState: function(uiState) {
this.uiState = uiState;
},
getUiState: function() {
@ -79,15 +79,6 @@ describe('<Visualization/>', () => {
expect(wrapper.text()).toBe('No results found');
});
it('should display error message when there is a request error that should be shown and no data', () => {
const errorVis = { ...vis, requestError: { message: 'Request error' }, showRequestError: true };
const data = null;
const wrapper = render(
<Visualization vis={errorVis} visData={data} listenOnChange={true} uiState={uiState} />
);
expect(wrapper.text()).toBe('Request error');
});
it('should render chart when data is present', () => {
const wrapper = render(
<Visualization vis={vis} visData={visData} uiState={uiState} listenOnChange={true} />

View file

@ -23,10 +23,9 @@ import { PersistedState } from '../../../../../../../plugins/visualizations/publ
import { memoizeLast } from '../legacy/memoize';
import { VisualizationChart } from './visualization_chart';
import { VisualizationNoResults } from './visualization_noresults';
import { VisualizationRequestError } from './visualization_requesterror';
import { Vis } from '..';
import { ExprVis } from '../expressions/vis';
function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean {
function shouldShowNoResultsMessage(vis: ExprVis, visData: any): boolean {
const requiresSearch = get(vis, 'type.requiresSearch');
const rows: object[] | undefined = get(visData, 'rows');
const isZeroHits = get(visData, 'hits') === 0 || (rows && !rows.length);
@ -35,17 +34,11 @@ function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean {
return Boolean(requiresSearch && isZeroHits && shouldShowMessage);
}
function shouldShowRequestErrorMessage(vis: Vis, visData: any): boolean {
const requestError = get(vis, 'requestError');
const showRequestError = get(vis, 'showRequestError');
return Boolean(!visData && requestError && showRequestError);
}
interface VisualizationProps {
listenOnChange: boolean;
onInit?: () => void;
uiState: PersistedState;
vis: Vis;
vis: ExprVis;
visData: any;
visParams: any;
}
@ -56,20 +49,17 @@ export class Visualization extends React.Component<VisualizationProps> {
constructor(props: VisualizationProps) {
super(props);
props.vis._setUiState(props.uiState);
props.vis.setUiState(props.uiState);
}
public render() {
const { vis, visData, visParams, onInit, uiState, listenOnChange } = this.props;
const noResults = this.showNoResultsMessage(vis, visData);
const requestError = shouldShowRequestErrorMessage(vis, visData);
return (
<div className="visualization">
{requestError ? (
<VisualizationRequestError onInit={onInit} error={vis.requestError} />
) : noResults ? (
{noResults ? (
<VisualizationNoResults onInit={onInit} />
) : (
<VisualizationChart

View file

@ -21,14 +21,14 @@ import React from 'react';
import * as Rx from 'rxjs';
import { debounceTime, filter, share, switchMap } from 'rxjs/operators';
import { PersistedState } from '../../../../../../../plugins/visualizations/public';
import { Vis, VisualizationController } from '../vis';
import { getUpdateStatus } from '../legacy/update_status';
import { VisualizationController } from '../types';
import { ResizeChecker } from '../../../../../../../plugins/kibana_utils/public';
import { ExprVis } from '../expressions/vis';
interface VisualizationChartProps {
onInit?: () => void;
uiState: PersistedState;
vis: Vis;
vis: ExprVis;
visData: any;
visParams: any;
listenOnChange: boolean;
@ -40,10 +40,9 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
private chartDiv = React.createRef<HTMLDivElement>();
private containerDiv = React.createRef<HTMLDivElement>();
private renderSubject: Rx.Subject<{
vis: Vis;
vis: ExprVis;
visParams: any;
visData: any;
container: HTMLElement;
}>;
private renderSubscription: Rx.Subscription;
@ -54,11 +53,9 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
const render$ = this.renderSubject.asObservable().pipe(share());
const success$ = render$.pipe(
filter(
({ vis, visData, container }) => vis && container && (!vis.type.requiresSearch || visData)
),
filter(({ vis, visData }) => vis && (!vis.type.requiresSearch || visData)),
debounceTime(100),
switchMap(async ({ vis, visData, visParams, container }) => {
switchMap(async ({ vis, visData, visParams }) => {
if (!this.visualization) {
// This should never happen, since we only should trigger another rendering
// after this component has mounted and thus the visualization implementation
@ -66,15 +63,11 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
throw new Error('Visualization implementation was not initialized on first render.');
}
vis.size = [container.clientWidth, container.clientHeight];
const status = getUpdateStatus(vis.type.requiresUpdateStatus, this, this.props);
return this.visualization.render(visData, visParams, status);
return this.visualization.render(visData, visParams);
})
);
const requestError$ = render$.pipe(filter(({ vis }) => vis.requestError));
this.renderSubscription = Rx.merge(success$, requestError$).subscribe(() => {
this.renderSubscription = success$.subscribe(() => {
if (this.props.onInit) {
this.props.onInit();
}
@ -145,7 +138,6 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
vis: this.props.vis,
visData: this.props.visData,
visParams: this.props.visParams,
container: this.containerDiv.current,
});
}
}

View file

@ -28,18 +28,18 @@ import { getUISettings, getSavedObjects } from '../services';
export async function getIndexPattern(
savedVis: VisSavedObject
): Promise<IIndexPattern | undefined> {
if (savedVis.vis.type.name !== 'metrics') {
return savedVis.vis.indexPattern;
if (savedVis.visState.type !== 'metrics') {
return savedVis.searchSource!.getField('index');
}
const savedObjectsClient = getSavedObjects().client;
const defaultIndex = getUISettings().get('defaultIndex');
if (savedVis.vis.params.index_pattern) {
if (savedVis.visState.params.index_pattern) {
const indexPatternObjects = await savedObjectsClient.find<IndexPatternAttributes>({
type: 'index-pattern',
fields: ['title', 'fields'],
search: `"${savedVis.vis.params.index_pattern}"`,
search: `"${savedVis.visState.params.index_pattern}"`,
searchFields: ['title'],
});
const [indexPattern] = indexPatternObjects.savedObjects.map(indexPatterns.getFromSavedObject);

View file

@ -45,13 +45,12 @@ import { PersistedState } from '../../../../../../../plugins/visualizations/publ
import { buildPipeline } from '../legacy/build_pipeline';
import { Vis } from '../vis';
import { getExpressions, getUiActions } from '../services';
import { VisSavedObject } from '../types';
import { VIS_EVENT_TO_TRIGGER } from './events';
const getKeys = <T extends {}>(o: T): Array<keyof T> => Object.keys(o) as Array<keyof T>;
export interface VisualizeEmbeddableConfiguration {
savedVisualization: VisSavedObject;
vis: Vis;
indexPatterns?: IIndexPattern[];
editUrl: string;
editable: boolean;
@ -73,7 +72,6 @@ export interface VisualizeInput extends EmbeddableInput {
export interface VisualizeOutput extends EmbeddableOutput {
editUrl: string;
indexPatterns?: IIndexPattern[];
savedObjectId: string;
visTypeName: string;
}
@ -81,9 +79,6 @@ type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>;
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
private handler?: ExpressionLoader;
private savedVisualization: VisSavedObject;
private appState: { save(): void } | undefined;
private uiState: PersistedState;
private timefilter: TimefilterContract;
private timeRange?: TimeRange;
private query?: Query;
@ -99,49 +94,24 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
constructor(
timefilter: TimefilterContract,
{
savedVisualization,
editUrl,
indexPatterns,
editable,
appState,
uiState,
}: VisualizeEmbeddableConfiguration,
{ vis, editUrl, indexPatterns, editable }: VisualizeEmbeddableConfiguration,
initialInput: VisualizeInput,
parent?: Container
) {
super(
initialInput,
{
defaultTitle: savedVisualization.title,
defaultTitle: vis.title,
editUrl,
indexPatterns,
editable,
savedObjectId: savedVisualization.id!,
visTypeName: savedVisualization.vis.type.name,
visTypeName: vis.type.name,
},
parent
);
this.timefilter = timefilter;
this.appState = appState;
this.savedVisualization = savedVisualization;
this.vis = this.savedVisualization.vis;
this.vis.on('update', this.handleVisUpdate);
this.vis.on('reload', this.reload);
if (uiState) {
this.uiState = uiState;
} else {
const parsedUiState = savedVisualization.uiStateJSON
? JSON.parse(savedVisualization.uiStateJSON)
: {};
this.uiState = new PersistedState(parsedUiState);
this.uiState.on('change', this.uiStateChangeHandler);
}
this.vis._setUiState(this.uiState);
this.vis = vis;
this.vis.uiState.on('change', this.uiStateChangeHandler);
this.autoRefreshFetchSubscription = timefilter
.getAutoRefreshFetch$()
@ -155,7 +125,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
public getVisualizationDescription() {
return this.savedVisualization.description;
return this.vis.description;
}
public getInspectorAdapters = () => {
@ -184,16 +154,16 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
if (!_.isEqual(visCustomizations, this.visCustomizations)) {
this.visCustomizations = visCustomizations;
// Turn this off or the uiStateChangeHandler will fire for every modification.
this.uiState.off('change', this.uiStateChangeHandler);
this.uiState.clearAllKeys();
this.uiState.set('vis', visCustomizations);
this.vis.uiState.off('change', this.uiStateChangeHandler);
this.vis.uiState.clearAllKeys();
this.vis.uiState.set('vis', visCustomizations);
getKeys(visCustomizations).forEach(key => {
this.uiState.set(key, visCustomizations[key]);
this.vis.uiState.set(key, visCustomizations[key]);
});
this.uiState.on('change', this.uiStateChangeHandler);
this.vis.uiState.on('change', this.uiStateChangeHandler);
}
} else if (!this.appState) {
this.uiState.clearAllKeys();
} else if (this.parent) {
this.vis.uiState.clearAllKeys();
}
}
@ -227,8 +197,8 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
}
if (this.savedVisualization.description && this.domNode) {
this.domNode.setAttribute('data-description', this.savedVisualization.description);
if (this.vis.description && this.domNode) {
this.domNode.setAttribute('data-description', this.vis.description);
}
if (this.handler && dirty) {
@ -236,6 +206,22 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
}
// this is a hack to make editor still work, will be removed once we clean up editor
// @ts-ignore
hasInspector = () => {
const visTypesWithoutInspector = [
'markdown',
'input_control_vis',
'metrics',
'vega',
'timelion',
];
if (visTypesWithoutInspector.includes(this.vis.type.name)) {
return false;
}
return this.getInspectorAdapters();
};
/**
*
* @param {Element} domNode
@ -245,26 +231,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
this.transferCustomizationsToUiState();
this.savedVisualization.vis._setUiState(this.uiState);
this.uiState = this.savedVisualization.vis.getUiState();
// 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',
'timelion',
];
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);
@ -277,12 +243,12 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
this.handler.events$.subscribe(async event => {
// maps hack, remove once esaggs function is cleaned up and ready to accept variables
if (event.name === 'bounds') {
const agg = this.vis.getAggConfig().aggs.find((a: any) => {
const agg = this.vis.data.aggs!.aggs.find((a: any) => {
return get(a, 'type.dslName') === 'geohash_grid';
});
if (
agg.params.precision !== event.data.precision ||
!_.isEqual(agg.params.boundingBox, event.data.boundingBox)
(agg && agg.params.precision !== event.data.precision) ||
(agg && !_.isEqual(agg.params.boundingBox, event.data.boundingBox))
) {
agg.params.boundingBox = event.data.boundingBox;
agg.params.precision = event.data.precision;
@ -296,7 +262,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
event.name === 'brush' ? VIS_EVENT_TO_TRIGGER.brush : VIS_EVENT_TO_TRIGGER.filter;
const context: EmbeddableVisTriggerContext = {
embeddable: this,
timeFieldName: this.vis.indexPattern.timeFieldName,
timeFieldName: this.vis.data.indexPattern!.timeFieldName!,
data: event.data,
};
getUiActions()
@ -308,8 +274,8 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
div.setAttribute('data-title', this.output.title || '');
if (this.savedVisualization.description) {
div.setAttribute('data-description', this.savedVisualization.description);
if (this.vis.description) {
div.setAttribute('data-description', this.vis.description);
}
div.setAttribute('data-test-subj', 'visualizationLoader');
@ -339,10 +305,8 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
public destroy() {
super.destroy();
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();
this.vis.uiState.off('change', this.uiStateChangeHandler);
if (this.handler) {
this.handler.destroy();
this.handler.getElement().remove();
@ -361,35 +325,25 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
query: this.input.query,
filters: this.input.filters,
},
uiState: this.uiState,
uiState: this.vis.uiState,
};
this.expression = await buildPipeline(this.vis, {
searchSource: this.savedVisualization.searchSource,
timefilter: this.timefilter,
timeRange: this.timeRange,
savedObjectId: this.savedVisualization.id,
});
this.vis.filters = { timeRange: this.timeRange };
if (this.handler) {
this.handler.update(this.expression, expressionParams);
}
this.vis.emit('apply');
}
private handleVisUpdate = async () => {
if (this.appState) {
this.appState.save();
}
this.updateHandler();
};
private uiStateChangeHandler = () => {
this.updateInput({
...this.uiState.toJSON(),
...this.vis.uiState.toJSON(),
});
};

View file

@ -26,9 +26,8 @@ import {
ErrorEmbeddable,
} from '../../../../../../../plugins/embeddable/public';
import { DisabledLabEmbeddable } from './disabled_lab_embeddable';
import { getIndexPattern } from './get_index_pattern';
import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable';
import { VisSavedObject } from '../types';
import { Vis } from '../types';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
import {
getCapabilities,
@ -39,6 +38,7 @@ import {
getTimeFilter,
} from '../services';
import { showNewVisModal } from '../wizard';
import { convertToSerializedVis } from '../saved_visualizations/_saved_vis';
interface VisualizationAttributes extends SavedObjectAttributes {
visState: string;
@ -94,31 +94,31 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
}
public async createFromObject(
savedObject: VisSavedObject,
vis: Vis,
input: Partial<VisualizeInput> & { id: string },
parent?: Container
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
const savedVisualizations = getSavedVisualizationsLoader();
try {
const visId = savedObject.id as string;
const visId = vis.id as string;
const editUrl = visId
? getHttp().basePath.prepend(`/app/kibana${savedVisualizations.urlFor(visId)}`)
: '';
const isLabsEnabled = getUISettings().get<boolean>('visualize:enableLabs');
if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') {
return new DisabledLabEmbeddable(savedObject.title, input);
if (!isLabsEnabled && vis.type.stage === 'experimental') {
return new DisabledLabEmbeddable(vis.title, input);
}
const indexPattern = await getIndexPattern(savedObject);
const indexPattern = vis.data.indexPattern;
const indexPatterns = indexPattern ? [indexPattern] : [];
const editable = await this.isEditable();
return new VisualizeEmbeddable(
getTimeFilter(),
{
savedVisualization: savedObject,
vis,
indexPatterns,
editUrl,
editable,
@ -143,7 +143,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
try {
const savedObject = await savedVisualizations.get(savedObjectId);
return this.createFromObject(savedObject, input, parent);
const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject));
return this.createFromObject(vis, input, parent);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);

View file

@ -32,25 +32,47 @@ import _ from 'lodash';
import { PersistedState } from '../../../../../../../plugins/visualizations/public';
import { getTypes } from '../services';
import { VisType } from '../vis_types';
import { VisParams } from '../types';
export class Vis extends EventEmitter {
constructor(visState = { type: 'histogram' }) {
export interface ExprVisState {
title?: string;
type: VisType | string;
params?: VisParams;
}
export interface ExprVisAPIEvents {
filter: (data: any) => void;
brush: (data: any) => void;
}
export interface ExprVisAPI {
events: ExprVisAPIEvents;
}
export class ExprVis extends EventEmitter {
public title: string = '';
public type: VisType;
public params: VisParams = {};
public sessionState: Record<string, any> = {};
public API: ExprVisAPI;
public eventsSubject: any;
private uiState: PersistedState;
constructor(visState: ExprVisState = { type: 'histogram' }) {
super();
this._setUiState(new PersistedState());
this.type = this.getType(visState.type);
this.uiState = new PersistedState();
this.setState(visState);
// Session state is for storing information that is transitory, and will not be saved with the visualization.
// For instance, map bounds, which depends on the view port, browser window size, etc.
this.sessionState = {};
this.API = {
events: {
filter: data => {
filter: (data: any) => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'filterBucket', data });
},
brush: data => {
brush: (data: any) => {
if (!this.eventsSubject) return;
this.eventsSubject.next({ name: 'brush', data });
},
@ -58,18 +80,22 @@ export class Vis extends EventEmitter {
};
}
setState(state) {
this.title = state.title || '';
const type = state.type || this.type;
private getType(type: string | VisType) {
if (_.isString(type)) {
this.type = getTypes().get(type);
return getTypes().get(type);
if (!this.type) {
throw new Error(`Invalid type "${type}"`);
}
} else {
this.type = type;
return type;
}
}
setState(state: ExprVisState) {
this.title = state.title || '';
if (state.type) {
this.type = this.getType(state.type);
}
this.params = _.defaultsDeep(
{},
_.cloneDeep(state.params || {}),
@ -77,10 +103,6 @@ export class Vis extends EventEmitter {
);
}
setCurrentState(state) {
this.setState(state);
}
getState() {
return {
title: this.title,
@ -106,34 +128,27 @@ export class Vis extends EventEmitter {
}
hasUiState() {
return !!this.__uiState;
}
/***
* this should not be used outside of visualize
* @param uiState
* @private
*/
_setUiState(uiState) {
if (uiState instanceof PersistedState) {
this.__uiState = uiState;
}
return !!this.uiState;
}
getUiState() {
return this.__uiState;
return this.uiState;
}
setUiState(state: PersistedState) {
this.uiState = state;
}
/**
* Currently this is only used to extract map-specific information
* (e.g. mapZoom, mapCenter).
*/
uiStateVal(key, val) {
uiStateVal(key: string, val: any) {
if (this.hasUiState()) {
if (_.isUndefined(val)) {
return this.__uiState.get(key);
return this.uiState.get(key);
}
return this.__uiState.set(key, val);
return this.uiState.set(key, val);
}
return val;
}

View file

@ -20,8 +20,9 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
// @ts-ignore
import { Vis } from './vis';
import { ExprVis } from './vis';
import { Visualization } from '../components';
import { VisParams } from '../types';
export const visualization = () => ({
name: 'visualization',
@ -31,9 +32,9 @@ export const visualization = () => ({
const { visData, visConfig, params } = config;
const visType = config.visType || visConfig.type;
const vis = new Vis({
type: visType,
params: visConfig,
const vis = new ExprVis({
type: visType as string,
params: visConfig as VisParams,
});
vis.eventsSubject = { next: handlers.event };

View file

@ -39,12 +39,11 @@ export { VisualizationsSetup, VisualizationsStart };
/** @public types */
export { VisTypeAlias, VisType } from './vis_types';
export { VisSavedObject } from './types';
export { Vis, VisParams, VisState } from './vis';
export { Vis, VisParams, SerializedVis, SerializedVisData, VisData } from './vis';
import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable';
export type VisualizeEmbeddableFactoryContract = PublicContract<VisualizeEmbeddableFactory>;
export type VisualizeEmbeddableContract = PublicContract<VisualizeEmbeddable>;
export { TypesService } from './vis_types/types_service';
export { Status } from './legacy/update_status'; // should remove
export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable';
export { SchemaConfig } from './legacy/build_pipeline';

View file

@ -27,7 +27,7 @@ import {
Schemas,
} from './build_pipeline';
import { Vis } from '..';
import { searchSourceMock, dataPluginMock } from '../../../../../../../plugins/data/public/mocks';
import { dataPluginMock } from '../../../../../../../plugins/data/public/mocks';
import { IAggConfig } from '../../../../../../../plugins/data/public';
jest.mock('ui/new_platform');
@ -78,19 +78,11 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
describe('buildPipelineVisFunction', () => {
let visStateDef: ReturnType<Vis['getCurrentState']>;
let schemaConfig: SchemaConfig;
let schemasDef: Schemas;
let uiState: any;
beforeEach(() => {
visStateDef = {
title: 'title',
// @ts-ignore
type: 'type',
params: {},
} as ReturnType<Vis['getCurrentState']>;
schemaConfig = {
accessor: 0,
label: '',
@ -105,66 +97,53 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
it('handles vega function', () => {
const vis = {
...visStateDef,
params: { spec: 'this is a test' },
};
const actual = buildPipelineVisFunction.vega(vis, schemasDef, uiState);
const actual = buildPipelineVisFunction.vega(vis.params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles input_control_vis function', () => {
const visState = {
...visStateDef,
params: {
some: 'nested',
data: { here: true },
},
const params = {
some: 'nested',
data: { here: true },
};
const actual = buildPipelineVisFunction.input_control_vis(visState, schemasDef, uiState);
const actual = buildPipelineVisFunction.input_control_vis(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles metrics/tsvb function', () => {
const visState = { ...visStateDef, params: { foo: 'bar' } };
const actual = buildPipelineVisFunction.metrics(visState, schemasDef, uiState);
const params = { foo: 'bar' };
const actual = buildPipelineVisFunction.metrics(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles timelion function', () => {
const visState = {
...visStateDef,
params: { expression: 'foo', interval: 'bar' },
};
const actual = buildPipelineVisFunction.timelion(visState, schemasDef, uiState);
const params = { expression: 'foo', interval: 'bar' };
const actual = buildPipelineVisFunction.timelion(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles markdown function', () => {
const visState = {
...visStateDef,
params: {
markdown: '## hello _markdown_',
fontSize: 12,
openLinksInNewTab: true,
foo: 'bar',
},
const params = {
markdown: '## hello _markdown_',
fontSize: 12,
openLinksInNewTab: true,
foo: 'bar',
};
const actual = buildPipelineVisFunction.markdown(visState, schemasDef, uiState);
const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles undefined markdown function', () => {
const visState = {
...visStateDef,
params: { fontSize: 12, openLinksInNewTab: true, foo: 'bar' },
};
const actual = buildPipelineVisFunction.markdown(visState, schemasDef, uiState);
const params = { fontSize: 12, openLinksInNewTab: true, foo: 'bar' };
const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
describe('handles table function', () => {
it('without splits or buckets', () => {
const visState = { ...visStateDef, params: { foo: 'bar' } };
const params = { foo: 'bar' };
const schemas = {
...schemasDef,
metric: [
@ -172,22 +151,22 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
{ ...schemaConfig, accessor: 1 },
],
};
const actual = buildPipelineVisFunction.table(visState, schemas, uiState);
const actual = buildPipelineVisFunction.table(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with splits', () => {
const visState = { ...visStateDef, params: { foo: 'bar' } };
const params = { foo: 'bar' };
const schemas = {
...schemasDef,
split_row: [1, 2],
};
const actual = buildPipelineVisFunction.table(visState, schemas, uiState);
const actual = buildPipelineVisFunction.table(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with splits and buckets', () => {
const visState = { ...visStateDef, params: { foo: 'bar' } };
const params = { foo: 'bar' };
const schemas = {
...schemasDef,
metric: [
@ -197,17 +176,14 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
split_row: [2, 4],
bucket: [3],
};
const actual = buildPipelineVisFunction.table(visState, schemas, uiState);
const actual = buildPipelineVisFunction.table(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with showPartialRows=true and showMetricsAtAllLevels=true', () => {
const visState = {
...visStateDef,
params: {
showMetricsAtAllLevels: true,
showPartialRows: true,
},
const params = {
showMetricsAtAllLevels: true,
showPartialRows: true,
};
const schemas = {
...schemasDef,
@ -219,17 +195,14 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
],
bucket: [0, 3],
};
const actual = buildPipelineVisFunction.table(visState, schemas, uiState);
const actual = buildPipelineVisFunction.table(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with showPartialRows=true and showMetricsAtAllLevels=false', () => {
const visState = {
...visStateDef,
params: {
showMetricsAtAllLevels: false,
showPartialRows: true,
},
const params = {
showMetricsAtAllLevels: false,
showPartialRows: true,
};
const schemas = {
...schemasDef,
@ -241,14 +214,14 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
],
bucket: [0, 3],
};
const actual = buildPipelineVisFunction.table(visState, schemas, uiState);
const actual = buildPipelineVisFunction.table(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
describe('handles metric function', () => {
it('without buckets', () => {
const visState = { ...visStateDef, params: { metric: {} } };
const params = { metric: {} };
const schemas = {
...schemasDef,
metric: [
@ -256,12 +229,12 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
{ ...schemaConfig, accessor: 1 },
],
};
const actual = buildPipelineVisFunction.metric(visState, schemas, uiState);
const actual = buildPipelineVisFunction.metric(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with buckets', () => {
const visState = { ...visStateDef, params: { metric: {} } };
const params = { metric: {} };
const schemas = {
...schemasDef,
metric: [
@ -270,21 +243,21 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
],
group: [{ accessor: 2 }],
};
const actual = buildPipelineVisFunction.metric(visState, schemas, uiState);
const actual = buildPipelineVisFunction.metric(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with percentage mode should have percentage format', () => {
const visState = { ...visStateDef, params: { metric: { percentageMode: true } } };
const params = { metric: { percentageMode: true } };
const schemas = { ...schemasDef };
const actual = buildPipelineVisFunction.metric(visState, schemas, uiState);
const actual = buildPipelineVisFunction.metric(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
describe('handles tagcloud function', () => {
it('without buckets', () => {
const actual = buildPipelineVisFunction.tagcloud(visStateDef, schemasDef, uiState);
const actual = buildPipelineVisFunction.tagcloud({}, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
@ -293,21 +266,21 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
...schemasDef,
segment: [{ accessor: 1 }],
};
const actual = buildPipelineVisFunction.tagcloud(visStateDef, schemas, uiState);
const actual = buildPipelineVisFunction.tagcloud({}, schemas, uiState);
expect(actual).toMatchSnapshot();
});
it('with boolean param showLabel', () => {
const visState = { ...visStateDef, params: { showLabel: false } };
const actual = buildPipelineVisFunction.tagcloud(visState, schemasDef, uiState);
const params = { showLabel: false };
const actual = buildPipelineVisFunction.tagcloud(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
});
describe('handles region_map function', () => {
it('without buckets', () => {
const visState = { ...visStateDef, params: { metric: {} } };
const actual = buildPipelineVisFunction.region_map(visState, schemasDef, uiState);
const params = { metric: {} };
const actual = buildPipelineVisFunction.region_map(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
@ -316,19 +289,19 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
...schemasDef,
segment: [1, 2],
};
const actual = buildPipelineVisFunction.region_map(visStateDef, schemas, uiState);
const actual = buildPipelineVisFunction.region_map({}, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
it('handles tile_map function', () => {
const visState = { ...visStateDef, params: { metric: {} } };
const params = { metric: {} };
const schemas = {
...schemasDef,
segment: [1, 2],
geo_centroid: [3, 4],
};
const actual = buildPipelineVisFunction.tile_map(visState, schemas, uiState);
const actual = buildPipelineVisFunction.tile_map(params, schemas, uiState);
expect(actual).toMatchSnapshot();
});
@ -337,7 +310,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
...schemasDef,
segment: [1, 2],
};
const actual = buildPipelineVisFunction.pie(visStateDef, schemas, uiState);
const actual = buildPipelineVisFunction.pie({}, schemas, uiState);
expect(actual).toMatchSnapshot();
});
});
@ -347,11 +320,16 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
it('calls toExpression on vis_type if it exists', async () => {
const vis = ({
getCurrentState: () => {},
getUiState: () => null,
getState: () => {},
isHierarchical: () => false,
aggs: {
getResponseAggs: () => [],
data: {
aggs: {
getResponseAggs: () => [],
},
searchSource: {
getField: jest.fn(),
getParent: jest.fn(),
},
},
// @ts-ignore
type: {
@ -359,7 +337,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
},
} as unknown) as Vis;
const expression = await buildPipeline(vis, {
searchSource: searchSourceMock,
timefilter: dataStart.query.timefilter.timefilter,
});
expect(expression).toMatchSnapshot();
@ -370,7 +347,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
const dataStart = dataPluginMock.createStartContract();
let aggs: IAggConfig[];
let visState: any;
let vis: Vis;
let params: any;
@ -397,7 +373,11 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
describe('test y dimension format for histogram chart', () => {
beforeEach(() => {
visState = {
vis = {
// @ts-ignore
type: {
name: 'histogram',
},
params: {
seriesParams: [
{
@ -414,24 +394,16 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
},
],
},
};
vis = {
// @ts-ignore
type: {
name: 'histogram',
},
aggs: {
getResponseAggs: () => {
return aggs;
},
data: {
aggs: {
getResponseAggs: () => {
return aggs;
},
} as any,
},
isHierarchical: () => {
return false;
},
getCurrentState: () => {
return visState;
},
};
});
@ -443,7 +415,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
it('with one numeric metric in percentage mode', async () => {
visState.params.valueAxes[0].scale.mode = 'percentage';
vis.params.valueAxes[0].scale.mode = 'percentage';
const dimensions = await buildVislibDimensions(vis, params);
const expected = { id: 'percent' };
const actual = dimensions.y[0].format;
@ -454,33 +426,31 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
const aggConfig = aggs[0];
aggs = [{ ...aggConfig } as IAggConfig, { ...aggConfig, id: '5' } as IAggConfig];
visState = {
params: {
seriesParams: [
{
data: { id: '0' },
valueAxis: 'axis-y-1',
vis.params = {
seriesParams: [
{
data: { id: '0' },
valueAxis: 'axis-y-1',
},
{
data: { id: '5' },
valueAxis: 'axis-y-2',
},
],
valueAxes: [
{
id: 'axis-y-1',
scale: {
mode: 'normal',
},
{
data: { id: '5' },
valueAxis: 'axis-y-2',
},
{
id: 'axis-y-2',
scale: {
mode: 'percentage',
},
],
valueAxes: [
{
id: 'axis-y-1',
scale: {
mode: 'normal',
},
},
{
id: 'axis-y-2',
scale: {
mode: 'percentage',
},
},
],
},
},
],
};
const dimensions = await buildVislibDimensions(vis, params);
@ -493,29 +463,27 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
describe('test y dimension format for gauge chart', () => {
beforeEach(() => {
visState = { params: { gauge: {} } };
vis = {
// @ts-ignore
type: {
name: 'gauge',
},
aggs: {
getResponseAggs: () => {
return aggs;
},
params: { gauge: {} },
data: {
aggs: {
getResponseAggs: () => {
return aggs;
},
} as any,
},
isHierarchical: () => {
return false;
},
getCurrentState: () => {
return visState;
},
};
});
it('with percentageMode = false', async () => {
visState.params.gauge.percentageMode = false;
vis.params.gauge.percentageMode = false;
const dimensions = await buildVislibDimensions(vis, params);
const expected = { id: 'number' };
const actual = dimensions.y[0].format;
@ -523,7 +491,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
});
it('with percentageMode = true', async () => {
visState.params.gauge.percentageMode = true;
vis.params.gauge.percentageMode = true;
const dimensions = await buildVislibDimensions(vis, params);
const expected = { id: 'percent' };
const actual = dimensions.y[0].format;

View file

@ -21,14 +21,13 @@ import { get } from 'lodash';
import moment from 'moment';
import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/public';
import {
fieldFormats,
IAggConfig,
ISearchSource,
fieldFormats,
search,
TimefilterContract,
} from '../../../../../../../plugins/data/public';
const { isDateHistogramBucketAggConfig } = search.aggs;
import { Vis, VisParams } from '../types';
const { isDateHistogramBucketAggConfig } = search.aggs;
interface SchemaConfigParams {
precision?: number;
@ -59,7 +58,7 @@ export interface Schemas {
}
type buildVisFunction = (
visState: ReturnType<Vis['getCurrentState']>,
params: VisParams,
schemas: Schemas,
uiState: any,
meta?: { savedObjectId?: string }
@ -139,7 +138,12 @@ const getSchemas = (
const schemas: Schemas = {
metric: [],
};
const responseAggs = vis.aggs.getResponseAggs().filter((agg: IAggConfig) => agg.enabled);
if (!vis.data.aggs) {
return schemas;
}
const responseAggs = vis.data.aggs.getResponseAggs().filter((agg: IAggConfig) => agg.enabled);
const isHierarchical = vis.isHierarchical();
const metrics = responseAggs.filter((agg: IAggConfig) => agg.type.type === 'metrics');
responseAggs.forEach((agg: IAggConfig) => {
@ -228,9 +232,8 @@ export const prepareDimension = (variable: string, data: any) => {
};
const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): void => {
const visState = vis.getCurrentState();
const visConfig = visState.params;
const responseAggs = vis.aggs.getResponseAggs().filter((agg: IAggConfig) => agg.enabled);
const visConfig = vis.params;
const responseAggs = vis.data.aggs!.getResponseAggs().filter((agg: IAggConfig) => agg.enabled);
(dimensions.y || []).forEach(yDimension => {
const yAgg = responseAggs[yDimension.accessor];
@ -252,27 +255,26 @@ const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): vo
};
export const buildPipelineVisFunction: BuildPipelineVisFunction = {
vega: visState => {
return `vega ${prepareString('spec', visState.params.spec)}`;
vega: params => {
return `vega ${prepareString('spec', params.spec)}`;
},
input_control_vis: visState => {
return `input_control_vis ${prepareJson('visConfig', visState.params)}`;
input_control_vis: params => {
return `input_control_vis ${prepareJson('visConfig', params)}`;
},
metrics: (visState, schemas, uiState = {}, meta) => {
const paramsJson = prepareJson('params', visState.params);
metrics: (params, schemas, uiState = {}) => {
const paramsJson = prepareJson('params', params);
const uiStateJson = prepareJson('uiState', uiState);
const savedObjectIdParam = prepareString('savedObjectId', meta?.savedObjectId);
const params = [paramsJson, uiStateJson, savedObjectIdParam].filter(param => Boolean(param));
return `tsvb ${params.join(' ')}`;
const paramsArray = [paramsJson, uiStateJson].filter(param => Boolean(param));
return `tsvb ${paramsArray.join(' ')}`;
},
timelion: visState => {
const expression = prepareString('expression', visState.params.expression);
const interval = prepareString('interval', visState.params.interval);
timelion: params => {
const expression = prepareString('expression', params.expression);
const interval = prepareString('interval', params.interval);
return `timelion_vis ${expression}${interval}`;
},
markdown: visState => {
const { markdown, fontSize, openLinksInNewTab } = visState.params;
markdown: params => {
const { markdown, fontSize, openLinksInNewTab } = params;
let escapedMarkdown = '';
if (typeof markdown === 'string' || markdown instanceof String) {
escapedMarkdown = escapeString(markdown.toString());
@ -282,14 +284,14 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
expr += prepareValue('openLinksInNewTab', openLinksInNewTab);
return expr;
},
table: (visState, schemas) => {
table: (params, schemas) => {
const visConfig = {
...visState.params,
...buildVisConfig.table(schemas, visState.params),
...params,
...buildVisConfig.table(schemas, params),
};
return `kibana_table ${prepareJson('visConfig', visConfig)}`;
},
metric: (visState, schemas) => {
metric: (params, schemas) => {
const {
percentageMode,
useRanges,
@ -299,11 +301,11 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
labels,
invertColors,
style,
} = visState.params.metric;
} = params.metric;
const { metrics, bucket } = buildVisConfig.metric(schemas).dimensions;
// fix formatter for percentage mode
if (get(visState.params, 'metric.percentageMode') === true) {
if (get(params, 'metric.percentageMode') === true) {
metrics.forEach((metric: SchemaConfig) => {
metric.format = { id: 'percent' };
});
@ -335,8 +337,8 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
return expr;
},
tagcloud: (visState, schemas) => {
const { scale, orientation, minFontSize, maxFontSize, showLabel } = visState.params;
tagcloud: (params, schemas) => {
const { scale, orientation, minFontSize, maxFontSize, showLabel } = params;
const { metric, bucket } = buildVisConfig.tagcloud(schemas);
let expr = `tagcloud metric={visdimension ${metric.accessor}} `;
expr += prepareValue('scale', scale);
@ -348,23 +350,23 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
return expr;
},
region_map: (visState, schemas) => {
region_map: (params, schemas) => {
const visConfig = {
...visState.params,
...params,
...buildVisConfig.region_map(schemas),
};
return `regionmap ${prepareJson('visConfig', visConfig)}`;
},
tile_map: (visState, schemas) => {
tile_map: (params, schemas) => {
const visConfig = {
...visState.params,
...params,
...buildVisConfig.tile_map(schemas),
};
return `tilemap ${prepareJson('visConfig', visConfig)}`;
},
pie: (visState, schemas) => {
pie: (params, schemas) => {
const visConfig = {
...visState.params,
...params,
...buildVisConfig.pie(schemas),
};
return `kibana_pie ${prepareJson('visConfig', visConfig)}`;
@ -440,7 +442,6 @@ const buildVisConfig: BuildVisConfigFunction = {
export const buildVislibDimensions = async (
vis: any,
params: {
searchSource: any;
timefilter: TimefilterContract;
timeRange?: any;
abortSignal?: AbortSignal;
@ -460,7 +461,7 @@ export const buildVislibDimensions = async (
splitColumn: schemas.split_column,
};
if (schemas.segment) {
const xAgg = vis.aggs.getResponseAggs()[dimensions.x.accessor];
const xAgg = vis.data.aggs.getResponseAggs()[dimensions.x.accessor];
if (xAgg.type.name === 'date_histogram') {
dimensions.x.params.date = true;
const { esUnit, esValue } = xAgg.buckets.getInterval();
@ -472,7 +473,7 @@ export const buildVislibDimensions = async (
} else if (xAgg.type.name === 'histogram') {
const intervalParam = xAgg.type.paramByName('interval');
const output = { params: {} as any };
await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, params.searchSource, {
await intervalParam.modifyAggConfigOnSearchRequestStart(xAgg, vis.data.searchSource, {
abortSignal: params.abortSignal,
});
intervalParam.write(xAgg, output);
@ -487,18 +488,14 @@ export const buildVislibDimensions = async (
export const buildPipeline = async (
vis: Vis,
params: {
searchSource: ISearchSource;
timefilter: TimefilterContract;
timeRange?: any;
savedObjectId?: string;
}
) => {
const { searchSource } = params;
const { indexPattern } = vis;
const query = searchSource.getField('query');
const filters = searchSource.getField('filter');
const visState = vis.getCurrentState();
const uiState = vis.getUiState();
const { indexPattern, searchSource } = vis.data;
const query = searchSource!.getField('query');
const filters = searchSource!.getField('filter');
const { uiState } = vis;
// context
let pipeline = `kibana | kibana_context `;
@ -508,18 +505,18 @@ export const buildPipeline = async (
if (filters) {
pipeline += prepareJson('filters', filters);
}
if (vis.savedSearchId) {
pipeline += prepareString('savedSearchId', vis.savedSearchId);
if (vis.data.savedSearchId) {
pipeline += prepareString('savedSearchId', vis.data.savedSearchId);
}
pipeline += '| ';
// request handler
if (vis.type.requestHandler === 'courier') {
pipeline += `esaggs
${prepareString('index', indexPattern.id)}
${prepareString('index', indexPattern!.id)}
metricsAtAllLevels=${vis.isHierarchical()}
partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false}
${prepareJson('aggConfigs', visState.aggs)} | `;
${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `;
}
const schemas = getSchemas(vis, {
@ -527,18 +524,16 @@ export const buildPipeline = async (
timefilter: params.timefilter,
});
if (buildPipelineVisFunction[vis.type.name]) {
pipeline += buildPipelineVisFunction[vis.type.name](visState, schemas, uiState, {
savedObjectId: params.savedObjectId,
});
pipeline += buildPipelineVisFunction[vis.type.name](vis.params, schemas, uiState);
} else if (vislibCharts.includes(vis.type.name)) {
const visConfig = visState.params;
const visConfig = { ...vis.params };
visConfig.dimensions = await buildVislibDimensions(vis, params);
pipeline += `vislib type='${vis.type.name}' ${prepareJson('visConfig', visState.params)}`;
pipeline += `vislib type='${vis.type.name}' ${prepareJson('visConfig', visConfig)}`;
} else if (vis.type.toExpression) {
pipeline += await vis.type.toExpression(vis, params);
} else {
const visConfig = visState.params;
const visConfig = { ...vis.params };
visConfig.dimensions = schemas;
pipeline += `visualization type='${vis.type.name}'
${prepareJson('visConfig', visConfig)}

View file

@ -1,102 +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 { getUpdateStatus, Status } from './update_status';
// Parts of the tests in this file are generated more dynamically, based on the
// values inside the Status object.Make sure this object has one function per entry
// in Status, that actually change on the passed $scope, what needs to be changed
// so that we expect the getUpdateStatus function to actually detect a change.
const changeFunctions = {
[Status.AGGS]: $scope => ($scope.vis.aggs = { foo: 'new' }),
[Status.DATA]: $scope => ($scope.visData = { foo: 'new' }),
[Status.PARAMS]: $scope => ($scope.vis.params = { foo: 'new' }),
[Status.RESIZE]: $scope => ($scope.vis.size = [50, 50]),
[Status.TIME]: $scope => ($scope.vis.filters.timeRange = { from: 'now-7d', to: 'now' }),
[Status.UI_STATE]: $scope => ($scope.uiState = { foo: 'new' }),
};
describe('getUpdateStatus', () => {
function getScope() {
return {
vis: {
aggs: {},
size: [100, 100],
params: {},
filters: {},
},
uiState: {},
visData: {},
};
}
function initStatusCheckerAndChangeProperty(type, requiresUpdateStatus) {
const $scope = getScope();
// Call the getUpdateStatus function initially, so it can store it's current state
getUpdateStatus(requiresUpdateStatus, $scope, $scope);
// Get the change function for that specific change type
const changeFn = changeFunctions[type];
if (!changeFn) {
throw new Error(`Please implement the test change function for ${type}.`);
}
// Call that change function to manipulate the scope so it changed.
changeFn($scope);
return getUpdateStatus(requiresUpdateStatus, $scope, $scope);
}
it('should be a function', () => {
expect(typeof getUpdateStatus).toBe('function');
});
Object.entries(Status).forEach(([typeKey, typeValue]) => {
// This block automatically creates very simple tests for each of the Status
// keys, so we have simple tests per changed property.
// If it makes sense to test more specific behavior of a specific change detection
// please add additional tests for that.
it(`should detect changes for Status.${typeKey}`, () => {
// Check whether the required change type is not correctly determined
const status = initStatusCheckerAndChangeProperty(typeValue, [typeValue]);
expect(status[typeValue]).toBe(true);
});
it(`should not detect changes in other properties when changing Status.${typeKey}`, () => {
// Only change typeKey, but track changes for all status changes
const status = initStatusCheckerAndChangeProperty(typeValue, Object.values(Status));
Object.values(Status)
// Filter out the actual changed property so we only test for all other properties
.filter(stat => stat !== typeValue)
.forEach(otherProp => {
expect(status[otherProp]).toBeFalsy();
});
});
it(`should not detect changes if not requested for Status.${typeKey}`, () => {
const allOtherStatusProperties = Object.values(Status).filter(stat => stat !== typeValue);
// Change only the typeKey property, but do not listen for changes on it
// listen on all other status changes instead.
const status = initStatusCheckerAndChangeProperty(typeValue, allOtherStatusProperties);
// The typeValue check should be falsy, since we did not request tracking it.
expect(status[typeValue]).toBeFalsy();
});
});
});

View file

@ -1,117 +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 { PersistedState } from '../../../../../../../plugins/visualizations/public';
import { calculateObjectHash } from '../../../../../../../plugins/kibana_utils/common';
import { Vis } from '../vis';
enum Status {
AGGS = 'aggs',
DATA = 'data',
PARAMS = 'params',
RESIZE = 'resize',
TIME = 'time',
UI_STATE = 'uiState',
}
/**
* Checks whether the hash of a specific key in the given oldStatus has changed
* compared to the new valueHash passed.
*/
function hasHashChanged<T extends string>(
valueHash: string,
oldStatus: { [key in T]?: string },
name: T
): boolean {
const oldHash = oldStatus[name];
return oldHash !== valueHash;
}
interface Size {
width: number;
height: number;
}
function hasSizeChanged(size: Size, oldSize?: Size): boolean {
if (!oldSize) {
return true;
}
return oldSize.width !== size.width || oldSize.height !== size.height;
}
function getUpdateStatus<T extends Status>(
requiresUpdateStatus: T[] = [],
obj: any,
param: { vis: Vis; visData: any; uiState: PersistedState }
): { [reqStats in T]: boolean } {
const status = {} as { [reqStats in Status]: boolean };
// If the vis type doesn't need update status, skip all calculations
if (requiresUpdateStatus.length === 0) {
return status;
}
if (!obj._oldStatus) {
obj._oldStatus = {};
}
for (const requiredStatus of requiresUpdateStatus) {
let hash;
// Calculate all required status updates for this visualization
switch (requiredStatus) {
case Status.AGGS:
hash = calculateObjectHash(param.vis.aggs);
status.aggs = hasHashChanged(hash, obj._oldStatus, 'aggs');
obj._oldStatus.aggs = hash;
break;
case Status.DATA:
hash = calculateObjectHash(param.visData);
status.data = hasHashChanged(hash, obj._oldStatus, 'data');
obj._oldStatus.data = hash;
break;
case Status.PARAMS:
hash = calculateObjectHash(param.vis.params);
status.params = hasHashChanged(hash, obj._oldStatus, 'param');
obj._oldStatus.param = hash;
break;
case Status.RESIZE:
const width: number = param.vis.size ? param.vis.size[0] : 0;
const height: number = param.vis.size ? param.vis.size[1] : 0;
const size = { width, height };
status.resize = hasSizeChanged(size, obj._oldStatus.resize);
obj._oldStatus.resize = size;
break;
case Status.TIME:
const timeRange = param.vis.filters && param.vis.filters.timeRange;
hash = calculateObjectHash(timeRange);
status.time = hasHashChanged(hash, obj._oldStatus, 'time');
obj._oldStatus.time = hash;
break;
case Status.UI_STATE:
hash = calculateObjectHash(param.uiState);
status.uiState = hasHashChanged(hash, obj._oldStatus, 'uiState');
obj._oldStatus.uiState = hash;
break;
}
}
return status;
}
export { getUpdateStatus, Status };

View file

@ -41,6 +41,8 @@ const createStartContract = (): VisualizationsStart => ({
savedVisualizationsLoader: {} as any,
showNewVisModal: jest.fn(),
createVis: jest.fn(),
convertFromSerializedVis: jest.fn(),
convertToSerializedVis: jest.fn(),
});
const createInstance = async () => {

View file

@ -39,6 +39,8 @@ import {
setSavedVisualizationsLoader,
setTimeFilter,
setAggs,
setChrome,
setOverlays,
} from './services';
import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable';
import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public';
@ -48,14 +50,16 @@ import { visualization as visualizationRenderer } from './expressions/visualizat
import {
DataPublicPluginSetup,
DataPublicPluginStart,
IIndexPattern,
} from '../../../../../../plugins/data/public';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public';
import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations';
import { VisImpl } from './vis_impl';
import { SerializedVis, Vis } from './vis';
import { showNewVisModal } from './wizard';
import { UiActionsStart } from '../../../../../../plugins/ui_actions/public';
import { VisState } from './types';
import {
convertFromSerializedVis,
convertToSerializedVis,
} from './saved_visualizations/_saved_vis';
/**
* Interface for this plugin's returned setup/start contracts.
@ -67,7 +71,9 @@ export type VisualizationsSetup = TypesSetup;
export interface VisualizationsStart extends TypesStart {
savedVisualizationsLoader: SavedVisualizationsLoader;
createVis: (indexPattern: IIndexPattern, visState?: VisState) => VisImpl;
createVis: (visType: string, visState?: SerializedVis) => Vis;
convertToSerializedVis: typeof convertToSerializedVis;
convertFromSerializedVis: typeof convertFromSerializedVis;
showNewVisModal: typeof showNewVisModal;
}
@ -138,6 +144,8 @@ export class VisualizationsPlugin
setUiActions(uiActions);
setTimeFilter(data.query.timefilter.timefilter);
setAggs(data.search.aggs);
setOverlays(core.overlays);
setChrome(core.chrome);
const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
@ -155,8 +163,9 @@ export class VisualizationsPlugin
* @param {IIndexPattern} indexPattern - index pattern to use
* @param {VisState} visState - visualization configuration
*/
createVis: (indexPattern: IIndexPattern, visState?: VisState) =>
new VisImpl(indexPattern, visState),
createVis: (visType: string, visState?: SerializedVis) => new Vis(visType, visState),
convertToSerializedVis,
convertFromSerializedVis,
savedVisualizationsLoader,
};
}

View file

@ -32,65 +32,74 @@ import {
// @ts-ignore
import { updateOldState } from '../legacy/vis_update_state';
import { extractReferences, injectReferences } from './saved_visualization_references';
import { IIndexPattern } from '../../../../../../../plugins/data/public';
import { VisSavedObject } from '../types';
import { VisImpl } from '../vis_impl';
import {
IIndexPattern,
ISearchSource,
SearchSource,
} from '../../../../../../../plugins/data/public';
import { ISavedVis, SerializedVis } from '../types';
import { createSavedSearchesLoader } from '../../../../../../../plugins/discover/public';
import { getChrome, getOverlays, getIndexPatterns, getSavedObjects } from '../services';
async function _afterEsResp(savedVis: VisSavedObject, services: any) {
await _getLinkedSavedSearch(savedVis, services);
savedVis.searchSource!.setField('size', 0);
savedVis.vis = savedVis.vis ? _updateVis(savedVis) : await _createVis(savedVis);
return savedVis;
}
export const convertToSerializedVis = async (savedVis: ISavedVis): Promise<SerializedVis> => {
const { visState } = savedVis;
const searchSource =
savedVis.searchSource && (await getSearchSource(savedVis.searchSource, savedVis.savedSearchId));
async function _getLinkedSavedSearch(savedVis: VisSavedObject, services: any) {
const linkedSearch = !!savedVis.savedSearchId;
const current = savedVis.savedSearch;
const indexPattern =
searchSource && searchSource.getField('index') ? searchSource.getField('index')!.id : undefined;
if (linkedSearch && current && current.id === savedVis.savedSearchId) {
return;
const aggs = indexPattern ? visState.aggs || [] : visState.aggs;
return {
id: savedVis.id,
title: savedVis.title,
type: visState.type,
description: savedVis.description,
params: visState.params,
uiState: JSON.parse(savedVis.uiStateJSON || '{}'),
data: {
indexPattern,
aggs,
searchSource,
savedSearchId: savedVis.savedSearchId,
},
};
};
export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => {
return {
id: vis.id,
title: vis.title,
description: vis.description,
visState: {
type: vis.type,
aggs: vis.data.aggs,
params: vis.params,
},
uiStateJSON: JSON.stringify(vis.uiState),
searchSource: vis.data.searchSource!,
savedSearchId: vis.data.savedSearchId,
};
};
const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => {
const searchSource = inputSearchSource.createCopy
? inputSearchSource.createCopy()
: new SearchSource({ ...(inputSearchSource as any).fields });
if (savedSearchId) {
const savedSearch = await createSavedSearchesLoader({
savedObjectsClient: getSavedObjects().client,
indexPatterns: getIndexPatterns(),
chrome: getChrome(),
overlays: getOverlays(),
}).get(savedSearchId);
searchSource.setParent(savedSearch.searchSource);
}
if (savedVis.savedSearch) {
savedVis.searchSource!.setParent(savedVis.savedSearch.searchSource.getParent());
savedVis.savedSearch.destroy();
delete savedVis.savedSearch;
}
const savedSearches = createSavedSearchesLoader(services);
if (linkedSearch) {
savedVis.savedSearch = await savedSearches.get(savedVis.savedSearchId!);
savedVis.searchSource!.setParent(savedVis.savedSearch!.searchSource);
}
}
async function _createVis(savedVis: VisSavedObject) {
savedVis.visState = updateOldState(savedVis.visState);
// visState doesn't yet exist when importing a visualization, so we can't
// assume that exists at this point. If it does exist, then we're not
// importing a visualization, so we want to sync the title.
if (savedVis.visState) {
savedVis.visState.title = savedVis.title;
}
savedVis.vis = new VisImpl(savedVis.searchSource!.getField('index')!, savedVis.visState);
savedVis.vis!.savedSearchId = savedVis.savedSearchId;
return savedVis.vis;
}
function _updateVis(savedVis: VisSavedObject) {
if (savedVis.vis && savedVis.searchSource) {
savedVis.vis.indexPattern = savedVis.searchSource.getField('index');
savedVis.visState.title = savedVis.title;
savedVis.vis.setState(savedVis.visState);
savedVis.vis.savedSearchId = savedVis.savedSearchId;
}
return savedVis.vis;
}
searchSource!.setField('size', 0);
return searchSource;
};
export function createSavedVisClass(services: SavedObjectKibanaServices) {
const SavedObjectClass = createSavedObjectClass(services);
@ -131,8 +140,16 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) {
savedSearchId: opts.savedSearchId,
version: 1,
},
afterESResp: (savedObject: SavedObject) => {
return _afterEsResp(savedObject as VisSavedObject, services) as Promise<SavedObject>;
afterESResp: async (savedObject: SavedObject) => {
const savedVis = (savedObject as any) as ISavedVis;
savedVis.visState = await updateOldState(savedVis.visState);
if (savedVis.savedSearchId && savedVis.searchSource) {
savedObject.searchSource = await getSearchSource(
savedVis.searchSource,
savedVis.savedSearchId
);
}
return (savedVis as any) as SavedObject;
},
});
this.showInRecentlyAccessed = true;

View file

@ -18,7 +18,7 @@
*/
import { extractReferences, injectReferences } from './saved_visualization_references';
import { VisSavedObject, VisState } from '../types';
import { VisSavedObject, SavedVisState } from '../types';
describe('extractReferences', () => {
test('extracts nothing if savedSearchId is empty', () => {
@ -140,7 +140,7 @@ Object {
},
],
},
} as unknown) as VisState,
} as unknown) as SavedVisState,
} as VisSavedObject;
const references = [
{
@ -201,7 +201,7 @@ Object {
},
],
},
} as unknown) as VisState,
} as unknown) as SavedVisState,
} as VisSavedObject;
expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot(
`"Could not find index pattern reference \\"control_0_index_pattern\\""`

View file

@ -18,10 +18,13 @@
*/
import {
ApplicationStart,
Capabilities,
ChromeStart,
HttpStart,
I18nStart,
IUiSettingsClient,
OverlayStart,
SavedObjectsStart,
} from '../../../../../../core/public';
import { TypesStart } from './vis_types';
@ -76,3 +79,9 @@ export const [getSavedVisualizationsLoader, setSavedVisualizationsLoader] = crea
export const [getAggs, setAggs] = createGetterSetter<DataPublicPluginStart['search']['aggs']>(
'AggConfigs'
);
export const [getOverlays, setOverlays] = createGetterSetter<OverlayStart>('Overlays');
export const [getChrome, setChrome] = createGetterSetter<ChromeStart>('Chrome');
export const [getApplication, setApplication] = createGetterSetter<ApplicationStart>('Application');

View file

@ -18,21 +18,33 @@
*/
import { SavedObject } from '../../../../../../plugins/saved_objects/public';
import { Vis, VisState, VisParams, VisualizationController } from './vis';
import { ISearchSource } from '../../../../../../plugins/data/public/';
import { SavedSearch } from '../../../../../../plugins/discover/public';
import { ISearchSource, AggConfigOptions } from '../../../../../../plugins/data/public';
import { SerializedVis, Vis, VisParams } from './vis';
export { Vis, VisState, VisParams, VisualizationController };
export { Vis, SerializedVis, VisParams };
export interface VisSavedObject extends SavedObject {
vis: Vis;
description?: string;
searchSource: ISearchSource;
export interface VisualizationController {
render(visData: any, visParams: any): Promise<void>;
destroy(): void;
isLoaded?(): Promise<void> | void;
}
export interface SavedVisState {
type: string;
params: VisParams;
aggs: AggConfigOptions[];
}
export interface ISavedVis {
id: string;
title: string;
description?: string;
visState: SavedVisState;
searchSource?: ISearchSource;
uiStateJSON?: string;
destroy: () => void;
savedSearchRefName?: string;
savedSearchId?: string;
savedSearch?: SavedSearch;
visState: VisState;
}
// @ts-ignore-next-line
export interface VisSavedObject extends SavedObject, ISavedVis {}

View file

@ -17,47 +17,182 @@
* under the License.
*/
/**
* @name Vis
*
* @description This class consists of aggs, params, listeners, title, and type.
* - Aggs: Instances of IAggConfig.
* - Params: The settings in the Options tab.
*
* Not to be confused with vislib/vis.js.
*/
import { isFunction, defaults, cloneDeep } from 'lodash';
import { PersistedState } from '../../../../../../../src/plugins/visualizations/public';
// @ts-ignore
import { updateVisualizationConfig } from './legacy/vis_update';
import { getTypes, getAggs } from './services';
import { VisType } from './vis_types';
import { Status } from './legacy/update_status';
import { IAggConfigs } from '../../../../../../plugins/data/public';
import {
IAggConfigs,
IndexPattern,
ISearchSource,
AggConfigOptions,
} from '../../../../../../plugins/data/public';
export interface Vis {
type: VisType;
getCurrentState: (
includeDisabled?: boolean
) => {
title: string;
type: string;
params: VisParams;
aggs: Array<{ [key: string]: any }>;
};
/**
* If a visualization based on the saved search,
* the id is necessary for building an expression function in src/plugins/expressions/common/expression_functions/specs/kibana_context.ts
*/
export interface SerializedVisData {
expression?: string;
aggs: AggConfigOptions[];
indexPattern?: string;
searchSource?: ISearchSource;
savedSearchId?: string;
}
// Since we haven't typed everything here yet, we basically "any" the rest
// of that interface. This should be removed as soon as this type definition
// has been completed. But that way we at least have typing for a couple of
// properties on that type.
[key: string]: any;
export interface SerializedVis {
id: string;
title: string;
description?: string;
type: string;
params: VisParams;
uiState?: any;
data: SerializedVisData;
}
export interface VisData {
ast?: string;
aggs?: IAggConfigs;
indexPattern?: IndexPattern;
searchSource?: ISearchSource;
savedSearchId?: string;
}
export interface VisParams {
[key: string]: any;
}
export interface VisState {
title: string;
type: VisType;
params: VisParams;
aggs: IAggConfigs;
}
export class Vis {
public readonly type: VisType;
public readonly id: string;
public title: string = '';
public description: string = '';
public params: VisParams = {};
// Session state is for storing information that is transitory, and will not be saved with the visualization.
// For instance, map bounds, which depends on the view port, browser window size, etc.
public sessionState: Record<string, any> = {};
public data: VisData = {};
export interface VisualizationController {
render(visData: any, visParams: any, update: { [key in Status]: boolean }): Promise<void>;
destroy(): void;
isLoaded?(): Promise<void> | void;
public readonly uiState: PersistedState;
constructor(visType: string, visState: SerializedVis = {} as any) {
this.type = this.getType(visType);
this.params = this.getParams(visState.params);
this.uiState = new PersistedState(visState.uiState);
this.id = visState.id;
this.setState(visState || {});
}
private getType(visType: string) {
const type = getTypes().get(visType);
if (!type) {
throw new Error(`Invalid type "${visType}"`);
}
return type;
}
private getParams(params: VisParams) {
return defaults({}, cloneDeep(params || {}), cloneDeep(this.type.visConfig.defaults || {}));
}
setState(state: SerializedVis) {
let typeChanged = false;
if (state.type && this.type.name !== state.type) {
// @ts-ignore
this.type = this.getType(state.type);
typeChanged = true;
}
if (state.title !== undefined) {
this.title = state.title;
}
if (state.description !== undefined) {
this.description = state.description;
}
if (state.params || typeChanged) {
this.params = this.getParams(state.params);
}
// move to migration script
updateVisualizationConfig(state.params, this.params);
if (state.data && state.data.searchSource) {
this.data.searchSource = state.data.searchSource!;
this.data.indexPattern = this.data.searchSource.getField('index');
}
if (state.data && state.data.savedSearchId) {
this.data.savedSearchId = state.data.savedSearchId;
}
if (state.data && state.data.aggs) {
let configStates = state.data.aggs;
configStates = this.initializeDefaultsFromSchemas(configStates, this.type.schemas.all || []);
if (!this.data.indexPattern) {
if (state.data.aggs.length) {
throw new Error('trying to initialize aggs without index pattern');
}
return;
}
this.data.aggs = getAggs().createAggConfigs(this.data.indexPattern, configStates);
}
}
clone() {
return new Vis(this.type.name, this.serialize());
}
serialize(): SerializedVis {
const aggs = this.data.aggs ? this.data.aggs.aggs.map(agg => agg.toJSON()) : [];
const indexPattern = this.data.searchSource && this.data.searchSource.getField('index');
return {
id: this.id,
title: this.title,
type: this.type.name,
params: cloneDeep(this.params) as any,
uiState: this.uiState.toJSON(),
data: {
aggs: aggs as any,
indexPattern: indexPattern ? indexPattern.id : undefined,
searchSource: this.data.searchSource!.createCopy(),
savedSearchId: this.data.savedSearchId,
},
};
}
toAST() {
return this.type.toAST(this.params);
}
// deprecated
isHierarchical() {
if (isFunction(this.type.hierarchicalData)) {
return !!this.type.hierarchicalData(this);
} else {
return !!this.type.hierarchicalData;
}
}
private initializeDefaultsFromSchemas(configStates: AggConfigOptions[], schemas: any) {
// Set the defaults for any schema which has them. If the defaults
// for some reason has more then the max only set the max number
// of defaults (not sure why a someone define more...
// but whatever). Also if a schema.name is already set then don't
// set anything.
const newConfigs = [...configStates];
schemas
.filter((schema: any) => Array.isArray(schema.defaults) && schema.defaults.length > 0)
.filter((schema: any) => !configStates.find(agg => agg.schema && agg.schema === schema.name))
.forEach((schema: any) => {
const defaultSchemaConfig = schema.defaults.slice(0, schema.max);
defaultSchemaConfig.forEach((d: any) => newConfigs.push(d));
});
return newConfigs;
}
}

View file

@ -1,55 +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, VisState, VisParams } from './vis';
import { VisType } from './vis_types';
import { IAggConfig, IIndexPattern } from '../../../../../../plugins/data/public';
import { Schema } from '../../../../vis_default_editor/public';
type InitVisStateType =
| Partial<VisState>
| Partial<Omit<VisState, 'type'> & { type: string }>
| string;
export type VisImplConstructor = new (
indexPattern: IIndexPattern,
visState?: InitVisStateType
) => VisImpl;
export declare class VisImpl implements Vis {
constructor(indexPattern: IIndexPattern, visState?: InitVisStateType);
type: VisType;
getCurrentState: (
includeDisabled?: boolean
) => {
title: string;
type: string;
params: VisParams;
aggs: Array<{ [key: string]: any }>;
};
private initializeDefaultsFromSchemas(configStates: IAggConfig[], schemas: Schema[]);
// Since we haven't typed everything here yet, we basically "any" the rest
// of that interface. This should be removed as soon as this type definition
// has been completed. But that way we at least have typing for a couple of
// properties on that type.
[key: string]: any;
}

View file

@ -1,223 +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.
*/
/**
* @name Vis
*
* @description This class consists of aggs, params, listeners, title, and type.
* - Aggs: Instances of IAggConfig.
* - Params: The settings in the Options tab.
*
* Not to be confused with vislib/vis.js.
*/
import { EventEmitter } from 'events';
import _ from 'lodash';
import { PersistedState } from '../../../../../../../src/plugins/visualizations/public';
import { updateVisualizationConfig } from './legacy/vis_update';
import { getTypes, getAggs } from './services';
class VisImpl extends EventEmitter {
constructor(indexPattern, visState) {
super();
visState = visState || {};
if (_.isString(visState)) {
visState = {
type: visState,
};
}
this.indexPattern = indexPattern;
this._setUiState(new PersistedState());
this.setCurrentState(visState);
this.setState(this.getCurrentState(), false);
// Session state is for storing information that is transitory, and will not be saved with the visualization.
// For instance, map bounds, which depends on the view port, browser window size, etc.
this.sessionState = {};
this.API = {
events: {
filter: data => this.eventsSubject.next({ name: 'filterBucket', data }),
brush: data => this.eventsSubject.next({ name: 'brush', data }),
},
};
}
initializeDefaultsFromSchemas(configStates, schemas) {
// Set the defaults for any schema which has them. If the defaults
// for some reason has more then the max only set the max number
// of defaults (not sure why a someone define more...
// but whatever). Also if a schema.name is already set then don't
// set anything.
const newConfigs = [...configStates];
schemas
.filter(schema => Array.isArray(schema.defaults) && schema.defaults.length > 0)
.filter(schema => !configStates.find(agg => agg.schema && agg.schema === schema.name))
.forEach(schema => {
const defaults = schema.defaults.slice(0, schema.max);
defaults.forEach(d => newConfigs.push(d));
});
return newConfigs;
}
setCurrentState(state) {
this.title = state.title || '';
const type = state.type || this.type;
if (_.isString(type)) {
this.type = getTypes().get(type);
if (!this.type) {
throw new Error(`Invalid type "${type}"`);
}
} else {
this.type = type;
}
this.params = _.defaults(
{},
_.cloneDeep(state.params || {}),
_.cloneDeep(this.type.visConfig.defaults || {})
);
updateVisualizationConfig(state.params, this.params);
if (state.aggs || !this.aggs) {
let configStates = state.aggs ? state.aggs.aggs || state.aggs : [];
configStates = this.initializeDefaultsFromSchemas(configStates, this.type.schemas.all || []);
this.aggs = getAggs().createAggConfigs(this.indexPattern, configStates);
}
}
setState(state, updateCurrentState = true) {
this._state = _.cloneDeep(state);
if (updateCurrentState) {
this.setCurrentState(this._state);
}
}
setVisType(type) {
this.type.type = type;
}
updateState() {
this.setState(this.getCurrentState(true));
this.emit('update');
}
forceReload() {
this.emit('reload');
}
getCurrentState(includeDisabled) {
return {
title: this.title,
type: this.type.name,
params: _.cloneDeep(this.params),
aggs: this.aggs.aggs
.map(agg => agg.toJSON())
.filter(agg => includeDisabled || agg.enabled)
.filter(Boolean),
};
}
copyCurrentState(includeDisabled = false) {
const state = this.getCurrentState(includeDisabled);
state.aggs = getAggs().createAggConfigs(
this.indexPattern,
state.aggs.aggs || state.aggs,
this.type.schemas.all
);
return state;
}
getStateInternal(includeDisabled) {
return {
title: this._state.title,
type: this._state.type,
params: this._state.params,
aggs: this._state.aggs.filter(agg => includeDisabled || agg.enabled),
};
}
getEnabledState() {
return this.getStateInternal(false);
}
getAggConfig() {
return this.aggs.clone({ enabledOnly: true });
}
getState() {
return this.getStateInternal(true);
}
isHierarchical() {
if (_.isFunction(this.type.hierarchicalData)) {
return !!this.type.hierarchicalData(this);
} else {
return !!this.type.hierarchicalData;
}
}
hasSchemaAgg(schemaName, aggTypeName) {
const aggs = this.aggs.bySchemaName(schemaName) || [];
return aggs.some(function(agg) {
if (!agg.type || !agg.type.name) return false;
return agg.type.name === aggTypeName;
});
}
hasUiState() {
return !!this.__uiState;
}
/***
* this should not be used outside of visualize
* @param uiState
* @private
*/
_setUiState(uiState) {
if (uiState instanceof PersistedState) {
this.__uiState = uiState;
}
}
getUiState() {
return this.__uiState;
}
/**
* Currently this is only used to extract map-specific information
* (e.g. mapZoom, mapCenter).
*/
uiStateVal(key, val) {
if (this.hasUiState()) {
if (_.isUndefined(val)) {
return this.__uiState.get(key);
}
return this.__uiState.set(key, val);
}
return val;
}
}
VisImpl.prototype.type = 'histogram';
export { VisImpl };

View file

@ -328,6 +328,7 @@ export {
AggParamType,
AggTypeFieldFilters, // TODO convert to interface
AggTypeFilters, // TODO convert to interface
AggConfigOptions,
BUCKET_TYPES,
DateRangeKey, // only used in field formatter deserialization, which will live in data
IAggConfig,

View file

@ -54,6 +54,22 @@ import { Unit } from '@elastic/datemath';
import { UnregisterCallback } from 'history';
import { UserProvidedValues } from 'src/core/server/types';
// Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface AggConfigOptions {
// (undocumented)
enabled?: boolean;
// (undocumented)
id?: string;
// (undocumented)
params?: Record<string, any>;
// (undocumented)
schema?: string;
// (undocumented)
type: IAggType;
}
// Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1838,21 +1854,21 @@ export type TSearchStrategyProvider<T extends TStrategyTypes> = (context: ISearc
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:379:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts

View file

@ -85,7 +85,7 @@ export class PersistedState extends EventEmitter {
setSilent(key: PersistedStateKey | any, value?: any) {
const params = prepSetParams(key, value, this._path);
if (params.key) {
if (params.key || params.value) {
return this.setValue(params.key, params.value, true);
}
}