Skip to content

Commit eeba9c7

Browse files
Modularize render complete (#74504) (#75546)
* chore: 🤖 remove unused render-complete logic * feat: 🎸 add render-complete observables to IEmbeddable * Revert "chore: 🤖 remove unused render-complete logic" This reverts commit 0049c01. * 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> # Conflicts: # src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
1 parent 932e0b5 commit eeba9c7

File tree

7 files changed

+125
-53
lines changed

7 files changed

+125
-53
lines changed

‎src/plugins/discover/public/application/angular/directives/render_complete.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
* under the License.
1818
*/
1919
import { IScope } from 'angular';
20-
import { RenderCompleteHelper } from '../../../../../kibana_utils/public';
20+
import { RenderCompleteListener } from '../../../../../kibana_utils/public';
2121

2222
export function createRenderCompleteDirective() {
2323
return {
2424
controller($scope: IScope, $element: JQLite) {
2525
const el = $element[0];
26-
const renderCompleteHelper = new RenderCompleteHelper(el);
27-
$scope.$on('$destroy', renderCompleteHelper.destroy);
26+
const renderCompleteListener = new RenderCompleteListener(el);
27+
$scope.$on('$destroy', renderCompleteListener.destroy);
2828
},
2929
};
3030
}

‎src/plugins/embeddable/kibana.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
],
1010
"requiredBundles": [
1111
"savedObjects",
12-
"kibanaReact"
12+
"kibanaReact",
13+
"kibanaUtils"
1314
]
1415
}

‎src/plugins/embeddable/public/lib/embeddables/embeddable.tsx‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import { cloneDeep, isEqual } from 'lodash';
2121
import * as Rx from 'rxjs';
22+
import { distinctUntilChanged, map } from 'rxjs/operators';
23+
import { RenderCompleteDispatcher } from '../../../../kibana_utils/public';
2224
import { Adapters, ViewMode } from '../types';
2325
import { IContainer } from '../containers';
2426
import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable';
@@ -47,6 +49,8 @@ export abstract class Embeddable<
4749
private readonly input$: Rx.BehaviorSubject<TEmbeddableInput>;
4850
private readonly output$: Rx.BehaviorSubject<TEmbeddableOutput>;
4951

52+
protected renderComplete = new RenderCompleteDispatcher();
53+
5054
// Listener to parent changes, if this embeddable exists in a parent, in order
5155
// to update input when the parent changes.
5256
private parentSubscription?: Rx.Subscription;
@@ -77,6 +81,15 @@ export abstract class Embeddable<
7781
this.onResetInput(newInput);
7882
});
7983
}
84+
85+
this.getOutput$()
86+
.pipe(
87+
map(({ title }) => title || ''),
88+
distinctUntilChanged()
89+
)
90+
.subscribe((title) => {
91+
this.renderComplete.setTitle(title);
92+
});
8093
}
8194

8295
public getIsContainer(): this is IContainer {
@@ -105,8 +118,8 @@ export abstract class Embeddable<
105118
return this.input;
106119
}
107120

