mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Modularize render complete (#74504)
* chore: 🤖 remove unused render-complete logic
* feat: 🎸 add render-complete observables to IEmbeddable
* Revert "chore: 🤖 remove unused render-complete logic"
This reverts commit 0049c01fbd
.
* refactor: 💡 rename render complete "helper" to "listener"
* feat: 🎸 add render complete dispatcher to embeddable
* refactor: 💡 move data-title setup to Embeddable
* refactor: 💡 move embeddable data-title setting to render-compl
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
bb8e5dc3a7
commit
cd36188c40
7 changed files with 121 additions and 58 deletions
|
@ -17,14 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { IScope } from 'angular';
|
||||
import { RenderCompleteHelper } from '../../../../../kibana_utils/public';
|
||||
import { RenderCompleteListener } from '../../../../../kibana_utils/public';
|
||||
|
||||
export function createRenderCompleteDirective() {
|
||||
return {
|
||||
controller($scope: IScope, $element: JQLite) {
|
||||
const el = $element[0];
|
||||
const renderCompleteHelper = new RenderCompleteHelper(el);
|
||||
$scope.$on('$destroy', renderCompleteHelper.destroy);
|
||||
const renderCompleteListener = new RenderCompleteListener(el);
|
||||
$scope.$on('$destroy', renderCompleteListener.destroy);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
],
|
||||
"requiredBundles": [
|
||||
"savedObjects",
|
||||
"kibanaReact"
|
||||
"kibanaReact",
|
||||
"kibanaUtils"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { RenderCompleteDispatcher } from '../../../../kibana_utils/public';
|
||||
import { Adapters, ViewMode } from '../types';
|
||||
import { IContainer } from '../containers';
|
||||
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
|
||||
|
@ -47,6 +49,8 @@ export abstract class Embeddable<
|
|||
private readonly input$: Rx.BehaviorSubject<TEmbeddableInput>;
|
||||
private readonly output$: Rx.BehaviorSubject<TEmbeddableOutput>;
|
||||
|
||||
protected renderComplete = new RenderCompleteDispatcher();
|
||||
|
||||
// Listener to parent changes, if this embeddable exists in a parent, in order
|
||||
// to update input when the parent changes.
|
||||
private parentSubscription?: Rx.Subscription;
|
||||
|
@ -77,6 +81,15 @@ export abstract class Embeddable<
|
|||
this.onResetInput(newInput);
|
||||
});
|
||||
}
|
||||
|
||||
this.getOutput$()
|
||||
.pipe(
|
||||
map(({ title }) => title || ''),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((title) => {
|
||||
this.renderComplete.setTitle(title);
|
||||
});
|
||||
}
|
||||
|
||||
public getIsContainer(): this is IContainer {
|
||||
|
@ -105,8 +118,8 @@ export abstract class Embeddable<
|
|||
return this.input;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return this.output.title;
|
||||
public getTitle(): string {
|
||||
return this.output.title || '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,7 +146,10 @@ export abstract class Embeddable<
|
|||
}
|
||||
}
|
||||
|
||||
public render(domNode: HTMLElement | Element): void {
|
||||
public render(el: HTMLElement): void {
|
||||
this.renderComplete.setEl(el);
|
||||
this.renderComplete.setTitle(this.output.title || '');
|
||||
|
||||
if (this.destroyed) {
|
||||
throw new Error('Embeddable has been destroyed');
|
||||
}
|
||||
|
|
|
@ -17,19 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const dispatchCustomEvent = (el: HTMLElement, eventName: string) => {
|
||||
// we're using the native events so that we aren't tied to the jQuery custom events,
|
||||
// otherwise we have to use jQuery(element).on(...) because jQuery's events sit on top
|
||||
// of the native events per https://github.com/jquery/jquery/issues/2476
|
||||
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
|
||||
};
|
||||
|
||||
export function dispatchRenderComplete(el: HTMLElement) {
|
||||
dispatchCustomEvent(el, 'renderComplete');
|
||||
}
|
||||
|
||||
export function dispatchRenderStart(el: HTMLElement) {
|
||||
dispatchCustomEvent(el, 'renderStart');
|
||||
}
|
||||
|
||||
export * from './render_complete_helper';
|
||||
export * from './render_complete_listener';
|
||||
export * from './render_complete_dispatcher';
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const dispatchEvent = (el: HTMLElement, eventName: string) => {
|
||||
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
|
||||
};
|
||||
|
||||
export function dispatchRenderComplete(el: HTMLElement) {
|
||||
dispatchEvent(el, 'renderComplete');
|
||||
}
|
||||
|
||||
export function dispatchRenderStart(el: HTMLElement) {
|
||||
dispatchEvent(el, 'renderStart');
|
||||
}
|
||||
|
||||
/**
|
||||
* Should call `dispatchComplete()` when UI block has finished loading its data and has
|
||||
* completely rendered. Should `dispatchInProgress()` every time UI block
|
||||
* starts loading data again. At the start it is assumed that UI block is loading
|
||||
* so it dispatches "in progress" automatically, so you need to call `setRenderComplete`
|
||||
* at least once.
|
||||
*
|
||||
* This is used for reporting to know that UI block is ready, so
|
||||
* it can take a screenshot. It is also used in functional tests to know that
|
||||
* page has stabilized.
|
||||
*/
|
||||
export class RenderCompleteDispatcher {
|
||||
private count: number = 0;
|
||||
private el?: HTMLElement;
|
||||
|
||||
constructor(el?: HTMLElement) {
|
||||
this.setEl(el);
|
||||
}
|
||||
|
||||
public setEl(el?: HTMLElement) {
|
||||
this.el = el;
|
||||
this.count = 0;
|
||||
if (el) this.dispatchInProgress();
|
||||
}
|
||||
|
||||
public dispatchInProgress() {
|
||||
if (!this.el) return;
|
||||
this.el.setAttribute('data-render-complete', 'false');
|
||||
this.el.setAttribute('data-rendering-count', String(this.count));
|
||||
dispatchRenderStart(this.el);
|
||||
}
|
||||
|
||||
public dispatchComplete() {
|
||||
if (!this.el) return;
|
||||
this.count++;
|
||||
this.el.setAttribute('data-render-complete', 'true');
|
||||
this.el.setAttribute('data-rendering-count', String(this.count));
|
||||
dispatchRenderComplete(this.el);
|
||||
}
|
||||
|
||||
public dispatchError() {
|
||||
if (!this.el) return;
|
||||
this.count++;
|
||||
this.el.setAttribute('data-render-complete', 'false');
|
||||
this.el.setAttribute('data-rendering-count', String(this.count));
|
||||
}
|
||||
|
||||
public setTitle(title: string) {
|
||||
if (!this.el) return;
|
||||
this.el.setAttribute('data-title', title);
|
||||
}
|
||||
}
|
|
@ -17,9 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
const attributeName = 'data-render-complete';
|
||||
export class RenderCompleteListener {
|
||||
private readonly attributeName = 'data-render-complete';
|
||||
|
||||
export class RenderCompleteHelper {
|
||||
constructor(private readonly element: HTMLElement) {
|
||||
this.setup();
|
||||
}
|
||||
|
@ -30,23 +30,23 @@ export class RenderCompleteHelper {
|
|||
};
|
||||
|
||||
public setup = () => {
|
||||
this.element.setAttribute(attributeName, 'false');
|
||||
this.element.setAttribute(this.attributeName, 'false');
|
||||
this.element.addEventListener('renderStart', this.start);
|
||||
this.element.addEventListener('renderComplete', this.complete);
|
||||
};
|
||||
|
||||
public disable = () => {
|
||||
this.element.setAttribute(attributeName, 'disabled');
|
||||
this.element.setAttribute(this.attributeName, 'disabled');
|
||||
this.destroy();
|
||||
};
|
||||
|
||||
private start = () => {
|
||||
this.element.setAttribute(attributeName, 'false');
|
||||
this.element.setAttribute(this.attributeName, 'false');
|
||||
return true;
|
||||
};
|
||||
|
||||
private complete = () => {
|
||||
this.element.setAttribute(attributeName, 'true');
|
||||
this.element.setAttribute(this.attributeName, 'true');
|
||||
return true;
|
||||
};
|
||||
}
|
|
@ -36,7 +36,6 @@ import {
|
|||
IContainer,
|
||||
Adapters,
|
||||
} from '../../../../plugins/embeddable/public';
|
||||
import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public';
|
||||
import {
|
||||
IExpressionLoaderParams,
|
||||
ExpressionsStart,
|
||||
|
@ -85,7 +84,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
private timefilter: TimefilterContract;
|
||||
private timeRange?: TimeRange;
|
||||
private query?: Query;
|
||||
private title?: string;
|
||||
private filters?: Filter[];
|
||||
private visCustomizations?: Pick<VisualizeInput, 'vis' | 'table'>;
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
@ -158,7 +156,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
if (!adapters) return;
|
||||
|
||||
return this.deps.start().plugins.inspector.open(adapters, {
|
||||
title: this.getTitle() || this.title || '',
|
||||
title: this.getTitle(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -222,16 +220,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
this.updateOutput({ title: this.vis.title });
|
||||
}
|
||||
|
||||
// Keep title depending on the output Embeddable to decouple the
|
||||
// visual appearance of the title and the actual title content (useful in Dashboard)
|
||||
if (this.output.title !== this.title) {
|
||||
this.title = this.output.title;
|
||||
|
||||
if (this.domNode) {
|
||||
this.domNode.setAttribute('data-title', this.title || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.vis.description && this.domNode) {
|
||||
this.domNode.setAttribute('data-description', this.vis.description);
|
||||
}
|
||||
|
@ -246,26 +234,20 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
hasInspector = () => Boolean(this.getInspectorAdapters());
|
||||
|
||||
onContainerLoading = () => {
|
||||
this.domNode.setAttribute('data-render-complete', 'false');
|
||||
this.renderComplete.dispatchInProgress();
|
||||
this.updateOutput({ loading: true, error: undefined });
|
||||
};
|
||||
|
||||
onContainerRender = (count: number) => {
|
||||
this.domNode.setAttribute('data-render-complete', 'true');
|
||||
this.domNode.setAttribute('data-rendering-count', count.toString());
|
||||
onContainerRender = () => {
|
||||
this.renderComplete.dispatchComplete();
|
||||
this.updateOutput({ loading: false, error: undefined });
|
||||
dispatchRenderComplete(this.domNode);
|
||||
};
|
||||
|
||||
onContainerError = (error: ExpressionRenderError) => {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
}
|
||||
this.domNode.setAttribute(
|
||||
'data-rendering-count',
|
||||
this.domNode.getAttribute('data-rendering-count') + 1
|
||||
);
|
||||
this.domNode.setAttribute('data-render-complete', 'false');
|
||||
this.renderComplete.dispatchError();
|
||||
this.updateOutput({ loading: false, error });
|
||||
};
|
||||
|
||||
|
@ -274,7 +256,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
* @param {Element} domNode
|
||||
*/
|
||||
public async render(domNode: HTMLElement) {
|
||||
super.render(domNode);
|
||||
this.timeRange = _.cloneDeep(this.input.timeRange);
|
||||
|
||||
this.transferCustomizationsToUiState();
|
||||
|
@ -284,6 +265,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
domNode.appendChild(div);
|
||||
|
||||
this.domNode = div;
|
||||
super.render(this.domNode);
|
||||
|
||||
const expressions = getExpressions();
|
||||
this.handler = new expressions.ExpressionLoader(this.domNode, undefined, {
|
||||
|
@ -332,19 +314,14 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
})
|
||||
);
|
||||
|
||||
div.setAttribute('data-title', this.output.title || '');
|
||||
|
||||
if (this.vis.description) {
|
||||
div.setAttribute('data-description', this.vis.description);
|
||||
}
|
||||
|
||||
div.setAttribute('data-test-subj', 'visualizationLoader');
|
||||
div.setAttribute('data-shared-item', '');
|
||||
div.setAttribute('data-rendering-count', '0');
|
||||
div.setAttribute('data-render-complete', 'false');
|
||||
|
||||
this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading));
|
||||
|
||||
this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender));
|
||||
|
||||
this.updateHandler();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue