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 @@ -11,7 +11,7 @@ import { memoize } from 'lodash';
import { DataView } from '@kbn/data-views-plugin/common';

import { pluginServices } from '../../services';
import { DataControlField, DataControlFieldRegistry, IEditableControlFactory } from '../../types';
import { DataControlFieldRegistry, IEditableControlFactory } from '../../types';

export const getDataControlFieldRegistry = memoize(
async (dataView: DataView) => {
Expand All @@ -30,20 +30,19 @@ 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: [] };
const fieldRegistry: DataControlFieldRegistry = {};
return new Promise<DataControlFieldRegistry>((resolve) => {
for (const field of dataView.fields.getAll()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

for ... of is not much better. How about an imperative for loop, the one with the iterator for(i=0; i<length;i++)

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.

I did some performance testing of all 3 options:

  1. Using .forEach with a callback - took 11.64ms on average over 5 tries
  2. Using for ... of for loop - took 12.52ms on average over 5 tries
  3. Using for(let i=0....) imperative for loop - took 12.80ms on average over 5 tries.

So it seems like the imperative for loop is actually the worst of the three, although the differences here are so minimal I'm sure all three options would average out to be more-or-less equivalent over 100+ tests instead of 5 (for example, in my tests for the imperative for loop there was one run that randomly took 17.9ms - doing more iterations would eliminate this variance).

I'm wondering if it is worth going this deep over such minimal improvements (~1ms for 10,000 fields), though? I'm in favour of sticking with readability in a situation like this, and I personally vote for the for ... of loop in this case because it matches the style of the internal factory loop. 🤷

Copy link
Contributor

Choose a reason for hiding this comment

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

right, looking at https://leanylabs.com/blog/js-forEach-map-reduce-vs-for-for_of/ for..in and a traditional for loop have about the same performance, with for..in having slightly better performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was just going to link the same article 😆 Awesome, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

I was reading that one too hahaha.

const compatibleControlTypes = [];
for (const factory of controlFactories) {
if (factory.isFieldCompatible) {
factory.isFieldCompatible(test);
if (factory.isFieldCompatible && factory.isFieldCompatible(field)) {
compatibleControlTypes.push(factory.type);
}
}
if (test.compatibleControlTypes.length === 0) {
return { ...registry };
if (compatibleControlTypes.length > 0) {
fieldRegistry[field.name] = { field, compatibleControlTypes };
}
return { ...registry, [field.name]: test };
}, {});

return fieldRegistry;
}
resolve(fieldRegistry);
});
};
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