[Controls] Fix Initialization Race Condition (#143220) (#143246)

* Fix Initialization Race Condition

(cherry picked from commit 702827b42f)

# Conflicts:
#	src/plugins/dashboard/public/application/embeddable/dashboard_container_factory.tsx
#	x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts
This commit is contained in:
Devon Thomson 2022-10-12 19:34:45 -04:00 committed by GitHub
parent bb518ae62a
commit f70e86982a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 54 additions and 32 deletions

View file

@ -10,7 +10,7 @@ import { skip, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import React from 'react';
import ReactDOM from 'react-dom';
import { Filter, uniqFilters } from '@kbn/es-query';
import { merge, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, merge, Subject, Subscription } from 'rxjs';
import { EuiContextMenuPanel } from '@elastic/eui';
import {
@ -57,6 +57,8 @@ export class ControlGroupContainer extends Container<
public readonly type = CONTROL_GROUP_TYPE;
public readonly anyControlOutputConsumerLoading$: Subject<boolean> = new Subject();
private initialized$ = new BehaviorSubject(false);
private subscriptions: Subscription = new Subscription();
private domNode?: HTMLElement;
private recalculateFilters$: Subject<null>;
@ -194,10 +196,11 @@ export class ControlGroupContainer extends Container<
});
// when all children are ready setup subscriptions
this.untilReady().then(() => {
this.untilAllChildrenReady().then(() => {
this.recalculateDataViews();
this.recalculateFilters();
this.setupSubscriptions();
this.initialized$.next(true);
});
}
@ -327,7 +330,7 @@ export class ControlGroupContainer extends Container<
};
}
public untilReady = () => {
public untilAllChildrenReady = () => {
const panelsLoading = () =>
Object.keys(this.getInput().panels).some(
(panelId) => !this.getOutput().embeddableLoaded[panelId]
@ -349,6 +352,24 @@ export class ControlGroupContainer extends Container<
return Promise.resolve();
};
public untilInitialized = () => {
if (this.initialized$.value === false) {
return new Promise<void>((resolve, reject) => {
const subscription = this.initialized$.subscribe((isInitialized) => {
if (this.destroyed) {
subscription.unsubscribe();
reject();
}
if (isInitialized) {
subscription.unsubscribe();
resolve();
}
});
});
}
return Promise.resolve();
};
public render(dom: HTMLElement) {
if (this.domNode) {
ReactDOM.unmountComponentAtNode(this.domNode);

View file

@ -153,16 +153,13 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
isProjectEnabledInLabs('labs:dashboard:dashboardControls')
) {
this.controlGroup = controlGroup;
this.controlGroup.untilReady().then(() => {
if (!this.controlGroup || isErrorEmbeddable(this.controlGroup)) return;
syncDashboardControlGroup({
dashboardContainer: this,
controlGroup: this.controlGroup,
}).then((result) => {
if (!result) return;
const { onDestroyControlGroup } = result;
this.onDestroyControlGroup = onDestroyControlGroup;
});
syncDashboardControlGroup({
dashboardContainer: this,
controlGroup: this.controlGroup,
}).then((result) => {
if (!result) return;
const { onDestroyControlGroup } = result;
this.onDestroyControlGroup = onDestroyControlGroup;
});
}

View file

@ -23,6 +23,7 @@ import {
ContainerOutput,
EmbeddableFactory,
EmbeddableFactoryDefinition,
isErrorEmbeddable,
} from '@kbn/embeddable-plugin/public';
import { DashboardContainerInput } from '../..';
@ -32,7 +33,6 @@ import {
createExtract,
createInject,
} from '../../../common/embeddable/dashboard_container_persistable_state';
import { pluginServices } from '../../services/plugin_services';
export type DashboardContainerFactory = EmbeddableFactory<
DashboardContainerInput,
@ -76,14 +76,13 @@ export class DashboardContainerFactoryDefinition
};
}
public create = async (
initialInput: DashboardContainerInput,
parent?: Container
): Promise<DashboardContainer | ErrorEmbeddable> => {
private buildControlGroup = async (
initialInput: DashboardContainerInput
): Promise<ControlGroupContainer | ErrorEmbeddable | undefined> => {
const { pluginServices } = await import('../../services/plugin_services');
const {
embeddable: { getEmbeddableFactory },
} = pluginServices.getServices();
const controlsGroupFactory = getEmbeddableFactory<
ControlGroupInput,
ControlGroupOutput,
@ -99,10 +98,23 @@ export class DashboardContainerFactoryDefinition
filters,
query,
});
if (controlGroup && !isErrorEmbeddable(controlGroup)) {
await controlGroup.untilInitialized();
}
return controlGroup;
};
const { DashboardContainer: DashboardContainerEmbeddable } = await import(
'./dashboard_container'
);
public create = async (
initialInput: DashboardContainerInput,
parent?: Container
): Promise<DashboardContainer | ErrorEmbeddable> => {
const controlGroupPromise = this.buildControlGroup(initialInput);
const dashboardContainerPromise = import('./dashboard_container');
const [controlGroup, { DashboardContainer: DashboardContainerEmbeddable }] = await Promise.all([
controlGroupPromise,
dashboardContainerPromise,
]);
return Promise.resolve(new DashboardContainerEmbeddable(initialInput, parent, controlGroup));
};

View file

@ -33,7 +33,6 @@ export interface DashboardViewportProps {
interface State {
isFullScreenMode: boolean;
controlGroupReady: boolean;
useMargins: boolean;
title: string;
description?: string;
@ -57,7 +56,6 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
this.controlsRoot = React.createRef();
this.state = {
controlGroupReady: !this.props.controlGroup,
isFullScreenMode,
panelCount: Object.values(panels).length,
useMargins,
@ -85,9 +83,6 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
if (this.props.controlGroup && this.controlsRoot.current) {
this.props.controlGroup.render(this.controlsRoot.current);
}
if (this.props.controlGroup) {
this.props.controlGroup?.untilReady().then(() => this.setState({ controlGroupReady: true }));
}
}
public componentWillUnmount() {
@ -164,9 +159,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
<DashboardEmptyScreen isEditMode={isEditMode} />
</div>
)}
{this.state.controlGroupReady && (
<DashboardGrid container={container} onDataLoaded={this.props.onDataLoaded} />
)}
<DashboardGrid container={container} onDataLoaded={this.props.onDataLoaded} />
</div>
</>
);

View file

@ -37,8 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const spaces = getService('spaces');
const elasticChart = getService('elasticChart');
// Skipping in 8.5 until https://github.com/elastic/kibana/pull/143220 is merged and backported...
describe.skip('Dashboard to dashboard drilldown', function () {
describe('Dashboard to dashboard drilldown', function () {
describe('Create & use drilldowns', () => {
before(async () => {
log.debug('Dashboard Drilldowns:initTests');