use embeddables in visualize editor (#48744)

This commit is contained in:
Peter Pisljar 2019-10-29 12:00:35 +01:00 committed by GitHub
parent 51df8afd7e
commit c09e1cd477
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 154 deletions

View file

@ -72,14 +72,6 @@
index-patterns="[indexPattern]"
></filter-bar>
<apply-filters-popover
key="applyFiltersKey"
filters="state.$newFilters"
on-cancel="onCancelApplyFilters"
on-submit="onApplyFilters"
index-patterns="indexPatterns"
></apply-filters-popover>
<div
class="euiCallOut euiCallOut--primary euiCallOut--small hide-for-sharing"
ng-if="vis.type.shouldMarkAsExperimentalInUI()"
@ -90,7 +82,14 @@
</div>
</div>
<div class="visualize" ng-if="!chrome.getVisible()"/>
<visualization-embedded
ng-if="!chrome.getVisible()"
class="visualize"
saved-obj="savedVis"
ui-state="uiState"
time-range="timeRange"
filters="filters"
query="query"/>
<h1
class="euiScreenReaderOnly"
@ -107,7 +106,8 @@
saved-obj="savedVis"
ui-state="uiState"
time-range="timeRange"
filters="globalFilters"
filters="filters"
query="query"
class="visEditor__content"
/>

View file

@ -22,6 +22,7 @@ import { Subscription } from 'rxjs';
import { i18n } from '@kbn/i18n';
import '../saved_visualizations/saved_visualizations';
import './visualization_editor';
import './visualization';
import 'ui/vis/editors/default/sidebar';
import 'ui/visualize';
import 'ui/collapsible_sidebar';
@ -46,7 +47,6 @@ import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
import { timefilter } from 'ui/timefilter';
import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader';
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
@ -142,6 +142,7 @@ function VisEditor(
AppState,
$window,
$injector,
$timeout,
indexPatterns,
kbnUrl,
redirectWhenMissing,
@ -403,22 +404,21 @@ function VisEditor(
$appStatus.dirty = status.dirty || !savedVis.id;
});
$scope.$watch('state.query', (newQuery) => {
const query = migrateLegacyQuery(newQuery);
$scope.updateQueryAndFetch({ query });
$scope.$watch('state.query', (newQuery, oldQuery) => {
if (!_.isEqual(newQuery, oldQuery)) {
const query = migrateLegacyQuery(newQuery);
if (!_.isEqual(query, newQuery)) {
$state.query = query;
}
$scope.fetch();
}
});
$state.replace();
const updateTimeRange = () => {
$scope.timeRange = timefilter.getTime();
// In case we are running in embedded mode (i.e. we used the visualize loader to embed)
// the visualization, we need to update the timeRange on the visualize handler.
if ($scope._handler) {
$scope._handler.update({
timeRange: $scope.timeRange,
});
}
$scope.$broadcast('render');
};
const subscriptions = new Subscription();
@ -435,9 +435,10 @@ function VisEditor(
// update the searchSource when query updates
$scope.fetch = function () {
$state.save();
$scope.query = $state.query;
savedVis.searchSource.setField('query', $state.query);
savedVis.searchSource.setField('filter', $state.filters);
$scope.vis.forceReload();
$scope.$broadcast('render');
};
// update the searchSource when filters update
@ -460,16 +461,8 @@ function VisEditor(
subscriptions.unsubscribe();
});
if (!$scope.chrome.getVisible()) {
getVisualizeLoader().then(loader => {
$scope._handler = loader.embedVisualizationWithSavedObject($element.find('.visualize')[0], savedVis, {
timeRange: $scope.timeRange,
uiState: $scope.uiState,
appState: $state,
listenOnChange: false
});
});
}
$timeout(() => { $scope.$broadcast('render'); });
}
$scope.updateQueryAndFetch = function ({ query, dateRange }) {
@ -482,7 +475,9 @@ function VisEditor(
timefilter.setTime(dateRange);
// If nothing has changed, trigger the fetch manually, otherwise it will happen as a result of the changes
if (!isUpdate) $scope.fetch();
if (!isUpdate) {
$scope.vis.forceReload();
}
};
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {

View file

@ -0,0 +1,68 @@
/*
* 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 { uiModules } from 'ui/modules';
import 'angular-sanitize';
import { start as embeddables } from '../../../../../core_plugins/embeddable_api/public/np_ready/public/legacy';
uiModules
.get('kibana/directive', ['ngSanitize'])
.directive('visualizationEmbedded', function (Private, $timeout, getAppState) {
return {
restrict: 'E',
scope: {
savedObj: '=',
uiState: '=?',
timeRange: '=',
filters: '=',
query: '=',
},
link: function ($scope, element) {
$scope.renderFunction = async () => {
if (!$scope._handler) {
$scope._handler = await embeddables.getEmbeddableFactory('visualization').createFromObject($scope.savedObj, {
timeRange: $scope.timeRange,
filters: $scope.filters || [],
query: $scope.query,
appState: getAppState(),
uiState: $scope.uiState,
});
$scope._handler.render(element[0]);
} else {
$scope._handler.updateInput({
timeRange: $scope.timeRange,
filters: $scope.filters || [],
query: $scope.query,
});
}
};
$scope.$on('render', (event) => {
event.preventDefault();
$timeout(() => { $scope.renderFunction(); });
});
$scope.$on('$destroy', () => {
$scope._handler.destroy();
});
}
};
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { debounce } from 'lodash';
import { uiModules } from 'ui/modules';
import 'angular-sanitize';
import { VisEditorTypesRegistryProvider } from 'ui/registry/vis_editor_types';
@ -34,6 +33,7 @@ uiModules
uiState: '=?',
timeRange: '=',
filters: '=',
query: '=',
},
link: function ($scope, element) {
const editorType = $scope.savedObj.vis.type.editor;
@ -46,6 +46,7 @@ uiModules
uiState: $scope.uiState,
timeRange: $scope.timeRange,
filters: $scope.filters,
query: $scope.query,
appState: getAppState(),
});
};
@ -58,10 +59,6 @@ uiModules
$scope.$on('$destroy', () => {
editor.destroy();
});
$scope.$watchGroup(['timeRange', 'filters'], debounce(() => {
$scope.renderFunction();
}, 100));
}
};
});

View file

@ -22,6 +22,7 @@ import { StaticIndexPattern } from 'ui/index_patterns';
import { PersistedState } from 'ui/persisted_state';
import { VisualizeLoader } from 'ui/visualize/loader';
import { EmbeddedVisualizeHandler } from 'ui/visualize/loader/embedded_visualize_handler';
import { AppState } from 'ui/state_management/app_state';
import {
VisSavedObject,
VisualizeLoaderParams,
@ -48,6 +49,8 @@ export interface VisualizeEmbeddableConfiguration {
editUrl: string;
loader: VisualizeLoader;
editable: boolean;
appState?: AppState;
uiState?: PersistedState;
}
export interface VisualizeInput extends EmbeddableInput {
@ -57,6 +60,8 @@ export interface VisualizeInput extends EmbeddableInput {
vis?: {
colors?: { [key: string]: string };
};
appState?: AppState;
uiState?: PersistedState;
}
export interface VisualizeOutput extends EmbeddableOutput {
@ -69,6 +74,7 @@ export interface VisualizeOutput extends EmbeddableOutput {
export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOutput> {
private savedVisualization: VisSavedObject;
private loader: VisualizeLoader;
private appState: AppState | undefined;
private uiState: PersistedState;
private handler?: EmbeddedVisualizeHandler;
private timeRange?: TimeRange;
@ -86,6 +92,8 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
editUrl,
indexPatterns,
editable,
appState,
uiState,
}: VisualizeEmbeddableConfiguration,
initialInput: VisualizeInput,
parent?: Container
@ -105,12 +113,18 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
this.savedVisualization = savedVisualization;
this.loader = loader;
const parsedUiState = savedVisualization.uiStateJSON
? JSON.parse(savedVisualization.uiStateJSON)
: {};
this.uiState = new PersistedState(parsedUiState);
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.uiState.on('change', this.uiStateChangeHandler);
}
this.appState = appState;
this.subscription = Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => {
this.handleChanges();
@ -149,7 +163,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
});
this.uiState.on('change', this.uiStateChangeHandler);
}
} else {
} else if (!this.appState) {
this.uiState.clearAllKeys();
}
}
@ -211,6 +225,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
}
const handlerParams: VisualizeLoaderParams = {
appState: this.appState,
uiState: this.uiState,
// Append visualization to container instead of replacing its content
append: true,

View file

@ -60,6 +60,7 @@ import { getIndexPattern } from './get_index_pattern';
import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable';
import { VISUALIZE_EMBEDDABLE_TYPE } from './constants';
import { TypesStart } from '../../../../visualizations/public/np_ready/public/types';
import { VisSavedObject } from '../../../../../ui/public/visualize/loader/types';
interface VisualizationAttributes extends SavedObjectAttributes {
visState: string;
@ -130,8 +131,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
});
}
public async createFromSavedObject(
savedObjectId: string,
public async createFromObject(
savedObject: VisSavedObject,
input: Partial<VisualizeInput> & { id: string },
parent?: Container
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
@ -140,11 +141,12 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
const savedVisualizations = $injector.get<SavedVisualizations>('savedVisualizations');
try {
const visId = savedObjectId;
const visId = savedObject.id as string;
const editUrl = chrome.addBasePath(`/app/kibana${savedVisualizations.urlFor(visId)}`);
const editUrl = visId
? chrome.addBasePath(`/app/kibana${savedVisualizations.urlFor(visId)}`)
: '';
const loader = await getVisualizeLoader();
const savedObject = await savedVisualizations.get(visId);
const isLabsEnabled = config.get<boolean>('visualize:enableLabs');
if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') {
@ -160,6 +162,8 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
indexPatterns,
editUrl,
editable: this.isEditable(),
appState: input.appState,
uiState: input.uiState,
},
input,
parent
@ -170,6 +174,25 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory<
}
}
public async createFromSavedObject(
savedObjectId: string,
input: Partial<VisualizeInput> & { id: string },
parent?: Container
): Promise<VisualizeEmbeddable | ErrorEmbeddable | DisabledLabEmbeddable> {
const $injector = await chrome.dangerouslyGetActiveInjector();
const savedVisualizations = $injector.get<SavedVisualizations>('savedVisualizations');
try {
const visId = savedObjectId;
const savedObject = await savedVisualizations.get(visId);
return this.createFromObject(savedObject, input, parent);
} catch (e) {
console.error(e); // eslint-disable-line no-console
return new ErrorEmbeddable(e, input, parent);
}
}
public async create() {
// TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up
// to allow for in place creation of visualizations without having to navigate away to a new URL.

View file

@ -18,9 +18,8 @@
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { get, isEqual } from 'lodash';
import { get } from 'lodash';
import { keyCodes, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
import { getVisualizeLoader } from 'ui/visualize/loader/visualize_loader';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import {
getInterval,
@ -30,6 +29,7 @@ import {
AUTO_INTERVAL,
} from './lib/get_interval';
import { PANEL_TYPES } from '../../common/panel_types';
import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy';
const MIN_CHART_HEIGHT = 300;
@ -65,23 +65,21 @@ class VisEditorVisualizationUI extends Component {
};
async _loadVisualization() {
const loader = await getVisualizeLoader();
if (!this._visEl.current) {
// In case the visualize loader isn't done before the component is unmounted.
return;
}
const { uiState, timeRange, appState, savedObj, onDataChange } = this.props;
const { timeRange, appState, savedObj, onDataChange } = this.props;
this._handler = loader.embedVisualizationWithSavedObject(this._visEl.current, savedObj, {
listenOnChange: false,
uiState,
timeRange,
appState,
this._handler = await embeddables.getEmbeddableFactory('visualization').createFromObject(savedObj, {
vis: {},
timeRange: timeRange,
filters: appState.filters || [],
});
this._handler.render(this._visEl.current);
this._subscription = this._handler.data$.subscribe(data => {
this._subscription = this._handler.handler.data$.subscribe(data => {
this.setPanelInterval(data.visData);
onDataChange(data);
});
@ -152,10 +150,12 @@ class VisEditorVisualizationUI extends Component {
this._loadVisualization();
}
componentDidUpdate(prevProps) {
if (this._handler && !isEqual(this.props.timeRange, prevProps.timeRange)) {
this._handler.update({
componentDidUpdate() {
if (this._handler) {
this._handler.updateInput({
timeRange: this.props.timeRange,
filters: this.props.filters || [],
query: this.props.query,
});
}
}

View file

@ -1,71 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
jest.mock('ui/visualize/loader/visualize_loader', () => ({}));
jest.mock('ui/new_platform');
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { VisEditorVisualization } from './vis_editor_visualization';
describe('getVisualizeLoader', () => {
let updateStub;
beforeEach(() => {
updateStub = jest.fn();
const handlerMock = {
update: updateStub,
data$: {
subscribe: () => {},
},
};
const loaderMock = {
embedVisualizationWithSavedObject: () => handlerMock,
};
require('ui/visualize/loader/visualize_loader').getVisualizeLoader = async () => loaderMock;
});
it('should not call _handler.update until getVisualizeLoader returns _handler', async () => {
const wrapper = mountWithIntl(<VisEditorVisualization />);
// Set prop to force DOM change and componentDidUpdate to be triggered
wrapper.setProps({
timeRange: {
from: '2019-03-20T20:35:37.637Z',
to: '2019-03-23T18:40:16.486Z',
},
});
expect(updateStub).not.toHaveBeenCalled();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Set prop to force DOM change and componentDidUpdate to be triggered
wrapper.setProps({
timeRange: {
from: 'now/d',
to: 'now/d',
},
});
expect(updateStub).toHaveBeenCalled();
});
});

View file

@ -34,10 +34,11 @@ import { parentPipelineAggHelper } from 'ui/agg_types/metrics/lib/parent_pipelin
import { DefaultEditorSize } from '../../editor_size';
import { VisEditorTypesRegistryProvider } from '../../../registry/vis_editor_types';
import { getVisualizeLoader } from '../../../visualize/loader/visualize_loader';
import { AggGroupNames } from './agg_groups';
const defaultEditor = function ($rootScope, $compile) {
import { start as embeddables } from '../../../../../core_plugins/embeddable_api/public/np_ready/public/legacy';
const defaultEditor = function ($rootScope, $compile, getAppState) {
return class DefaultEditor {
static key = 'default';
@ -57,7 +58,7 @@ const defaultEditor = function ($rootScope, $compile) {
}
}
render({ uiState, timeRange, filters, appState }) {
render({ uiState, timeRange, filters, query }) {
let $scope;
const updateScope = () => {
@ -66,7 +67,7 @@ const defaultEditor = function ($rootScope, $compile) {
//$scope.$apply();
};
return new Promise(resolve => {
return new Promise(async (resolve) => {
if (!this.$scope) {
this.$scope = $scope = $rootScope.$new();
@ -157,23 +158,21 @@ const defaultEditor = function ($rootScope, $compile) {
if (!this._handler) {
const visualizationEl = this.el.find('.visEditor__canvas')[0];
getVisualizeLoader().then(loader => {
if (!visualizationEl) {
return;
}
this._loader = loader;
this._handler = this._loader.embedVisualizationWithSavedObject(visualizationEl, this.savedObj, {
uiState: uiState,
listenOnChange: false,
timeRange: timeRange,
filters: filters,
appState: appState,
});
});
} else {
this._handler.update({
this._handler = await embeddables.getEmbeddableFactory('visualization').createFromObject(this.savedObj, {
uiState: uiState,
appState: getAppState(),
timeRange: timeRange,
filters: filters,
filters: filters || [],
query: query,
});
this._handler.render(visualizationEl);
} else {
this._handler.updateInput({
timeRange: timeRange,
filters: filters || [],
query: query,
});
}