From 0308537b7f56854ee4e9dde2fddb82a4704c7dfa Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 1 Oct 2024 06:31:45 +1000 Subject: [PATCH] [8.x] [Data View Mgmt] Implement state service (#193660) (#194022) # Backport This will backport the following commits from `main` to `8.x`: - [[Data View Mgmt] Implement state service (#193660)](https://github.com/elastic/kibana/pull/193660) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) Co-authored-by: Matthew Kime --- .../edit_index_pattern/edit_index_pattern.tsx | 317 ++++++++---------- .../edit_index_pattern_container.tsx | 7 +- .../edit_index_pattern/tabs/tabs.tsx | 95 ++---- .../edit_index_pattern/tabs/utils.ts | 7 +- .../data_view_management_service.ts | 294 ++++++++++++++++ .../data_view_mgmt_selectors.ts | 20 ++ .../mount_management_section.tsx | 10 + .../public/management_app/state_utils.ts | 28 ++ .../data_view_management/public/mocks.ts | 1 + .../data_view_management/public/plugin.ts | 3 +- .../data_view_management/public/types.ts | 2 + .../common/data_views/data_views.test.ts | 3 + .../common/data_views/data_views.ts | 16 +- 13 files changed, 551 insertions(+), 252 deletions(-) create mode 100644 src/plugins/data_view_management/public/management_app/data_view_management_service.ts create mode 100644 src/plugins/data_view_management/public/management_app/data_view_mgmt_selectors.ts create mode 100644 src/plugins/data_view_management/public/management_app/state_utils.ts diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index e3c6de8c8a932..14fa19306e40e 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import React, { useEffect, useState } from 'react'; import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom'; import { EuiFlexGroup, @@ -22,23 +22,18 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DataView, DataViewField, DataViewType, RuntimeField } from '@kbn/data-views-plugin/public'; -import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public'; +import { DataViewType, RuntimeField, DataView } from '@kbn/data-views-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { - SavedObjectRelation, - SavedObjectManagementTypeInfo, -} from '@kbn/saved-objects-management-plugin/public'; +import { SavedObjectRelation } from '@kbn/saved-objects-management-plugin/public'; import { pickBy } from 'lodash'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; import type * as CSS from 'csstype'; import { RollupDeprecationTooltip } from '@kbn/rollup'; import { IndexPatternManagmentContext } from '../../types'; import { Tabs } from './tabs'; import { IndexHeader } from './index_header'; -import { getTags } from '../utils'; import { removeDataView, RemoveDataViewProps } from './remove_data_view'; -import { APP_STATE_STORAGE_KEY } from './edit_index_pattern_state_container'; + +import { useStateSelector } from '../../management_app/state_utils'; const codeStyle: CSS.Properties = { marginLeft: '8px', @@ -72,6 +67,16 @@ const securitySolution = 'security-solution'; const getCompositeRuntimeFields = (dataView: DataView) => pickBy(dataView.getAllRuntimeFields(), (fld) => fld.type === 'composite'); +import { + dataViewSelector, + allowedTypesSelector, + relationshipsSelector, + tagsSelector, + isRefreshingSelector, + defaultIndexSelector, + fieldsSelector, +} from '../../management_app/data_view_mgmt_selectors'; + export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { const { @@ -82,39 +87,32 @@ export const EditIndexPattern = withRouter( IndexPatternEditor, savedObjectsManagement, application, + dataViewMgmtService, ...startServices } = useKibana().services; - const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const dataView = useStateSelector(dataViewMgmtService.state$, dataViewSelector); + const allowedTypes = useStateSelector(dataViewMgmtService.state$, allowedTypesSelector); + const relationships = useStateSelector(dataViewMgmtService.state$, relationshipsSelector); + const tags = useStateSelector(dataViewMgmtService.state$, tagsSelector); + const isRefreshing = useStateSelector(dataViewMgmtService.state$, isRefreshingSelector); + const defaultIndex = useStateSelector(dataViewMgmtService.state$, defaultIndexSelector); + const fields = useStateSelector(dataViewMgmtService.state$, fieldsSelector); + const fieldConflictCount = useStateSelector( + dataViewMgmtService.state$, + (state) => state.fieldConflictCount + ); + const conflictFieldsUrl = useStateSelector( + dataViewMgmtService.state$, + (state) => state.conflictFieldsUrl + ); + // has default const [compositeRuntimeFields, setCompositeRuntimeFields] = useState< Record >(() => getCompositeRuntimeFields(indexPattern)); - const [conflictedFields, setConflictedFields] = useState( - indexPattern.fields.getAll().filter((field) => field.type === 'conflict') - ); - const [defaultIndex, setDefaultIndex] = useState(uiSettings.get('defaultIndex')); - const [tags, setTags] = useState< - Array<{ key: string; 'data-test-subj': string; name: string }> - >([]); - const [showEditDialog, setShowEditDialog] = useState(false); - const [relationships, setRelationships] = useState([]); - const [allowedTypes, setAllowedTypes] = useState([]); - const [refreshCount, setRefreshCount] = useState(0); // used for forcing rerender of field list - const [isRefreshing, setIsRefreshing] = React.useState(false); - const conflictFieldsUrl = useMemo(() => { - return setStateToKbnUrl( - APP_STATE_STORAGE_KEY, - { - fieldTypes: ['conflict'], - tab: 'indexedFields', - }, - { useHash: uiSettings.get('state:storeInSessionStorage') }, - application.getUrlForApp('management', { - path: `/kibana/dataViews/dataView/${encodeURIComponent(indexPattern.id!)}`, - }) - ); - }, [application, indexPattern.id, uiSettings]); + const [showEditDialog, setShowEditDialog] = useState(false); + // subscribe and unsubscribe to hash change events useEffect(() => { // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. @@ -127,42 +125,6 @@ export const EditIndexPattern = withRouter( }; }, [history]); - useEffect(() => { - savedObjectsManagement.getAllowedTypes().then((resp) => { - setAllowedTypes(resp); - }); - }, [savedObjectsManagement]); - - useEffect(() => { - if (allowedTypes.length === 0 || !indexPattern.isPersisted()) { - return; - } - const allowedAsString = allowedTypes.map((item) => item.name); - savedObjectsManagement - .getRelationships(DATA_VIEW_SAVED_OBJECT_TYPE, indexPattern.id!, allowedAsString) - .then((resp) => { - setRelationships(resp.relations.map((r) => ({ ...r, title: r.meta.title! }))); - }); - }, [savedObjectsManagement, indexPattern, allowedTypes]); - - useEffect(() => { - setFields(indexPattern.getNonScriptedFields()); - setConflictedFields( - indexPattern.fields.getAll().filter((field) => field.type === 'conflict') - ); - }, [indexPattern, refreshCount]); - - useEffect(() => { - setTags( - getTags(indexPattern, indexPattern.id === defaultIndex, dataViews.getRollupsEnabled()) - ); - }, [defaultIndex, indexPattern, dataViews]); - - const setDefaultPattern = useCallback(() => { - uiSettings.set('defaultIndex', indexPattern.id); - setDefaultIndex(indexPattern.id || ''); - }, [uiSettings, indexPattern.id]); - const removeHandler = removeDataView({ dataViews, uiSettings, @@ -179,12 +141,12 @@ export const EditIndexPattern = withRouter( const displayIndexPatternEditor = showEditDialog ? ( { - setFields(indexPattern.getNonScriptedFields()); + dataViewMgmtService.refreshFields(); setShowEditDialog(false); }} onCancel={() => setShowEditDialog(false)} defaultTypeIsRollup={isRollup} - editData={indexPattern} + editData={dataView} /> ) : ( <> @@ -212,7 +174,7 @@ export const EditIndexPattern = withRouter( { defaultMessage: '{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.', - values: { conflictFieldsLength: conflictedFields.length }, + values: { conflictFieldsLength: fieldConflictCount }, } ); @@ -246,111 +208,110 @@ export const EditIndexPattern = withRouter( return (
- - removeHandler([indexPattern as RemoveDataViewProps],
{warning}
) - } - defaultIndex={defaultIndex} - canSave={userEditPermission} - > - - - - {Boolean(indexPattern.title) && ( - - - {indexPatternHeading} - - {indexPattern.title} - - - - )} - {Boolean(indexPattern.timeFieldName) && ( - - - {timeFilterHeading} - - {indexPattern.timeFieldName} - - - - )} - {indexPattern.id && indexPattern.id.indexOf(securitySolution) === 0 && ( - - {securityDataView} - - )} - {tags.map((tag) => ( - - {tag.key === 'default' ? ( - - {tag.name} - - ) : tag.key === 'rollup' ? ( - - {tag.name} - - ) : ( - {tag.name} - )} - - ))} - - {conflictedFields.length > 0 && ( - <> - - -

{mappingConflictLabel}

- - {i18n.translate( - 'indexPatternManagement.editIndexPattern.viewMappingConflictButton', - { - defaultMessage: 'View conflicts', - } + {dataView && ( + dataViewMgmtService.setDefaultDataView()} + editIndexPatternClick={editPattern} + deleteIndexPatternClick={() => + removeHandler([indexPattern as RemoveDataViewProps],
{warning}
) + } + defaultIndex={defaultIndex} + canSave={userEditPermission} + > + + + + {Boolean(indexPattern.title) && ( + + + {indexPatternHeading} + + {indexPattern.title} + + + + )} + {Boolean(indexPattern.timeFieldName) && ( + + + {timeFilterHeading} + + {indexPattern.timeFieldName} + + + + )} + {indexPattern.id && indexPattern.id.indexOf(securitySolution) === 0 && ( + + {securityDataView} + + )} + {tags.map((tag) => ( + + {tag.key === 'default' ? ( + + {tag.name} + + ) : tag.key === 'rollup' ? ( + + {tag.name} + + ) : ( + {tag.name} )} -
-
- - )} -
+ + ))} + + {fieldConflictCount > 0 && ( + <> + + +

{mappingConflictLabel}

+ + {i18n.translate( + 'indexPatternManagement.editIndexPattern.viewMappingConflictButton', + { + defaultMessage: 'View conflicts', + } + )} + +
+ + )} + + )} - { - setFields(indexPattern.getNonScriptedFields()); - setCompositeRuntimeFields(getCompositeRuntimeFields(indexPattern)); - }} - refreshIndexPatternClick={async () => { - setIsRefreshing(true); - await dataViews.refreshFields(indexPattern, false, true); - setRefreshCount(refreshCount + 1); // rerender field list - setIsRefreshing(false); - }} - isRefreshing={isRefreshing} - /> + {dataView && ( + { + dataViewMgmtService.refreshFields(); + setCompositeRuntimeFields(getCompositeRuntimeFields(indexPattern)); + }} + refreshIndexPatternClick={() => dataViewMgmtService.refreshFields()} + isRefreshing={isRefreshing} + /> + )} {displayIndexPatternEditor}
); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx index 244689687644a..5e3f53a3e130d 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern_container.tsx @@ -17,22 +17,23 @@ import { IndexPatternManagmentContext } from '../../types'; import { getEditBreadcrumbs } from '../breadcrumbs'; const EditIndexPatternCont: React.FC> = ({ ...props }) => { - const { dataViews, setBreadcrumbs, notifications } = + const { dataViews, setBreadcrumbs, notifications, dataViewMgmtService } = useKibana().services; const [error, setError] = useState(); const [indexPattern, setIndexPattern] = useState(); useEffect(() => { dataViews - .get(decodeURIComponent(props.match.params.id)) + .get(decodeURIComponent(props.match.params.id), undefined, true) .then((ip: DataView) => { + dataViewMgmtService.setDataView(ip); setIndexPattern(ip); setBreadcrumbs(getEditBreadcrumbs(ip)); }) .catch((err) => { setError(err); }); - }, [dataViews, props.match.params.id, setBreadcrumbs, setError]); + }, [dataViews, props.match.params.id, setBreadcrumbs, setError, dataViewMgmtService]); if (error) { const [errorTitle, errorMessage] = [ diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index aef91872254b2..df23c5cad9352 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -34,6 +34,7 @@ import { META_FIELDS, RuntimeField, } from '@kbn/data-views-plugin/public'; +import { AbstractDataView } from '@kbn/data-views-plugin/common'; import { SavedObjectRelation, SavedObjectManagementTypeInfo, @@ -53,6 +54,13 @@ import { ScriptedFieldsTable } from '../scripted_fields_table'; import { RelationshipsTable } from '../relationships_table'; import { getTabs, getPath, convertToEuiFilterOptions } from './utils'; import { getFieldInfo } from '../../utils'; +import { useStateSelector } from '../../../management_app/state_utils'; + +import { + fieldsSelector, + indexedFieldTypeSelector, + scriptedFieldLangsSelector, +} from '../../../management_app/data_view_mgmt_selectors'; interface TabsProps extends Pick { indexPattern: DataView; @@ -165,7 +173,6 @@ const SCHEMA_ITEMS: FilterItems[] = [ export const Tabs: React.FC = ({ indexPattern, saveIndexPattern, - fields, history, refreshFields, relationships, @@ -183,6 +190,7 @@ export const Tabs: React.FC = ({ http, application, savedObjectsManagement, + dataViewMgmtService, ...startServices } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); @@ -200,20 +208,24 @@ export const Tabs: React.FC = ({ }>({}); const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState([]); const [isScriptedFieldFilterOpen, setIsScriptedFieldFilterOpen] = useState(false); - const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState([]); const [isIndexedFilterOpen, setIsIndexedFilterOpen] = useState(false); - const [indexedFieldTypes, setIndexedFieldTypes] = useState([]); const [schemaFieldTypeFilter, setSchemaFieldTypeFilter] = useState([]); const [isSchemaFilterOpen, setIsSchemaFilterOpen] = useState(false); + const fields = useStateSelector(dataViewMgmtService.state$, fieldsSelector); + const indexedFieldTypes = convertToEuiFilterOptions( + useStateSelector(dataViewMgmtService.state$, indexedFieldTypeSelector) + ); + const scriptedFieldLanguages = useStateSelector( + dataViewMgmtService.state$, + scriptedFieldLangsSelector + ); const closeEditorHandler = useRef<() => void | undefined>(); const { DeleteRuntimeFieldProvider } = dataViewFieldEditor; const filteredIndexedFieldTypeFilter = useMemo(() => { - return uniq( - indexedFieldTypeFilter.filter((fieldType) => - indexedFieldTypes.some((item) => item.value === fieldType) - ) + return indexedFieldTypeFilter.filter((fieldType) => + indexedFieldTypes.some((item) => item.value === fieldType) ); }, [indexedFieldTypeFilter, indexedFieldTypes]); @@ -253,55 +265,6 @@ export const Tabs: React.FC = ({ [syncingStateFunc] ); - const updateFilterItem = ( - items: FilterItems[], - index: number, - updater: (a: FilterItems[]) => void - ) => { - if (!items[index]) { - return; - } - - const newItems = [...items]; - - switch (newItems[index].checked) { - case 'on': - newItems[index].checked = undefined; - break; - - default: - newItems[index].checked = 'on'; - } - - updater(newItems); - }; - - const refreshFilters = useCallback(() => { - const tempIndexedFieldTypes: string[] = []; - const tempScriptedFieldLanguages: string[] = []; - indexPattern.fields.getAll().forEach((field) => { - if (field.scripted) { - if (field.lang) { - tempScriptedFieldLanguages.push(field.lang); - } - } else { - // for conflicted fields, add conflict as a type - if (field.type === 'conflict') { - tempIndexedFieldTypes.push('conflict'); - } - if (field.esTypes) { - // add all types, may be multiple - field.esTypes.forEach((item) => tempIndexedFieldTypes.push(item)); - } - } - }); - - setIndexedFieldTypes(convertToEuiFilterOptions(tempIndexedFieldTypes)); - setScriptedFieldLanguages(convertToEuiFilterOptions(tempScriptedFieldLanguages)); - // need to reset based on changes to fields but indexPattern is the same - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern, fields]); - const closeFieldEditor = useCallback(() => { if (closeEditorHandler.current) { closeEditorHandler.current(); @@ -321,10 +284,6 @@ export const Tabs: React.FC = ({ [dataViewFieldEditor, indexPattern, refreshFields] ); - useEffect(() => { - refreshFilters(); - }, [indexPattern, indexPattern.fields, refreshFilters]); - useEffect(() => { return () => { // When the component unmounts, make sure to close the field editor @@ -340,6 +299,7 @@ export const Tabs: React.FC = ({ const refreshRef = useRef(null); const userEditPermission = dataViews.getCanSaveSync(); + const getFilterSection = useCallback( (type: string) => { return ( @@ -517,12 +477,14 @@ export const Tabs: React.FC = ({ checked={item.checked} key={item.value} onClick={() => { + // this does the filtering setScriptedFieldLanguageFilter( item.checked ? scriptedFieldLanguageFilter.filter((f) => f !== item.value) : [...scriptedFieldLanguageFilter, item.value] ); - updateFilterItem(scriptedFieldLanguages, index, setScriptedFieldLanguages); + // updates the UI + dataViewMgmtService.setScriptedFieldLangSelection(index); }} data-test-subj={`scriptedFieldLanguageFilterDropdown-option-${item.value}${ item.checked ? '-checked' : '' @@ -539,6 +501,7 @@ export const Tabs: React.FC = ({ ); }, [ + dataViewMgmtService, fieldFilter, filteredSchemaFieldTypeFilter, filteredIndexedFieldTypeFilter, @@ -606,7 +569,7 @@ export const Tabs: React.FC = ({ history.push(getPath(field, indexPattern)); }, }} - onRemoveField={refreshFilters} + onRemoveField={() => dataViewMgmtService.refreshFields()} painlessDocLink={docLinks.links.scriptedFields.painless} userEditPermission={dataViews.getCanSaveSync()} /> @@ -619,11 +582,13 @@ export const Tabs: React.FC = ({ {getFilterSection(type)} { + await saveIndexPattern(dv); + dataViewMgmtService.refreshFields(); + }} indexPattern={indexPattern} filterFilter={fieldFilter} fieldWildcardMatcher={fieldWildcardMatcherDecorated} - onAddOrRemoveFilter={refreshFilters} /> ); @@ -655,7 +620,6 @@ export const Tabs: React.FC = ({ indexPattern, filteredIndexedFieldTypeFilter, filteredSchemaFieldTypeFilter, - refreshFilters, scriptedFieldLanguageFilter, saveIndexPattern, openFieldEditor, @@ -670,6 +634,7 @@ export const Tabs: React.FC = ({ savedObjectsManagement, allowedTypes, relationships, + dataViewMgmtService, ] ); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts index 0d2a2bb6d273d..31f12e80366cc 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/utils.ts @@ -10,6 +10,7 @@ import { Dictionary, countBy, defaults, uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { DataView, DataViewField } from '@kbn/data-views-plugin/public'; +import { FilterChecked } from '@elastic/eui'; import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, @@ -126,7 +127,11 @@ export function getPath(field: DataViewField, indexPattern: DataView) { } export function convertToEuiFilterOptions(options: string[]) { - return uniq(options).map((option) => { + return uniq(options).map<{ + value: string; + name: string; + checked?: FilterChecked; + }>((option) => { return { value: option, name: option, diff --git a/src/plugins/data_view_management/public/management_app/data_view_management_service.ts b/src/plugins/data_view_management/public/management_app/data_view_management_service.ts new file mode 100644 index 0000000000000..3379655101910 --- /dev/null +++ b/src/plugins/data_view_management/public/management_app/data_view_management_service.ts @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { IUiSettingsClient, ApplicationStart } from '@kbn/core/public'; +import { BehaviorSubject, Observable, map, distinctUntilChanged } from 'rxjs'; + +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; +import { FilterChecked } from '@elastic/eui'; + +import { + SavedObjectsManagementPluginStart, + SavedObjectManagementTypeInfo, + SavedObjectRelation, +} from '@kbn/saved-objects-management-plugin/public'; + +import { + DataViewsPublicPluginStart, + INDEX_PATTERN_TYPE, + DataViewField, + DataView, +} from '@kbn/data-views-plugin/public'; + +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public'; + +import { getTags } from '../components/utils'; + +import { APP_STATE_STORAGE_KEY } from '../components/edit_index_pattern/edit_index_pattern_state_container'; + +import { convertToEuiFilterOptions } from '../components/edit_index_pattern/tabs/utils'; + +export interface SavedObjectRelationWithTitle extends SavedObjectRelation { + title: string; +} + +export type BehaviorObservable = Omit, 'next'>; + +export const matchedIndiciesDefault = { + allIndices: [], + exactMatchedIndices: [], + partialMatchedIndices: [], + visibleIndices: [], +}; + +/** + * ConstructorArgs for DataViewEditorService + */ +export interface DataViewMgmtServiceConstructorArgs { + /** + * Dependencies for the DataViewEditorService + */ + services: { + application: ApplicationStart; + dataViews: DataViewsPublicPluginStart; + savedObjectsManagement: SavedObjectsManagementPluginStart; + uiSettings: IUiSettingsClient; + }; + /** + * Whether service requires requireTimestampField + */ + requireTimestampField?: boolean; + /** + * Initial type, indexPattern, and name to populate service + */ + initialValues: { + name?: string; + type?: INDEX_PATTERN_TYPE; + indexPattern?: string; + }; +} + +export interface DataViewMgmtState { + dataView?: DataView; + allowedTypes: SavedObjectManagementTypeInfo[]; + relationships: SavedObjectRelationWithTitle[]; + fields: DataViewField[]; + scriptedFields: DataViewField[]; + scriptedFieldLangs: Array<{ + value: string; + name: string; + checked?: FilterChecked; + }>; + indexedFieldTypes: string[]; + fieldConflictCount: number; + tags: Array<{ key: string; 'data-test-subj': string; name: string }>; + isRefreshing: boolean; + defaultIndex: string; + conflictFieldsUrl: string; +} + +const defaultDataViewEditorState: DataViewMgmtState = { + allowedTypes: [], + relationships: [], + fields: [], + scriptedFields: [], + scriptedFieldLangs: [], + indexedFieldTypes: [], + fieldConflictCount: 0, + tags: [], + isRefreshing: true, + defaultIndex: '', + conflictFieldsUrl: '', +}; + +export const stateSelectorFactory = + (state$: Observable) => + (selector: (state: S) => R, equalityFn?: (arg0: R, arg1: R) => boolean) => + state$.pipe(map(selector), distinctUntilChanged(equalityFn)); + +export class DataViewMgmtService { + constructor({ + services: { dataViews, savedObjectsManagement, uiSettings, application }, + initialValues: {}, + }: DataViewMgmtServiceConstructorArgs) { + this.services = { + application, + dataViews, + savedObjectsManagement, + uiSettings, + }; + + this.internalState$ = new BehaviorSubject({ + ...defaultDataViewEditorState, + }); + + this.state$ = this.internalState$ as BehaviorObservable; + + // allowed types are set once and never change + this.allowedTypes = new Promise((resolve) => { + savedObjectsManagement.getAllowedTypes().then((resp) => { + this.updateState({ allowedTypes: resp }); + resolve(resp); + }); + }); + } + + private services: { + application: ApplicationStart; + dataViews: DataViewsPublicPluginStart; + savedObjectsManagement: SavedObjectsManagementPluginStart; + uiSettings: IUiSettingsClient; + }; + + private allowedTypes: Promise; + + private internalState$: BehaviorSubject; + state$: BehaviorObservable; + + private updateState = (newState: Partial) => { + this.internalState$.next({ ...this.state$.getValue(), ...newState }); + }; + + private getConflictFieldsKbnUrl = (dataViewId: string) => + setStateToKbnUrl( + APP_STATE_STORAGE_KEY, + { + fieldTypes: ['conflict'], + tab: 'indexedFields', + }, + { useHash: this.services.uiSettings.get('state:storeInSessionStorage') }, + this.services.application.getUrlForApp('management', { + path: `/kibana/dataViews/dataView/${encodeURIComponent(dataViewId)}`, + }) + ); + + private getTags = async (dataView: DataView) => { + if (dataView) { + const defaultIndex = await this.services.uiSettings.get('defaultIndex'); + const tags = getTags( + dataView, + dataView.id === defaultIndex, + this.services.dataViews.getRollupsEnabled() + ); + + return tags; + } + return []; + }; + + async updateScriptedFields() { + const dataView = this.state$.getValue().dataView; + if (dataView) { + const scriptedFieldRecords = dataView.getScriptedFields(); + const scriptedFields = Object.values(scriptedFieldRecords); + + const scriptedFieldLangs = Array.from( + scriptedFields.reduce((acc: Set, field) => { + if (field.lang) { + acc.add(field.lang); + } + return acc; + }, new Set()) + ); + + this.updateState({ + scriptedFields, + scriptedFieldLangs: convertToEuiFilterOptions(scriptedFieldLangs), + }); + } + } + + async setDataView(dataView: DataView) { + this.updateState({ isRefreshing: true }); + + const fieldRecords = dataView.fields + .filter((field) => !field.scripted) + .reduce((acc, field) => { + acc[field.name] = field; + return acc; + }, {} as Record); + + const fields = Object.values(fieldRecords); + const indexedFieldTypes = new Set(); + fields.forEach((field) => { + // for conflicted fields, add conflict as a type + if (field.type === 'conflict') { + indexedFieldTypes.add('conflict'); + } + if (field.esTypes) { + // add all types, may be multiple + field.esTypes.forEach((item) => indexedFieldTypes.add(item)); + } + }); + + const allowedAsString = (await this.allowedTypes).map((item) => item.name); + + this.services.savedObjectsManagement + .getRelationships(DATA_VIEW_SAVED_OBJECT_TYPE, dataView.id!, allowedAsString) + .then((resp) => { + this.updateState({ + relationships: resp.relations.map((r) => ({ ...r, title: r.meta.title! })), + }); + }); + + this.updateState({ + dataView, + fields, + indexedFieldTypes: Array.from(indexedFieldTypes), + fieldConflictCount: fields.filter((field) => field.type === 'conflict').length, + tags: await this.getTags(dataView), + isRefreshing: false, + conflictFieldsUrl: this.getConflictFieldsKbnUrl(dataView.id!), + scriptedFields: dataView.getScriptedFields(), + }); + + this.updateScriptedFields(); + } + + async refreshFields() { + const dataView = this.state$.getValue().dataView; + if (dataView) { + await this.services.dataViews.refreshFields(dataView, undefined, true); + return this.setDataView(dataView); + } + } + + async setDefaultDataView() { + const dataView = this.internalState$.getValue().dataView; + if (!dataView) { + return; + } + await this.services.uiSettings.set('defaultIndex', dataView.id); + + this.updateState({ tags: await this.getTags(dataView), defaultIndex: dataView.id }); + } + + setScriptedFieldLangSelection(index: number) { + const items = this.state$.getValue().scriptedFieldLangs; + + if (!items[index]) { + return; + } + + const scriptedFieldLangs = [...items]; + + switch (scriptedFieldLangs[index].checked) { + case 'on': + scriptedFieldLangs[index].checked = undefined; + break; + + default: + scriptedFieldLangs[index].checked = 'on'; + } + + this.updateState({ + scriptedFieldLangs, + }); + } +} diff --git a/src/plugins/data_view_management/public/management_app/data_view_mgmt_selectors.ts b/src/plugins/data_view_management/public/management_app/data_view_mgmt_selectors.ts new file mode 100644 index 0000000000000..e7f4db5d0c965 --- /dev/null +++ b/src/plugins/data_view_management/public/management_app/data_view_mgmt_selectors.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { DataViewMgmtState } from './data_view_management_service'; + +export const dataViewSelector = (state: DataViewMgmtState) => state.dataView; +export const allowedTypesSelector = (state: DataViewMgmtState) => state.allowedTypes; +export const relationshipsSelector = (state: DataViewMgmtState) => state.relationships; +export const tagsSelector = (state: DataViewMgmtState) => state.tags; +export const isRefreshingSelector = (state: DataViewMgmtState) => state.isRefreshing; +export const defaultIndexSelector = (state: DataViewMgmtState) => state.defaultIndex; +export const fieldsSelector = (state: DataViewMgmtState) => state.fields; +export const indexedFieldTypeSelector = (state: DataViewMgmtState) => state.indexedFieldTypes; +export const scriptedFieldLangsSelector = (state: DataViewMgmtState) => state.scriptedFieldLangs; diff --git a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx index 2298fea008dcf..995d5ed977ed3 100644 --- a/src/plugins/data_view_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/data_view_management/public/management_app/mount_management_section.tsx @@ -28,6 +28,7 @@ import { IndexPatternManagementSetupDependencies, } from '../plugin'; import { IndexPatternManagmentContext } from '../types'; +import { DataViewMgmtService } from './data_view_management_service'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -75,6 +76,15 @@ export async function mountManagementSection( } const deps: IndexPatternManagmentContext = { + dataViewMgmtService: new DataViewMgmtService({ + services: { + dataViews, + application, + savedObjectsManagement, + uiSettings, + }, + initialValues: {}, + }), application, chrome, uiSettings, diff --git a/src/plugins/data_view_management/public/management_app/state_utils.ts b/src/plugins/data_view_management/public/management_app/state_utils.ts new file mode 100644 index 0000000000000..90030d0dc45b2 --- /dev/null +++ b/src/plugins/data_view_management/public/management_app/state_utils.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs'; +import { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; + +export type BehaviorObservable = Omit, 'next'>; + +export function useStateSelector( + state$: BehaviorObservable, + selector: (state: S) => R, + equalityFn?: (arg0: R, arg1: R) => boolean +) { + const memoizedObservable = useMemo( + () => state$.pipe(map(selector), distinctUntilChanged(equalityFn)), + [state$, selector, equalityFn] + ); + const defaultValue = useMemo(() => selector(state$.value), [selector, state$]); + return useObservable(memoizedObservable, defaultValue); +} diff --git a/src/plugins/data_view_management/public/mocks.ts b/src/plugins/data_view_management/public/mocks.ts index 4a3963114373f..6abc53a64d3cf 100644 --- a/src/plugins/data_view_management/public/mocks.ts +++ b/src/plugins/data_view_management/public/mocks.ts @@ -74,6 +74,7 @@ const createIndexPatternManagmentContext = (): { docLinks, data, dataViews, + dataViewMgmtService: jest.fn(), noDataPage: noDataPagePublicMock.createStart(), unifiedSearch, dataViewFieldEditor, diff --git a/src/plugins/data_view_management/public/plugin.ts b/src/plugins/data_view_management/public/plugin.ts index bd818e4dfb441..77e8c12a13ad0 100644 --- a/src/plugins/data_view_management/public/plugin.ts +++ b/src/plugins/data_view_management/public/plugin.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import { CoreSetup, Plugin, PluginInitializerContext, IUiSettingsClient } from '@kbn/core/public'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { i18n } from '@kbn/i18n'; @@ -37,6 +37,7 @@ export interface IndexPatternManagementStartDependencies { spaces?: SpacesPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; savedObjectsManagement: SavedObjectsManagementPluginStart; + uiSettings: IUiSettingsClient; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/plugins/data_view_management/public/types.ts b/src/plugins/data_view_management/public/types.ts index eb09aead86195..b7a9279de8001 100644 --- a/src/plugins/data_view_management/public/types.ts +++ b/src/plugins/data_view_management/public/types.ts @@ -30,10 +30,12 @@ import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-manag import type { SettingsStart } from '@kbn/core-ui-settings-browser'; import type { NoDataPagePluginSetup } from '@kbn/no-data-page-plugin/public'; import type { IndexPatternManagementStart } from '.'; +import type { DataViewMgmtService } from './management_app/data_view_management_service'; export type StartServices = Pick; export interface IndexPatternManagmentContext extends StartServices { + dataViewMgmtService: DataViewMgmtService; application: ApplicationStart; chrome: ChromeStart; uiSettings: IUiSettingsClient; diff --git a/src/plugins/data_views/common/data_views/data_views.test.ts b/src/plugins/data_views/common/data_views/data_views.test.ts index 680d8eb037144..8d9e2fdbc148b 100644 --- a/src/plugins/data_views/common/data_views/data_views.test.ts +++ b/src/plugins/data_views/common/data_views/data_views.test.ts @@ -192,6 +192,7 @@ describe('IndexPatterns', () => { pattern: 'something', rollupIndex: undefined, type: undefined, + forceRefresh: false, }; await indexPatterns.get(id); @@ -206,12 +207,14 @@ describe('IndexPatterns', () => { const id = '1'; await indexPatterns.get(id); expect(apiClient.getFieldsForWildcard).toBeCalledWith({ + allowHidden: undefined, allowNoIndex: true, indexFilter: undefined, metaFields: false, pattern: 'something', rollupIndex: undefined, type: undefined, + forceRefresh: false, }); }); diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 5fe44fa230a15..5e07e91387878 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -639,6 +639,7 @@ export class DataViewsService { allowNoIndex: true, indexFilter: options.indexFilter, allowHidden: options.allowHidden, + forceRefresh: options.forceRefresh, }); }; @@ -841,21 +842,24 @@ export class DataViewsService { private getSavedObjectAndInit = async ( id: string, - displayErrors: boolean = true + displayErrors: boolean = true, + refreshFields: boolean = false ): Promise => { const savedObject = await this.savedObjectsClient.get(id); - return this.initFromSavedObject(savedObject, displayErrors); + return this.initFromSavedObject(savedObject, displayErrors, refreshFields); }; private initFromSavedObjectLoadFields = async ({ savedObjectId, spec, displayErrors = true, + refreshFields = false, }: { savedObjectId: string; spec: DataViewSpec; displayErrors?: boolean; + refreshFields?: boolean; }) => { const { title, type, typeMeta, runtimeFieldMap } = spec; const { fields, indices, etag } = await this.refreshFieldSpecMap( @@ -869,6 +873,7 @@ export class DataViewsService { rollupIndex: typeMeta?.params?.rollup_index, allowNoIndex: spec.allowNoIndex, allowHidden: spec.allowHidden, + forceRefresh: refreshFields, }, spec.fieldAttrs, displayErrors @@ -884,7 +889,8 @@ export class DataViewsService { private initFromSavedObject = async ( savedObject: SavedObject, - displayErrors: boolean = true + displayErrors: boolean = true, + refreshFields: boolean = false ): Promise => { const spec = this.savedObjectToSpec(savedObject); spec.fieldAttrs = savedObject.attributes.fieldAttrs @@ -900,6 +906,7 @@ export class DataViewsService { savedObjectId: savedObject.id, spec, displayErrors, + refreshFields, }); fields = fieldsAndIndices.fields; indices = fieldsAndIndices.indices; @@ -910,6 +917,7 @@ export class DataViewsService { savedObjectId: savedObject.id, spec, displayErrors, + refreshFields, }); fields = fieldsAndIndices.fields; indices = fieldsAndIndices.indices; @@ -1047,7 +1055,7 @@ export class DataViewsService { if (dataViewFromCache) { indexPatternPromise = dataViewFromCache; } else { - indexPatternPromise = this.getSavedObjectAndInit(id, displayErrors); + indexPatternPromise = this.getSavedObjectAndInit(id, displayErrors, refreshFields); this.dataViewCache.set(id, indexPatternPromise); }