mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Embeddable Refactor] Publish phase events (#175245)
Makes the Legacy Embeddable API properly implement the `publishesPhaseEvents` interface.
This commit is contained in:
parent
d3c51c45eb
commit
290bc8b359
7 changed files with 97 additions and 11 deletions
|
@ -18,11 +18,11 @@ export {
|
|||
type CanAccessViewMode,
|
||||
} from './interfaces/can_access_view_mode';
|
||||
export {
|
||||
apiFiresPhaseEvents,
|
||||
type FiresPhaseEvents,
|
||||
apiPublishesPhaseEvents,
|
||||
type PublishesPhaseEvents,
|
||||
type PhaseEvent,
|
||||
type PhaseEventType,
|
||||
} from './interfaces/fires_phase_events';
|
||||
} from './interfaces/publishes_phase_events';
|
||||
export { hasEditCapabilities, type HasEditCapabilities } from './interfaces/has_edit_capabilities';
|
||||
export { apiHasParentApi, type HasParentApi } from './interfaces/has_parent_api';
|
||||
export {
|
||||
|
|
|
@ -20,10 +20,12 @@ export interface PhaseEvent {
|
|||
timeToEvent: number;
|
||||
}
|
||||
|
||||
export interface FiresPhaseEvents {
|
||||
onPhaseChange: PublishingSubject<PhaseEvent>;
|
||||
export interface PublishesPhaseEvents {
|
||||
onPhaseChange: PublishingSubject<PhaseEvent | undefined>;
|
||||
}
|
||||
|
||||
export const apiFiresPhaseEvents = (unknownApi: null | unknown): unknownApi is FiresPhaseEvents => {
|
||||
return Boolean(unknownApi && (unknownApi as FiresPhaseEvents)?.onPhaseChange !== undefined);
|
||||
export const apiPublishesPhaseEvents = (
|
||||
unknownApi: null | unknown
|
||||
): unknownApi is PublishesPhaseEvents => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesPhaseEvents)?.onPhaseChange !== undefined);
|
||||
};
|
|
@ -10,8 +10,10 @@ import { DataView } from '@kbn/data-views-plugin/common';
|
|||
import { AggregateQuery, compareFilters, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { ErrorLike } from '@kbn/expressions-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PhaseEvent, PhaseEventType } from '@kbn/presentation-publishing';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { isNil } from 'lodash';
|
||||
import { BehaviorSubject, map, Subscription, distinct } from 'rxjs';
|
||||
import { embeddableStart } from '../../../kibana_services';
|
||||
import { isFilterableEmbeddable } from '../../filterable_embeddable';
|
||||
import {
|
||||
|
@ -40,6 +42,18 @@ function isVisualizeEmbeddable(
|
|||
return embeddable.type === 'visualization';
|
||||
}
|
||||
|
||||
const getEventStatus = (output: EmbeddableOutput): PhaseEventType => {
|
||||
if (!isNil(output.error)) {
|
||||
return 'error';
|
||||
} else if (output.rendered === true) {
|
||||
return 'rendered';
|
||||
} else if (output.loading === false) {
|
||||
return 'loaded';
|
||||
} else {
|
||||
return 'loading';
|
||||
}
|
||||
};
|
||||
|
||||
export const legacyEmbeddableToApi = (
|
||||
embeddable: CommonLegacyEmbeddable
|
||||
): { api: Omit<LegacyEmbeddableAPI, 'type' | 'getInspectorAdapters'>; destroyAPI: () => void } => {
|
||||
|
@ -66,6 +80,42 @@ export const legacyEmbeddableToApi = (
|
|||
});
|
||||
const isEditingEnabled = () => canEditEmbeddable(embeddable);
|
||||
|
||||
/**
|
||||
* Performance tracking
|
||||
*/
|
||||
const onPhaseChange = new BehaviorSubject<PhaseEvent | undefined>(undefined);
|
||||
|
||||
let loadingStartTime = 0;
|
||||
subscriptions.add(
|
||||
embeddable
|
||||
.getOutput$()
|
||||
.pipe(
|
||||
// Map loaded event properties
|
||||
map((output) => {
|
||||
if (output.loading === true) {
|
||||
loadingStartTime = performance.now();
|
||||
}
|
||||
return {
|
||||
id: embeddable.id,
|
||||
status: getEventStatus(output),
|
||||
error: output.error,
|
||||
};
|
||||
}),
|
||||
// Dedupe
|
||||
distinct((output) => loadingStartTime + output.id + output.status + !!output.error),
|
||||
// Map loaded event properties
|
||||
map((output): PhaseEvent => {
|
||||
return {
|
||||
...output,
|
||||
timeToEvent: performance.now() - loadingStartTime,
|
||||
};
|
||||
})
|
||||
)
|
||||
.subscribe((statusOutput) => {
|
||||
onPhaseChange.next(statusOutput);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Publish state for Presentation panel
|
||||
*/
|
||||
|
@ -155,6 +205,8 @@ export const legacyEmbeddableToApi = (
|
|||
dataLoading,
|
||||
blockingError,
|
||||
|
||||
onPhaseChange,
|
||||
|
||||
onEdit,
|
||||
isEditingEnabled,
|
||||
getTypeDisplayName,
|
||||
|
|
|
@ -43,6 +43,17 @@ class OutputTestEmbeddable extends Embeddable<EmbeddableInput, Output> {
|
|||
reload() {}
|
||||
}
|
||||
|
||||
class PhaseTestEmbeddable extends Embeddable<EmbeddableInput, EmbeddableOutput> {
|
||||
public readonly type = 'phaseTest';
|
||||
constructor() {
|
||||
super({ id: 'phaseTest', viewMode: ViewMode.VIEW }, {});
|
||||
}
|
||||
public reportsEmbeddableLoad(): boolean {
|
||||
return true;
|
||||
}
|
||||
reload() {}
|
||||
}
|
||||
|
||||
test('Embeddable calls input subscribers when changed', (done) => {
|
||||
const hello = new ContactCardEmbeddable(
|
||||
{ id: '123', firstName: 'Brienne', lastName: 'Tarth' },
|
||||
|
@ -104,6 +115,21 @@ test('updating output state retains instance information', async () => {
|
|||
expect(outputTest.getOutput().testClass).toBeInstanceOf(TestClass);
|
||||
});
|
||||
|
||||
test('fires phase events when output changes', async () => {
|
||||
const phaseEventTest = new PhaseTestEmbeddable();
|
||||
let phaseEventCount = 0;
|
||||
phaseEventTest.onPhaseChange.subscribe((event) => {
|
||||
if (event) {
|
||||
phaseEventCount++;
|
||||
}
|
||||
});
|
||||
expect(phaseEventCount).toBe(1); // loading is true by default which fires an event.
|
||||
phaseEventTest.updateOutput({ loading: false });
|
||||
expect(phaseEventCount).toBe(2);
|
||||
phaseEventTest.updateOutput({ rendered: true });
|
||||
expect(phaseEventCount).toBe(3);
|
||||
});
|
||||
|
||||
test('updated$ called after reload and batches input/output changes', async () => {
|
||||
const hello = new ContactCardEmbeddable(
|
||||
{ id: '123', firstName: 'Brienne', lastName: 'Tarth' },
|
||||
|
|
|
@ -126,6 +126,7 @@ export abstract class Embeddable<
|
|||
dataLoading: this.dataLoading,
|
||||
localFilters: this.localFilters,
|
||||
blockingError: this.blockingError,
|
||||
onPhaseChange: this.onPhaseChange,
|
||||
setPanelTitle: this.setPanelTitle,
|
||||
linkToLibrary: this.linkToLibrary,
|
||||
hidePanelTitle: this.hidePanelTitle,
|
||||
|
@ -164,6 +165,7 @@ export abstract class Embeddable<
|
|||
public panelTitle: LegacyEmbeddableAPI['panelTitle'];
|
||||
public dataLoading: LegacyEmbeddableAPI['dataLoading'];
|
||||
public localFilters: LegacyEmbeddableAPI['localFilters'];
|
||||
public onPhaseChange: LegacyEmbeddableAPI['onPhaseChange'];
|
||||
public linkToLibrary: LegacyEmbeddableAPI['linkToLibrary'];
|
||||
public blockingError: LegacyEmbeddableAPI['blockingError'];
|
||||
public setPanelTitle: LegacyEmbeddableAPI['setPanelTitle'];
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
PublishesViewMode,
|
||||
PublishesWritablePanelDescription,
|
||||
PublishesWritablePanelTitle,
|
||||
PublishesPhaseEvents,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EmbeddableInput } from '../../../common/types';
|
||||
|
@ -38,6 +39,7 @@ export type { EmbeddableInput };
|
|||
*/
|
||||
export type LegacyEmbeddableAPI = HasType &
|
||||
HasUniqueId &
|
||||
PublishesPhaseEvents &
|
||||
PublishesViewMode &
|
||||
PublishesDataViews &
|
||||
HasEditCapabilities &
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { EuiFlexGroup, EuiPanel, htmlIdGenerator } from '@elastic/eui';
|
||||
import { PanelLoader } from '@kbn/panel-loader';
|
||||
import {
|
||||
apiFiresPhaseEvents,
|
||||
apiPublishesPhaseEvents,
|
||||
apiHasParentApi,
|
||||
apiPublishesViewMode,
|
||||
useBatchedPublishingSubjects,
|
||||
|
@ -82,8 +82,10 @@ export const PresentationPanelInternal = <
|
|||
|
||||
useEffect(() => {
|
||||
let subscription: Subscription;
|
||||
if (api && onPanelStatusChange && apiFiresPhaseEvents(api)) {
|
||||
subscription = api.onPhaseChange.subscribe((phase) => onPanelStatusChange(phase));
|
||||
if (api && onPanelStatusChange && apiPublishesPhaseEvents(api)) {
|
||||
subscription = api.onPhaseChange.subscribe((phase) => {
|
||||
if (phase) onPanelStatusChange(phase);
|
||||
});
|
||||
}
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [api, onPanelStatusChange]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue