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:
Vadim Dalecky 2020-08-20 14:31:35 +02:00 committed by GitHub
parent bb8e5dc3a7
commit cd36188c40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 121 additions and 58 deletions

View file

@ -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);
},
};
}

View file

@ -12,6 +12,7 @@
],
"requiredBundles": [
"savedObjects",
"kibanaReact"
"kibanaReact",
"kibanaUtils"
]
}

View file

@ -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');
}

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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;
};
}

View file

@ -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();