108-
public getTitle() {
109-
return this.output.title;
121+
public getTitle(): string {
122+
return this.output.title || '';
110123
}
111124

112125
/**
@@ -133,7 +146,10 @@ export abstract class Embeddable<
133146
}
134147
}
135148

136-
public render(domNode: HTMLElement | Element): void {
149+
public render(el: HTMLElement): void {
150+
this.renderComplete.setEl(el);
151+
this.renderComplete.setTitle(this.output.title || '');
152+
137153
if (this.destroyed) {
138154
throw new Error('Embeddable has been destroyed');
139155
}

‎src/plugins/kibana_utils/public/render_complete/index.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,5 @@
1717
* under the License.
1818
*/
1919

20-
const dispatchCustomEvent = (el: HTMLElement, eventName: string) => {
21-
// we're using the native events so that we aren't tied to the jQuery custom events,
22-
// otherwise we have to use jQuery(element).on(...) because jQuery's events sit on top
23-
// of the native events per https://github.com/jquery/jquery/issues/2476
24-
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
25-
};
26-
27-
export function dispatchRenderComplete(el: HTMLElement) {
28-
dispatchCustomEvent(el, 'renderComplete');
29-
}
30-
31-
export function dispatchRenderStart(el: HTMLElement) {
32-
dispatchCustomEvent(el, 'renderStart');
33-
}
34-
35-
export * from './render_complete_helper';
20+
export * from './render_complete_listener';
21+
export * from './render_complete_dispatcher';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
const dispatchEvent = (el: HTMLElement, eventName: string) => {
21+
el.dispatchEvent(new CustomEvent(eventName, { bubbles: true }));
22+
};
23+
24+
export function dispatchRenderComplete(el: HTMLElement) {
25+
dispatchEvent(el, 'renderComplete');
26+
}
27+
28+
export function dispatchRenderStart(el: HTMLElement) {
29+
dispatchEvent(el, 'renderStart');
30+
}
31+
32+
/**
33+
* Should call `dispatchComplete()` when UI block has finished loading its data and has
34+
* completely rendered. Should `dispatchInProgress()` every time UI block
35+
* starts loading data again. At the start it is assumed that UI block is loading
36+
* so it dispatches "in progress" automatically, so you need to call `setRenderComplete`
37+
* at least once.
38+
*
39+
* This is used for reporting to know that UI block is ready, so
40+
* it can take a screenshot. It is also used in functional tests to know that
41+
* page has stabilized.
42+
*/
43+
export class RenderCompleteDispatcher {
44+
private count: number = 0;
45+
private el?: HTMLElement;
46+
47+
constructor(el?: HTMLElement) {
48+
this.setEl(el);
49+
}
50+
51+
public setEl(el?: HTMLElement) {
52+
this.el = el;
53+
this.count = 0;
54+
if (el) this.dispatchInProgress();
55+
}
56+
57+
public dispatchInProgress() {
58+
if (!this.el) return;
59+
this.el.setAttribute('data-render-complete', 'false');
60+
this.el.setAttribute('data-rendering-count', String(this.count));
61+
dispatchRenderStart(this.el);
62+
}
63+
64+
public dispatchComplete() {
65+
if (!this.el) return;
66+
this.count++;
67+
this.el.setAttribute('data-render-complete', 'true');
68+
this.el.setAttribute('data-rendering-count', String(this.count));
69+
dispatchRenderComplete(this.el);
70+
}
71+
72+
public dispatchError() {
73+
if (!this.el) return;
74+
this.count++;
75+
this.el.setAttribute('data-render-complete', 'false');
76+
this.el.setAttribute('data-rendering-count', String(this.count));
77+
}
78+
79+
public setTitle(title: string) {
80+
if (!this.el) return;
81+
this.el.setAttribute('data-title', title);
82+
}
83+
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
* under the License.
1818
*/
1919

20-
const attributeName = 'data-render-complete';
20+
export class RenderCompleteListener {
21+
private readonly attributeName = 'data-render-complete';
2122

22-
export class RenderCompleteHelper {
2323
constructor(private readonly element: HTMLElement) {
2424
this.setup();
2525
}
@@ -30,23 +30,23 @@ export class RenderCompleteHelper {
3030
};
3131

3232
public setup = () => {
33-
this.element.setAttribute(attributeName, 'false');
33+
this.element.setAttribute(this.attributeName, 'false');
3434
this.element.addEventListener('renderStart', this.start);
3535
this.element.addEventListener('renderComplete', this.complete);
3636
};
3737

3838
public disable = () => {
39-
this.element.setAttribute(attributeName, 'disabled');
39+
this.element.setAttribute(this.attributeName, 'disabled');
4040
this.destroy();
4141
};
4242

4343
private start = () => {
44-
this.element.setAttribute(attributeName, 'false');
44+
this.element.setAttribute(this.attributeName, 'false');
4545
return true;
4646
};
4747

4848
private complete = () => {
49-
this.element.setAttribute(attributeName, 'true');
49+
this.element.setAttribute(this.attributeName, 'true');
5050
return true;
5151
};
5252
}

‎src/plugins/visualizations/public/embeddable/visualize_embeddable.ts‎

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
IContainer,
3737
Adapters,
3838
} from '../../../../plugins/embeddable/public';
39-
import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public';
4039
import {
4140
IExpressionLoaderParams,
4241
ExpressionsStart,
@@ -85,7 +84,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
8584
private timefilter: TimefilterContract;
8685
private timeRange?: TimeRange;
8786
private query?: Query;
88-
private title?: string;
8987
private filters?: Filter[];
9088
private visCustomizations?: Pick<VisualizeInput, 'vis' | 'table'>;
9189
private subscriptions: Subscription[] = [];
@@ -158,7 +156,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
158156
if (!adapters) return;
159157

160158
return this.deps.start().plugins.inspector.open(adapters, {
161-
title: this.getTitle() || '',
159+
title: this.getTitle(),
162160
});
163161
};
164162

@@ -216,11 +214,10 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
216214
dirty = true;
217215
}
218216

219-
if (this.output.title !== this.title) {
220-
this.title = this.output.title;
221-
if (this.domNode) {
222-
this.domNode.setAttribute('data-title', this.title || '');
223-
}
217+
// propagate the title to the output embeddable
218+
// but only when the visualization is in edit/Visualize mode
219+
if (!this.parent && this.vis.title !== this.output.title) {
220+
this.updateOutput({ title: this.vis.title });
224221
}
225222

226223
if (this.vis.description && this.domNode) {
@@ -237,26 +234,20 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
237234
hasInspector = () => Boolean(this.getInspectorAdapters());
238235

239236
onContainerLoading = () => {
240-
this.domNode.setAttribute('data-render-complete', 'false');
237+
this.renderComplete.dispatchInProgress();
241238
this.updateOutput({ loading: true, error: undefined });
242239
};
243240

244-
onContainerRender = (count: number) => {
245-
this.domNode.setAttribute('data-render-complete', 'true');
246-
this.domNode.setAttribute('data-rendering-count', count.toString());
241+
onContainerRender = () => {
242+
this.renderComplete.dispatchComplete();
247243
this.updateOutput({ loading: false, error: undefined });
248-
dispatchRenderComplete(this.domNode);
249244
};
250245

251246
onContainerError = (error: ExpressionRenderError) => {
252247
if (this.abortController) {
253248
this.abortController.abort();
254249
}
255-
this.domNode.setAttribute(
256-
'data-rendering-count',
257-
this.domNode.getAttribute('data-rendering-count') + 1
258-
);
259-
this.domNode.setAttribute('data-render-complete', 'false');
250+
this.renderComplete.dispatchError();
260251
this.updateOutput({ loading: false, error });
261252
};
262253

@@ -265,7 +256,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
265256
* @param {Element} domNode
266257
*/
267258
public async render(domNode: HTMLElement) {
268-
super.render(domNode);
269259
this.timeRange = _.cloneDeep(this.input.timeRange);
270260

271261
this.transferCustomizationsToUiState();
@@ -275,6 +265,7 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
275265
domNode.appendChild(div);
276266

277267
this.domNode = div;
268+
super.render(this.domNode);
278269

279270
const expressions = getExpressions();
280271
this.handler = new expressions.ExpressionLoader(this.domNode, undefined, {
@@ -323,19 +314,14 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
323314
})
324315
);
325316

326-
div.setAttribute('data-title', this.output.title || '');
327-
328317
if (this.vis.description) {
329318
div.setAttribute('data-description', this.vis.description);
330319
}
331320

332321
div.setAttribute('data-test-subj', 'visualizationLoader');
333322
div.setAttribute('data-shared-item', '');
334-
div.setAttribute('data-rendering-count', '0');
335-
div.setAttribute('data-render-complete', 'false');
336323

337324
this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading));
338-
339325
this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender));
340326

341327
this.updateHandler();

0 commit comments

Comments
 (0)