Skip to content

Commit

Permalink
[Embeddable Refactor] Publish phase events (elastic#175245)
Browse files Browse the repository at this point in the history
Makes the Legacy Embeddable API properly implement the `publishesPhaseEvents` interface.
  • Loading branch information
ThomThomson authored and CoenWarmer committed Feb 15, 2024
1 parent b962710 commit 1933b20
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 11 deletions.
6 changes: 3 additions & 3 deletions packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 } => {
Expand All @@ -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
*/
Expand Down Expand Up @@ -155,6 +205,8 @@ export const legacyEmbeddableToApi = (
dataLoading,
blockingError,

onPhaseChange,

onEdit,
isEditingEnabled,
getTypeDisplayName,
Expand Down
26 changes: 26 additions & 0 deletions src/plugins/embeddable/public/lib/embeddables/embeddable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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' },
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'];
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PublishesViewMode,
PublishesWritablePanelDescription,
PublishesWritablePanelTitle,
PublishesPhaseEvents,
} from '@kbn/presentation-publishing';
import { Observable } from 'rxjs';
import { EmbeddableInput } from '../../../common/types';
Expand All @@ -38,6 +39,7 @@ export type { EmbeddableInput };
*/
export type LegacyEmbeddableAPI = HasType &
HasUniqueId &
PublishesPhaseEvents &
PublishesViewMode &
PublishesDataViews &
HasEditCapabilities &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { EuiFlexGroup, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import { PanelLoader } from '@kbn/panel-loader';
import {
apiFiresPhaseEvents,
apiPublishesPhaseEvents,
apiHasParentApi,
apiPublishesViewMode,
useBatchedPublishingSubjects,
Expand Down Expand Up @@ -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]);
Expand Down

0 comments on commit 1933b20

Please sign in to comment.