Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard] [Controls] Improve async controls flyout behaviour #154004

120 changes: 57 additions & 63 deletions src/plugins/controls/public/control_group/editor/control_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import React, { useEffect, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import useAsync from 'react-use/lib/useAsync';

import {
EuiFlyoutHeader,
Expand All @@ -35,7 +36,7 @@ import {
EuiSwitch,
EuiTextColor,
} from '@elastic/eui';
import { DataViewListItem, DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { DataViewField } from '@kbn/data-views-plugin/common';
import {
LazyDataViewPicker,
LazyFieldPicker,
Expand All @@ -46,7 +47,6 @@ import { ControlGroupStrings } from '../control_group_strings';
import {
ControlEmbeddable,
ControlWidth,
DataControlFieldRegistry,
DataControlInput,
IEditableControlFactory,
} from '../../types';
Expand All @@ -71,12 +71,6 @@ interface EditControlProps {
onTypeEditorChange: (partial: Partial<DataControlInput>) => void;
}

interface ControlEditorState {
dataViewListItems: DataViewListItem[];
selectedDataView?: DataView;
selectedField?: DataViewField;
}

const FieldPicker = withSuspense(LazyFieldPicker, null);
const DataViewPicker = withSuspense(LazyDataViewPicker, null);

Expand Down Expand Up @@ -104,10 +98,6 @@ export const ControlEditor = ({
const { useEmbeddableSelector: select } = useControlGroupContainerContext();
const editorConfig = select((state) => state.componentState.editorConfig);

const [state, setState] = useState<ControlEditorState>({
dataViewListItems: [],
});

const [defaultTitle, setDefaultTitle] = useState<string>();
const [currentTitle, setCurrentTitle] = useState(title ?? '');
const [currentWidth, setCurrentWidth] = useState(width);
Expand All @@ -116,47 +106,54 @@ export const ControlEditor = ({
const [selectedField, setSelectedField] = useState<string | undefined>(
embeddable ? embeddable.getInput().fieldName : undefined
);

const [fieldRegistry, setFieldRegistry] = useState<DataControlFieldRegistry>();
useEffect(() => {
(async () => {
if (state.selectedDataView?.id) {
setFieldRegistry(await getDataControlFieldRegistry(await get(state.selectedDataView.id)));
}
})();
}, [state.selectedDataView?.id, get]);
const [selectedDataViewId, setSelectedDataViewId] = useState<string>();

useMount(() => {
let mounted = true;
if (selectedField) setDefaultTitle(selectedField);

(async () => {
const dataViewListItems = await getIdsWithTitle();
if (!mounted) return;

const initialId =
embeddable?.getInput().dataViewId ?? getRelevantDataViewId?.() ?? (await getDefaultId());
let dataView: DataView | undefined;
if (initialId) {
onTypeEditorChange({ dataViewId: initialId });
dataView = await get(initialId);
setSelectedDataViewId(initialId);
}
if (!mounted) return;
setState((s) => ({
...s,
selectedDataView: dataView,
dataViewListItems,
}));
})();
return () => {
mounted = false;
};
});

const { loading: dataViewListLoading, value: dataViewListItems = [] } = useAsync(() => {
return getIdsWithTitle();
});

const {
loading: dataViewLoading,
value: { selectedDataView, fieldRegistry } = {
selectedDataView: undefined,
fieldRegistry: undefined,
},
} = useAsync(async () => {
if (!selectedDataViewId) {
return;
}
const dataView = await get(selectedDataViewId);
const registry = await getDataControlFieldRegistry(dataView);
return {
selectedDataView: dataView,
fieldRegistry: registry,
};
}, [selectedDataViewId]);

useEffect(
() => setControlEditorValid(Boolean(selectedField) && Boolean(state.selectedDataView)),
[selectedField, setControlEditorValid, state.selectedDataView]
() => setControlEditorValid(Boolean(selectedField) && Boolean(selectedDataView)),
[selectedField, setControlEditorValid, selectedDataView]
);

const { selectedDataView: dataView } = state;
const controlType =
selectedField && fieldRegistry && fieldRegistry[selectedField].compatibleControlTypes[0];
const factory = controlType && getControlFactory(controlType);
Expand All @@ -178,47 +175,44 @@ export const ControlEditor = ({
{!editorConfig?.hideDataViewSelector && (
<EuiFormRow label={ControlGroupStrings.manageControl.getDataViewTitle()}>
<DataViewPicker
dataViews={state.dataViewListItems}
selectedDataViewId={dataView?.id}
dataViews={dataViewListItems}
selectedDataViewId={selectedDataViewId}
onChangeDataViewId={(dataViewId) => {
setLastUsedDataViewId?.(dataViewId);
if (dataViewId === dataView?.id) return;

if (dataViewId === selectedDataViewId) return;
onTypeEditorChange({ dataViewId });
setSelectedField(undefined);
get(dataViewId).then((newDataView) => {
setState((s) => ({ ...s, selectedDataView: newDataView }));
});
setSelectedDataViewId(dataViewId);
}}
trigger={{
label:
state.selectedDataView?.getName() ??
selectedDataView?.getName() ??
ControlGroupStrings.manageControl.getSelectDataViewMessage(),
}}
selectableProps={{ isLoading: dataViewListLoading }}
/>
</EuiFormRow>
)}
{fieldRegistry && (
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => Boolean(fieldRegistry[field.name])}
selectedFieldName={selectedField}
dataView={dataView}
onSelectField={(field) => {
onTypeEditorChange({
fieldName: field.name,
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
setSelectedField(field.name);
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
}}
/>
</EuiFormRow>
)}
<EuiFormRow label={ControlGroupStrings.manageControl.getFieldTitle()}>
<FieldPicker
filterPredicate={(field: DataViewField) => Boolean(fieldRegistry?.[field.name])}
selectedFieldName={selectedField}
dataView={selectedDataView}
onSelectField={(field) => {
onTypeEditorChange({
fieldName: field.name,
});
const newDefaultTitle = field.displayName ?? field.name;
setDefaultTitle(newDefaultTitle);
setSelectedField(field.name);
if (!currentTitle || currentTitle === defaultTitle) {
setCurrentTitle(newDefaultTitle);
updateTitle(newDefaultTitle);
}
}}
selectableProps={{ isLoading: dataViewListLoading || dataViewLoading }}
/>
</EuiFormRow>
<EuiFormRow label={ControlGroupStrings.manageControl.getControlTypeTitle()}>
{factory ? (
<EuiFlexGroup alignItems="center" gutterSize="xs">
Expand Down Expand Up @@ -284,7 +278,7 @@ export const ControlEditor = ({
<CustomSettings
onChange={onTypeEditorChange}
initialInput={embeddable?.getInput()}
fieldType={fieldRegistry[selectedField].field.type}
fieldType={fieldRegistry?.[selectedField].field.type}
/>
</EuiFormRow>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { memoize } from 'lodash';

import { DataView } from '@kbn/data-views-plugin/common';
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';

import { pluginServices } from '../../services';
import { DataControlField, DataControlFieldRegistry, IEditableControlFactory } from '../../types';
Expand All @@ -30,20 +30,30 @@ const loadFieldRegistryFromDataView = async (
const controlFactories = getControlTypes().map(
(controlType) => getControlFactory(controlType) as IEditableControlFactory
);
const fieldRegistry: DataControlFieldRegistry = dataView.fields
.getAll()
.reduce((registry, field) => {
const test: DataControlField = { field, compatibleControlTypes: [] };
for (const factory of controlFactories) {
if (factory.isFieldCompatible) {
factory.isFieldCompatible(test);
}
const fieldRegistry: DataControlFieldRegistry = {};
await Promise.all(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other performance consideration is to use for loop for iterating through large collections. Each time map is run, a new function is created and this creates a large garbage collection penalty.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! I bet we could increase this performance even further, cause the code snipped that I suggested also uses map unnecessarily.

Copy link
Contributor Author

@Heenawter Heenawter Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to forEach in 9d29b33

Changed to for loop in afb1718

dataView.fields.getAll().map(async (field): Promise<void> => {
const test = await getDataControlField(controlFactories, field);
if (test.compatibleControlTypes.length > 0) {
fieldRegistry[field.name] = test;
}
if (test.compatibleControlTypes.length === 0) {
return { ...registry };
}
return { ...registry, [field.name]: test };
}, {});
})
);

return fieldRegistry;
};

const getDataControlField = (
controlFactories: IEditableControlFactory[],
field: DataViewField
): Promise<DataControlField> => {
return new Promise((resolve) => {
const compatibleControlTypes = [];
for (const factory of controlFactories) {
if (factory.isFieldCompatible && factory.isFieldCompatible(field)) {
compatibleControlTypes.push(factory.type);
}
}
resolve({ field, compatibleControlTypes });
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import deepEqual from 'fast-deep-equal';

import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';

Expand All @@ -21,7 +22,7 @@ import {
OPTIONS_LIST_CONTROL,
} from '../../../common/options_list/types';
import { OptionsListEditorOptions } from '../components/options_list_editor_options';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import { ControlEmbeddable, IEditableControlFactory } from '../../types';

export class OptionsListEmbeddableFactory
implements EmbeddableFactoryDefinition, IEditableControlFactory<OptionsListEmbeddableInput>
Expand Down Expand Up @@ -57,15 +58,13 @@ export class OptionsListEmbeddableFactory
return newInput;
};

public isFieldCompatible = (dataControlField: DataControlField) => {
if (
!dataControlField.field.spec.scripted &&
((dataControlField.field.aggregatable && dataControlField.field.type === 'string') ||
dataControlField.field.type === 'boolean' ||
dataControlField.field.type === 'ip')
) {
dataControlField.compatibleControlTypes.push(this.type);
}
public isFieldCompatible = (field: DataViewField) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup!

return (
!field.spec.scripted &&
((field.aggregatable && field.type === 'string') ||
field.type === 'boolean' ||
field.type === 'ip')
);
};

public controlEditorOptionsComponent = OptionsListEditorOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import deepEqual from 'fast-deep-equal';

import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { lazyLoadReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { i18n } from '@kbn/i18n';

import {
Expand All @@ -20,7 +21,7 @@ import {
RangeSliderEmbeddableInput,
RANGE_SLIDER_CONTROL,
} from '../../../common/range_slider/types';
import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '../../types';
import { ControlEmbeddable, IEditableControlFactory } from '../../types';

export class RangeSliderEmbeddableFactory
implements EmbeddableFactoryDefinition, IEditableControlFactory<RangeSliderEmbeddableInput>
Expand Down Expand Up @@ -68,10 +69,8 @@ export class RangeSliderEmbeddableFactory
return newInput;
};

public isFieldCompatible = (dataControlField: DataControlField) => {
if (dataControlField.field.aggregatable && dataControlField.field.type === 'number') {
dataControlField.compatibleControlTypes.push(this.type);
}
public isFieldCompatible = (field: DataViewField) => {
return field.aggregatable && field.type === 'number';
};

public inject = createRangeSliderInject();
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/controls/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ export type ControlEmbeddable<
/**
* Control embeddable editor types
*/
export interface IEditableControlFactory<T extends ControlInput = ControlInput> {
export interface IEditableControlFactory<T extends ControlInput = ControlInput>
extends Pick<EmbeddableFactory, 'type'> {
controlEditorOptionsComponent?: (props: ControlEditorProps<T>) => JSX.Element;
presaveTransformFunction?: (
newState: Partial<T>,
embeddable?: ControlEmbeddable<T>
) => Partial<T>;
isFieldCompatible?: (dataControlField: DataControlField) => void; // reducer
isFieldCompatible?: (field: DataViewField) => boolean;
}

export interface ControlEditorProps<T extends ControlInput = ControlInput> {
Expand Down
Loading