Skip to content

Commit

Permalink
[data views] Runtime field creation, preview sidebar - move code from…
Browse files Browse the repository at this point in the history
… container (hooks) to controller (service) (#181770)

## Summary

This PR moves some runtime field editor UI code from react hooks into a
plain js controller / service. Tech debt reduction.
  • Loading branch information
mattkime authored May 31, 2024
1 parent 0851587 commit f89b5ae
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 185 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export { waitForUpdates, waitForDocumentsAndPreviewUpdate } from './helpers';
const defaultProps: Props = {
onSave: () => {},
onCancel: () => {},
isSavingField: false,
};

const getActions = (testBed: TestBed) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
const defaultProps: Props = {
onSave: () => {},
onCancel: () => {},
isSavingField: false,
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import { defer, BehaviorSubject } from 'rxjs';
import { notificationServiceMock, uiSettingsServiceMock } from '@kbn/core/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { fieldFormatsMock as fieldFormats } from '@kbn/field-formats-plugin/common/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/data_views/data_view.stub';
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { PreviewController } from '../../../public/components/preview/preview_controller';
import { FieldEditorProvider, Context } from '../../../public/components/field_editor_context';
import { FieldPreviewProvider } from '../../../public/components/preview';
Expand Down Expand Up @@ -150,9 +152,17 @@ export const WithFieldEditorDependencies =

const mergedDependencies = merge({}, dependencies, overridingDependencies);
const previewController = new PreviewController({
deps: {
dataViews: dataViewPluginMocks.createStartContract(),
search,
fieldFormats,
usageCollection: {
reportUiCounter: jest.fn(),
} as UsageCollectionStart,
notifications: notificationServiceMock.createStartContract(),
},
dataView,
search,
fieldFormats,
onSave: jest.fn(),
fieldTypeToProcess: 'runtime',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,20 @@ export interface Props {
fieldToEdit?: Field;
/** Optional preselected configuration for new field */
fieldToCreate?: Field;
isSavingField: boolean;
/** Handler to call when the component mounts.
* We will pass "up" data that the parent component might need
*/
onMounted?: (args: { canCloseValidator: () => boolean }) => void;
}

const isPanelVisibleSelector = (state: PreviewState) => state.isPanelVisible;
const isSavingSelector = (state: PreviewState) => state.isSaving;

const FieldEditorFlyoutContentComponent = ({
fieldToEdit,
fieldToCreate,
onSave,
onCancel,
isSavingField,
onMounted,
}: Props) => {
const isMounted = useRef(false);
Expand All @@ -81,6 +80,7 @@ const FieldEditorFlyoutContentComponent = ({

const { controller } = useFieldPreviewContext();
const isPanelVisible = useStateSelector(controller.state$, isPanelVisibleSelector);
const isSavingField = useStateSelector(controller.state$, isSavingSelector);

const [formState, setFormState] = useState<FieldEditorFormState>({
isSubmitted: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
* Side Public License, v 1.
*/

import React, { useCallback, useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { DocLinksStart, NotificationsStart, CoreStart } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';

import { BehaviorSubject } from 'rxjs';
import {
Expand All @@ -19,10 +17,8 @@ import {
UsageCollectionStart,
DataViewsPublicPluginStart,
FieldFormatsStart,
RuntimeType,
} from '../shared_imports';
import type { Field, PluginStart, InternalFieldType } from '../types';
import { pluginName } from '../constants';
import { getLinks, ApiService } from '../lib';
import {
FieldEditorFlyoutContent,
Expand Down Expand Up @@ -86,40 +82,21 @@ export const FieldEditorFlyoutContentContainer = ({
uiSettings,
}: Props) => {
const [controller] = useState(
() => new PreviewController({ dataView, search, fieldFormats, fieldTypeToProcess })
);
const [isSaving, setIsSaving] = useState(false);

const { fields } = dataView;

const namesNotAllowed = useMemo(() => {
const fieldNames = dataView.fields.map((fld) => fld.name);
const runtimeCompositeNames = Object.entries(dataView.getAllRuntimeFields())
.filter(([, _runtimeField]) => _runtimeField.type === 'composite')
.map(([_runtimeFieldName]) => _runtimeFieldName);
return {
fields: fieldNames,
runtimeComposites: runtimeCompositeNames,
};
}, [dataView]);

const existingConcreteFields = useMemo(() => {
const existing: Array<{ name: string; type: string }> = [];

fields
.filter((fld) => {
const isFieldBeingEdited = fieldToEdit?.name === fld.name;
return !isFieldBeingEdited && fld.isMapped;
() =>
new PreviewController({
deps: {
dataViews,
search,
fieldFormats,
usageCollection,
notifications,
},
dataView,
onSave,
fieldToEdit,
fieldTypeToProcess,
})
.forEach((fld) => {
existing.push({
name: fld.name,
type: (fld.esTypes && fld.esTypes[0]) || '',
});
});

return existing;
}, [fields, fieldToEdit]);
);

const services = useMemo(
() => ({
Expand All @@ -130,125 +107,6 @@ export const FieldEditorFlyoutContentContainer = ({
[apiService, search, notifications]
);

const updateRuntimeField = useCallback(
(updatedField: Field): DataViewField[] => {
const nameHasChanged = Boolean(fieldToEdit) && fieldToEdit!.name !== updatedField.name;
const typeHasChanged = Boolean(fieldToEdit) && fieldToEdit!.type !== updatedField.type;
const hasChangeToOrFromComposite =
typeHasChanged && (fieldToEdit!.type === 'composite' || updatedField.type === 'composite');

const { script } = updatedField;

if (fieldTypeToProcess === 'runtime') {
try {
usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_runtime');
// eslint-disable-next-line no-empty
} catch {}
// rename an existing runtime field
if (nameHasChanged || hasChangeToOrFromComposite) {
dataView.removeRuntimeField(fieldToEdit!.name);
}

dataView.addRuntimeField(updatedField.name, {
type: updatedField.type as RuntimeType,
script,
fields: updatedField.fields,
});
} else {
try {
usageCollection.reportUiCounter(pluginName, METRIC_TYPE.COUNT, 'save_concrete');
// eslint-disable-next-line no-empty
} catch {}
}

return dataView.addRuntimeField(updatedField.name, updatedField);
},
[fieldToEdit, dataView, fieldTypeToProcess, usageCollection]
);

const updateConcreteField = useCallback(
(updatedField: Field): DataViewField[] => {
const editedField = dataView.getFieldByName(updatedField.name);

if (!editedField) {
throw new Error(
`Unable to find field named '${
updatedField.name
}' on index pattern '${dataView.getIndexPattern()}'`
);
}

// Update custom label, popularity and format
dataView.setFieldCustomLabel(updatedField.name, updatedField.customLabel);
dataView.setFieldCustomDescription(updatedField.name, updatedField.customDescription);

editedField.count = updatedField.popularity || 0;
if (updatedField.format) {
dataView.setFieldFormat(updatedField.name, updatedField.format!);
} else {
dataView.deleteFieldFormat(updatedField.name);
}

return [editedField];
},
[dataView]
);

const saveField = useCallback(
async (updatedField: Field) => {
try {
usageCollection.reportUiCounter(
pluginName,
METRIC_TYPE.COUNT,
fieldTypeToProcess === 'runtime' ? 'save_runtime' : 'save_concrete'
);
// eslint-disable-next-line no-empty
} catch {}

setIsSaving(true);

try {
const editedFields: DataViewField[] =
fieldTypeToProcess === 'runtime'
? updateRuntimeField(updatedField)
: updateConcreteField(updatedField as Field);

const afterSave = () => {
const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', {
defaultMessage: "Saved '{fieldName}'",
values: { fieldName: updatedField.name },
});
notifications.toasts.addSuccess(message);
setIsSaving(false);
onSave(editedFields);
};

if (dataView.isPersisted()) {
await dataViews.updateSavedObject(dataView);
}
afterSave();

setIsSaving(false);
} catch (e) {
const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', {
defaultMessage: 'Failed to save field changes',
});
notifications.toasts.addError(e, { title });
setIsSaving(false);
}
},
[
onSave,
dataView,
dataViews,
notifications,
fieldTypeToProcess,
updateConcreteField,
updateRuntimeField,
usageCollection,
]
);

return (
<FieldEditorProvider
dataView={dataView}
Expand All @@ -258,19 +116,18 @@ export const FieldEditorFlyoutContentContainer = ({
services={services}
fieldFormatEditors={fieldFormatEditors}
fieldFormats={fieldFormats}
namesNotAllowed={namesNotAllowed}
existingConcreteFields={existingConcreteFields}
namesNotAllowed={controller.getNamesNotAllowed()}
existingConcreteFields={controller.getExistingConcreteFields()}
fieldName$={new BehaviorSubject(fieldToEdit?.name || '')}
subfields$={new BehaviorSubject(fieldToEdit?.fields)}
>
<FieldPreviewProvider controller={controller}>
<FieldEditorFlyoutContent
onSave={saveField}
onSave={controller.saveField}
onCancel={onCancel}
onMounted={onMounted}
fieldToCreate={fieldToCreate}
fieldToEdit={fieldToEdit}
isSavingField={isSaving}
/>
</FieldPreviewProvider>
</FieldEditorProvider>
Expand Down
Loading

0 comments on commit f89b5ae

Please sign in to comment.