From 4701edfbab0dd63b083d7080f5f53d496455b1a0 Mon Sep 17 00:00:00 2001 From: christineweng Date: Tue, 15 Jul 2025 16:59:58 -0500 Subject: [PATCH 01/12] managed data view --- .../kbn-content-management-utils/src/types.ts | 4 + .../logs/utils/get_all_logs_data_view_spec.ts | 1 + .../public/components/data_view_editor.tsx | 4 + .../data_view_editor_flyout_content.tsx | 32 ++++-- .../data_view_flyout_content_container.tsx | 4 + .../public/components/footer/footer.tsx | 14 ++- .../data_view_editor/public/open_editor.tsx | 4 + .../shared/data_view_editor/public/types.ts | 8 ++ .../common/content_management/v1/types.ts | 1 + .../common/data_views/abstract_data_views.ts | 7 ++ .../common/data_views/data_views.ts | 10 +- .../plugins/shared/data_views/common/types.ts | 7 +- .../schema/v1/cm_services.ts | 1 + .../public/create_data_view.ts | 1 + .../sidebar/discover_sidebar_responsive.tsx | 5 +- .../components/top_nav/discover_topnav.tsx | 13 +-- .../main/state_management/redux/hooks.tsx | 12 +- .../dataview_picker/change_dataview.test.tsx | 18 ++- .../dataview_picker/change_dataview.tsx | 38 ++++-- .../dataview_picker/data_view_picker.tsx | 6 - .../public/logs_explorer_url_schema.ts | 1 + .../components/data_view_picker/index.tsx | 5 +- .../hooks/use_managed_data_views.test.ts | 108 ------------------ .../hooks/use_managed_data_views.ts | 35 ------ .../hooks/use_saved_data_views.test.ts | 23 ++-- .../hooks/use_saved_data_views.ts | 17 ++- .../utils/create_explore_data_view.ts | 1 + .../containers/create_sourcerer_data_view.ts | 2 + 28 files changed, 162 insertions(+), 220 deletions(-) delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts delete mode 100644 x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts diff --git a/src/platform/packages/shared/kbn-content-management-utils/src/types.ts b/src/platform/packages/shared/kbn-content-management-utils/src/types.ts index 3a6c286a404aa..2d76a737d4069 100644 --- a/src/platform/packages/shared/kbn-content-management-utils/src/types.ts +++ b/src/platform/packages/shared/kbn-content-management-utils/src/types.ts @@ -75,6 +75,10 @@ export interface SavedObjectCreateOptions { * * For global object types (registered with `namespaceType: 'agnostic'`): this option cannot be used. */ initialNamespaces?: string[]; + /** + * Whether the object is managed by the application. + */ + managed?: boolean; } /** Saved Object search options - Pick and Omit to customize */ diff --git a/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts b/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts index 41a72405698c6..eb36e1a05fbe4 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts @@ -22,4 +22,5 @@ export const getAllLogsDataViewSpec = ({ }), title: allLogsIndexPattern, timeFieldName: '@timestamp', + managed: true, }); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx index ec193823a561e..2b32201781a99 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx @@ -25,6 +25,8 @@ export const DataViewEditor = ({ requireTimestampField = false, editData, allowAdHocDataView, + onDuplicate, + isEdit, getDataViewHelpText, }: DataViewEditorPropsWithServices) => { const { Provider: KibanaReactContextProvider } = @@ -45,6 +47,8 @@ export const DataViewEditor = ({ requireTimestampField={requireTimestampField} editData={editData} allowAdHocDataView={allowAdHocDataView} + onDuplicate={onDuplicate} + isEdit={isEdit} getDataViewHelpText={getDataViewHelpText} /> diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx index ab70360e68a9c..295deb41bf4e0 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -8,7 +8,7 @@ */ import type { ReactNode } from 'react'; -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo, useMemo } from 'react'; import { css } from '@emotion/react'; import { EuiTitle, @@ -72,6 +72,8 @@ export interface Props { showManagementLink?: boolean; allowAdHoc: boolean; dataViewEditorService: DataViewEditorService; + isEdit: boolean; + onDuplicate?: () => void; getDataViewHelpText?: (dataView: DataView) => ReactNode | string | undefined; } @@ -92,6 +94,8 @@ const IndexPatternEditorFlyoutContentComponent = ({ showManagementLink, getDataViewHelpText, dataViewEditorService, + onDuplicate, + isEdit, }: Props) => { const styles = useMemoCss(componentStyles); const isMobile = useIsWithinBreakpoints(['s', 'xs']); @@ -101,6 +105,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ } = useKibana(); const canSave = dataViews.getCanSaveSync(); + const isManaged = isEdit ? !!editData?.managed : false; const { form } = useForm({ // Prefill with data if editData exists @@ -113,8 +118,8 @@ const IndexPatternEditorFlyoutContentComponent = ({ ...(editData ? { title: editData.getIndexPattern(), - id: editData.id, - name: editData.name, + id: isEdit ? editData.id : undefined, + name: isEdit ? editData.name : undefined, allowHidden: editData.getAllowHidden(), ...(editData.timeFieldName === noTimeFieldValue ? { timestampField: { label: noTimeFieldLabel, value: noTimeFieldValue } } @@ -183,6 +188,15 @@ const IndexPatternEditorFlyoutContentComponent = ({ const rollupCaps = useObservable(dataViewEditorService.rollupCaps$); const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {}); + const namesNotAllowed = useMemo(() => { + // if form is popoluated but not editing an existing data view + // add the editData data view name to the not allowed names + if (!isEdit && editData) { + return [editData.name, ...(existingDataViewNames || [])]; + } + return existingDataViewNames || []; + }, [existingDataViewNames, isEdit, editData]); + useDebounce( () => { dataViewEditorService.setIndexPattern(title); @@ -285,10 +299,12 @@ const IndexPatternEditorFlyoutContentComponent = ({ : SubmittingType.persisting : undefined } - isEdit={!!editData} + isEdit={isEdit} isPersisted={Boolean(editData && editData.isPersisted())} allowAdHoc={allowAdHoc} canSave={canSave} + isManaged={isManaged} + onDuplicate={onDuplicate} /> ); @@ -297,9 +313,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ -

{editData ? editorTitleEditMode : editorTitle}

+

{editData && isEdit ? editorTitleEditMode : editorTitle}

- {showManagementLink && editData && editData.id && ( + {showManagementLink && isEdit && editData && editData.id && ( - + @@ -352,7 +368,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ { form.getFields().title.validate(); }} diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx index fdf4df7647ca2..0039ce6e3c55a 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -25,6 +25,8 @@ const DataViewFlyoutContentContainer = ({ editData, allowAdHocDataView, showManagementLink, + isEdit, + onDuplicate, getDataViewHelpText, }: DataViewEditorProps) => { const { @@ -101,6 +103,8 @@ const DataViewFlyoutContentContainer = ({ showManagementLink={showManagementLink} allowAdHoc={allowAdHocDataView || false} dataViewEditorService={dataViewEditorService} + onDuplicate={onDuplicate} + isEdit={isEdit || false} getDataViewHelpText={getDataViewHelpText} /> ); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx index de73a0c7d5f47..f1e819b82b898 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx @@ -26,12 +26,14 @@ export enum SubmittingType { interface FooterProps { onCancel: () => void; onSubmit: (isAdHoc?: boolean) => void; + onDuplicate?: () => void; submittingType: SubmittingType | undefined; submitDisabled: boolean; isEdit: boolean; isPersisted: boolean; allowAdHoc: boolean; canSave: boolean; + isManaged: boolean; } const closeButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutCloseButtonLabel', { @@ -66,6 +68,8 @@ export const Footer = ({ allowAdHoc, isPersisted, canSave, + isManaged, + onDuplicate, }: FooterProps) => { const isEditingAdHoc = isEdit && !isPersisted; const submitPersisted = () => { @@ -108,7 +112,7 @@ export const Footer = ({ )} - {(canSave || isEditingAdHoc) && ( + {(canSave || isEditingAdHoc) && !isManaged && ( )} + + {(canSave || isEditingAdHoc) && isManaged && onDuplicate && ( + + + {'Duplicate'} + + + )} diff --git a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx index 28c70cc0b4084..5037e89ab4b2c 100644 --- a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx @@ -50,6 +50,8 @@ export const getEditorOpener = requireTimestampField = false, allowAdHocDataView = false, editData, + onDuplicate, + isEdit, getDataViewHelpText, }: DataViewEditorProps): CloseEditor => { const closeEditor = () => { @@ -81,6 +83,8 @@ export const getEditorOpener = requireTimestampField={requireTimestampField} allowAdHocDataView={allowAdHocDataView} showManagementLink={Boolean(editData && editData.isPersisted())} + onDuplicate={onDuplicate} + isEdit={isEdit} getDataViewHelpText={getDataViewHelpText} /> , diff --git a/src/platform/plugins/shared/data_view_editor/public/types.ts b/src/platform/plugins/shared/data_view_editor/public/types.ts index 67c914d512f05..49550d10922fe 100644 --- a/src/platform/plugins/shared/data_view_editor/public/types.ts +++ b/src/platform/plugins/shared/data_view_editor/public/types.ts @@ -71,6 +71,14 @@ export interface DataViewEditorProps { * if set to true a link to the management page is shown */ showManagementLink?: boolean; + /** + * if set to true a duplicate button is shown + */ + onDuplicate?: () => void; + /** + * if set to true an existing data view is being edited + */ + isEdit?: boolean; /** * Optional callback to get help text based on the active data view */ diff --git a/src/platform/plugins/shared/data_views/common/content_management/v1/types.ts b/src/platform/plugins/shared/data_views/common/content_management/v1/types.ts index 367e91fec7b06..1bfce15b747b3 100644 --- a/src/platform/plugins/shared/data_views/common/content_management/v1/types.ts +++ b/src/platform/plugins/shared/data_views/common/content_management/v1/types.ts @@ -20,6 +20,7 @@ interface DataViewCreateOptions { id?: SavedObjectCreateOptions['id']; initialNamespaces?: SavedObjectCreateOptions['initialNamespaces']; overwrite?: SavedObjectCreateOptions['overwrite']; + managed?: SavedObjectCreateOptions['managed']; } interface DataViewUpdateOptions { diff --git a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts index 96c38bdb69025..4f42aeab87556 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts @@ -127,6 +127,10 @@ export abstract class AbstractDataView { * list of indices that the index pattern matched */ public matchedIndices: string[] = []; + /** + * Whether the data view is managed by the application. + */ + public managed: boolean = false; protected scriptedFieldsMap: DataViewFieldBaseSpecMap; @@ -196,6 +200,7 @@ export abstract class AbstractDataView { this.namespaces = spec.namespaces || []; this.name = spec.name || ''; this.allowHidden = spec.allowHidden || false; + this.managed = spec.managed || false; } getAllowHidden = () => this.allowHidden; @@ -388,6 +393,7 @@ export abstract class AbstractDataView { runtimeFieldMap: stringifyOrUndefined(this.runtimeFieldMap), name: this.name, allowHidden: this.allowHidden, + // managed: this.managed, }; } @@ -419,6 +425,7 @@ export abstract class AbstractDataView { allowNoIndex: this.allowNoIndex, name: this.name, allowHidden: this.getAllowHidden(), + managed: this.managed, }; // Filter undefined values from the spec diff --git a/src/platform/plugins/shared/data_views/common/data_views/data_views.ts b/src/platform/plugins/shared/data_views/common/data_views/data_views.ts index 03bf350a2bb55..d4fad32b15c43 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/data_views.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/data_views.ts @@ -90,6 +90,10 @@ export interface DataViewListItem { * Time field name if applicable */ timeFieldName?: string; + /** + * Whether the data view is managed by the application. + */ + managed?: boolean; } /** @@ -497,6 +501,7 @@ export class DataViewsService { typeMeta: obj?.attributes?.typeMeta && JSON.parse(obj?.attributes?.typeMeta), name: obj?.attributes?.name, timeFieldName: obj?.attributes?.timeFieldName, + managed: obj?.managed, })); }; @@ -827,6 +832,7 @@ export class DataViewsService { name, allowHidden, }, + managed, } = savedObject; const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined; @@ -864,6 +870,7 @@ export class DataViewsService { runtimeFieldMap: parsedRuntimeFieldMap, name, allowHidden, + managed, }; }; @@ -965,6 +972,7 @@ export class DataViewsService { spec.fieldFormats = savedObject.attributes.fieldFormatMap ? JSON.parse(savedObject.attributes.fieldFormatMap) : {}; + spec.managed = savedObject.managed; const indexPattern = await this.createFromSpec(spec, true, displayErrors); indexPattern.setEtag(etag); @@ -1262,13 +1270,13 @@ export class DataViewsService { throw new DuplicateDataViewError(`Duplicate data view: ${dataView.getName()}`); } } - const body = dataView.getAsSavedObjectBody(); const response: SavedObject = (await this.savedObjectsClient.create(body, { id: dataView.id, initialNamespaces: dataView.namespaces.length > 0 ? dataView.namespaces : undefined, overwrite, + managed: dataView.managed, })) as SavedObject; if (this.savedObjectsCache) { diff --git a/src/platform/plugins/shared/data_views/common/types.ts b/src/platform/plugins/shared/data_views/common/types.ts index 25cde7ef80e02..1e1f72754381d 100644 --- a/src/platform/plugins/shared/data_views/common/types.ts +++ b/src/platform/plugins/shared/data_views/common/types.ts @@ -301,7 +301,7 @@ export interface PersistenceAPI { create: ( attributes: DataViewAttributes, // SavedObjectsCreateOptions - options: { id?: string; initialNamespaces?: string[]; overwrite?: boolean } + options: { id?: string; initialNamespaces?: string[]; overwrite?: boolean; managed?: boolean } ) => Promise; /** * Delete a saved object by id @@ -326,6 +326,7 @@ export interface GetFieldsOptions { includeEmptyFields?: boolean; abortSignal?: AbortSignal; runtimeMappings?: estypes.MappingRuntimeFields; + // managed?: boolean; } /** @@ -562,6 +563,10 @@ export type DataViewSpec = { * Allow hidden and system indices when loading field list */ allowHidden?: boolean; + /** + * Whether the data view is managed by the application. + */ + managed?: boolean; }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions diff --git a/src/platform/plugins/shared/data_views/server/content_management/schema/v1/cm_services.ts b/src/platform/plugins/shared/data_views/server/content_management/schema/v1/cm_services.ts index 7e1d35a1179e1..058149c68c88c 100644 --- a/src/platform/plugins/shared/data_views/server/content_management/schema/v1/cm_services.ts +++ b/src/platform/plugins/shared/data_views/server/content_management/schema/v1/cm_services.ts @@ -65,6 +65,7 @@ const dataViewCreateOptionsSchema = schema.object({ id: createOptionsSchemas.id, initialNamespaces: createOptionsSchemas.initialNamespaces, overwrite: schema.maybe(createOptionsSchemas.overwrite), + managed: schema.maybe(schema.boolean()), }); const dataViewSearchOptionsSchema = schema.object({ diff --git a/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts b/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts index eb1bc9590ee4b..38078687bec97 100644 --- a/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts +++ b/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts @@ -80,6 +80,7 @@ const registerCreateDataViewRouteFactory = data_view: serviceKey === SERVICE_KEY ? dataViewSpecSchema : schema.never(), index_pattern: serviceKey === SERVICE_KEY_LEGACY ? dataViewSpecSchema : schema.never(), + managed: schema.maybe(schema.boolean({ defaultValue: false })), }), }, response: { diff --git a/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 04186920ef0ff..b24f50e3a5253 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -181,7 +181,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) ); const selectedDataViewRef = useRef(selectedDataView); const showFieldList = sidebarState.status !== DiscoverSidebarReducerStatus.INITIAL; - const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker(); + const { savedDataViews, adHocDataViews } = useDataViewsForPicker(); useEffect(() => { const subscription = props.documents$.subscribe((documentState) => { @@ -314,6 +314,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) onSave: async (dataView) => { onDataViewCreated(dataView); }, + isEdit: true, }); if (setDataViewEditorRef) { setDataViewEditorRef(ref); @@ -336,7 +337,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) state.query); - const { savedDataViews, managedDataViews, adHocDataViews } = useDataViewsForPicker(); + const { savedDataViews, adHocDataViews } = useDataViewsForPicker(); const dataView = useCurrentDataView(); const isESQLToDataViewTransitionModalVisible = useInternalStateSelector( (state) => state.isESQLToDataViewTransitionModalVisible @@ -191,19 +191,10 @@ export const DiscoverTopNav = ({ onCreateDefaultAdHocDataView: stateContainer.actions.createAndAppendAdHocDataView, onChangeDataView: stateContainer.actions.onChangeDataView, adHocDataViews, - managedDataViews, savedDataViews, onEditDataView: stateContainer.actions.onDataViewEdited, }; - }, [ - adHocDataViews, - addField, - createNewDataView, - dataView, - managedDataViews, - savedDataViews, - stateContainer, - ]); + }, [adHocDataViews, addField, createNewDataView, dataView, savedDataViews, stateContainer]); const onESQLDocsFlyoutVisibilityChanged = useCallback((isOpen: boolean) => { if (isOpen) { diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx index a034da956177e..95a05c19c8ba5 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx @@ -102,16 +102,10 @@ export const useCurrentChartPortalNode = () => useCurrentTabContext().currentCha export const useDataViewsForPicker = () => { const originalAdHocDataViews = useAdHocDataViews(); const savedDataViews = useInternalStateSelector((state) => state.savedDataViews); - const defaultProfileAdHocDataViewIds = useInternalStateSelector( - (state) => state.defaultProfileAdHocDataViewIds - ); return useMemo(() => { - const managedDataViews = originalAdHocDataViews.filter( - ({ id }) => id && defaultProfileAdHocDataViewIds.includes(id) - ); - const adHocDataViews = differenceBy(originalAdHocDataViews, managedDataViews, 'id'); + const adHocDataViews = differenceBy(originalAdHocDataViews, savedDataViews, 'id'); - return { savedDataViews, managedDataViews, adHocDataViews }; - }, [defaultProfileAdHocDataViewIds, originalAdHocDataViews, savedDataViews]); + return { savedDataViews, adHocDataViews }; + }, [originalAdHocDataViews, savedDataViews]); }; diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx index bd111145435a4..f68695926f517 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -20,6 +20,9 @@ import { DataViewSelector } from './data_view_selector'; import { dataViewMock, dataViewMockEsql } from './mocks/dataview'; import type { DataViewPickerProps } from './data_view_picker'; +const dataViewEditorMock = dataViewEditorPluginMock.createStartContract(); +const openEditorMock = jest.fn(); + describe('DataView component', () => { const createMockWebStorage = () => ({ clear: jest.fn(), @@ -48,8 +51,8 @@ describe('DataView component', () => { storageValue: boolean, uiSettingValue: boolean = false ) { - const dataViewEditorMock = dataViewEditorPluginMock.createStartContract(); (dataViewEditorMock.userPermissions.editDataView as jest.Mock).mockReturnValue(true); + (dataViewEditorMock.openEditor as jest.Mock).mockImplementation(openEditorMock); let dataMock = dataPluginMock.createStartContract(); dataMock = { ...dataMock, @@ -66,6 +69,9 @@ describe('DataView component', () => { uiSettings: { get: jest.fn(() => uiSettingValue), }, + application: { + navigateToApp: jest.fn(), + }, }; return ( @@ -78,6 +84,7 @@ describe('DataView component', () => { } let props: DataViewPickerProps; beforeEach(() => { + jest.clearAllMocks(); props = { currentDataViewId: 'dataview-1', trigger: { @@ -209,8 +216,14 @@ describe('DataView component', () => { id: 'dataview-1', title: 'dataview-1', }, + { + id: 'the-data-view-id', + title: 'the-data-view-title', + name: 'the-data-view', + type: 'default', + managed: true, + }, ], - managedDataViews: [dataViewMock], }, false ) @@ -226,6 +239,7 @@ describe('DataView component', () => { title: 'the-data-view-title', name: 'the-data-view', type: 'default', + managed: true, isManaged: true, }, ]); diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx index 9aa3c3fea0e1c..a9223b8cefb4c 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx @@ -26,7 +26,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; +import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/public'; import type { IUnifiedSearchPluginServices } from '../types'; import { type DataViewPickerProps } from './data_view_picker'; import type { DataViewListItemEnhanced } from './dataview_list'; @@ -45,11 +45,16 @@ const mapDataViewListItem = ( ...partial, }); -const mapAdHocDataView = (adHocDataView: DataView) => - mapDataViewListItem(adHocDataView, { isAdhoc: true }); +const mapAdHocDataView = (adHocDataView: DataView) => { + if (adHocDataView.managed) { + return mapDataViewListItem(adHocDataView, { isManaged: true }); + } + return mapDataViewListItem(adHocDataView, { isAdhoc: true }); +}; -const mapManagedDataView = (managedDataView: DataView) => - mapDataViewListItem(managedDataView, { isManaged: true }); +const mapManagedDataView = (managedDataView: DataViewListItem) => { + return { ...managedDataView, isManaged: true }; +}; const shrinkableContainerCss = css` min-width: 0; @@ -59,7 +64,6 @@ export function ChangeDataView({ isMissingCurrent, currentDataViewId, adHocDataViews, - managedDataViews, savedDataViews, onChangeDataView, onAddField, @@ -100,16 +104,16 @@ export function ChangeDataView({ useEffect(() => { const fetchDataViews = async () => { - const savedDataViewRefs = savedDataViews + const availableDataViewRefs = savedDataViews ? savedDataViews : (await data.dataViews.getIdsWithTitle()) ?? []; + const savedDataViewRefs = availableDataViewRefs.filter((dataView) => !dataView.managed); + const managedDataViewRefs = savedDataViews?.filter((dataView) => dataView.managed === true).map(mapManagedDataView) ?? []; const adHocDataViewRefs = adHocDataViews?.map(mapAdHocDataView) ?? []; - const managedDataViewRefs = managedDataViews?.map(mapManagedDataView) ?? []; - setDataViewsList([...savedDataViewRefs, ...adHocDataViewRefs, ...managedDataViewRefs]); }; fetchDataViews(); - }, [data, currentDataViewId, adHocDataViews, savedDataViews, managedDataViews]); + }, [data, currentDataViewId, adHocDataViews, savedDataViews]); const isAdHocSelected = useMemo(() => { return adHocDataViews?.some((dataView) => dataView.id === currentDataViewId); @@ -148,6 +152,18 @@ export function ChangeDataView({ ); }; + const onDuplicate = useCallback(async () => { + const dataView = await dataViews.get(currentDataViewId!); + if (onEditDataView) { + dataViewEditor.openEditor({ + editData: dataView, + onSave: (updatedDataView) => { + onEditDataView(updatedDataView); + }, + allowAdHocDataView: true, + }); + } + }, [currentDataViewId, dataViews, onEditDataView, dataViewEditor]); const items = useMemo(() => { const panelItems: EuiContextMenuPanelProps['items'] = []; @@ -179,6 +195,8 @@ export function ChangeDataView({ onSave: (updatedDataView) => { onEditDataView(updatedDataView); }, + onDuplicate, + isEdit: true, getDataViewHelpText, }); } else { diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx index f5b310f04c14d..8abc62cb4b321 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx @@ -45,10 +45,6 @@ export interface DataViewPickerProps { * The adHocDataviews. */ adHocDataViews?: DataView[]; - /** - * Data views managed by the application - */ - managedDataViews?: DataView[]; /** * Saved data views */ @@ -87,7 +83,6 @@ export const DataViewPicker = ({ isMissingCurrent, currentDataViewId, adHocDataViews, - managedDataViews, savedDataViews, onChangeDataView, onEditDataView, @@ -112,7 +107,6 @@ export const DataViewPicker = ({ onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView} trigger={trigger} adHocDataViews={adHocDataViews} - managedDataViews={managedDataViews} savedDataViews={savedDataViews} selectableProps={selectableProps} isDisabled={isDisabled} diff --git a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.ts b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.ts index 524641ea0ba6c..929860a1ec505 100644 --- a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.ts +++ b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.ts @@ -85,6 +85,7 @@ export const hydrateDataSourceSelection = ( name: 'All logs', title: 'logs-*,dataset-logs-*-*', timeFieldName: '@timestamp', + managed: true, }; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index c2ce011e511d4..812fddb9deb8b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -20,7 +20,6 @@ import { sharedStateSelector } from '../../redux/selectors'; import { sharedDataViewManagerSlice } from '../../redux/slices'; import { useSelectDataView } from '../../hooks/use_select_data_view'; import { DataViewManagerScopeName } from '../../constants'; -import { useManagedDataViews } from '../../hooks/use_managed_data_views'; import { useSavedDataViews } from '../../hooks/use_saved_data_views'; import { LOADING } from './translations'; import { DATA_VIEW_PICKER_TEST_ID } from './constants'; @@ -68,7 +67,6 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie .map((spec) => new DataView({ spec, fieldFormats })); }, [adhocDataViewSpecs, fieldFormats]); - const managedDataViews = useManagedDataViews(); const savedDataViews = useSavedDataViews(); const isDefaultSourcerer = scope === DataViewManagerScopeName.default; @@ -98,7 +96,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const createNewDataView = useCallback(() => { closeDataViewEditor.current = dataViewEditor.openEditor({ - onSave: async (newDataView) => { + onSave: async (newDataView: DataView) => { if (!newDataView.id) { return; } @@ -194,7 +192,6 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie onDataViewCreated={createNewDataView} adHocDataViews={adhocDataViews} savedDataViews={savedDataViews} - managedDataViews={managedDataViews} onClosePopover={onClosePopover} getDataViewHelpText={getDataViewHelpText} /> diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts deleted file mode 100644 index 33ce7a1d3b5fb..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { renderHook } from '@testing-library/react'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { useManagedDataViews } from './use_managed_data_views'; -import { DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID } from '../constants'; -import { DEFAULT_ALERT_DATA_VIEW_ID } from '../../../common/constants'; - -// Mock dependencies -jest.mock('react-redux', () => ({ - useSelector: jest.fn(), -})); - -jest.mock('../../common/lib/kibana', () => ({ - useKibana: jest.fn(), -})); - -jest.mock('@kbn/data-views-plugin/public', () => ({ - DataView: jest.fn(), -})); - -import { useSelector } from 'react-redux'; -import { useKibana } from '../../common/lib/kibana'; - -describe('useManagedDataViews', () => { - const mockFieldFormats = {}; - - beforeEach(() => { - jest.clearAllMocks(); - - // Mock DataView constructor - (DataView as jest.Mock).mockImplementation(({ spec }) => ({ - ...spec, - })); - - // Mock useKibana - (useKibana as jest.Mock).mockReturnValue({ - services: { - fieldFormats: mockFieldFormats, - }, - }); - }); - - it('should filter data views to only include those with Alert Data View ID', () => { - // Create mock data views with a mix of IDs - const mockDataViews = [ - { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, title: 'Security solution data view' }, - { id: 'some-other-id', title: 'Other data view' }, - { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, title: 'Another security solution data view' }, - { id: DEFAULT_ALERT_DATA_VIEW_ID, title: 'Security alert data view' }, - ]; - - // Mock the Redux selector - (useSelector as jest.Mock).mockReturnValue({ - dataViews: mockDataViews, - adhocDataViews: [], - defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - alertDataViewId: DEFAULT_ALERT_DATA_VIEW_ID, - }); - - // Render the hook - const { result } = renderHook(() => useManagedDataViews()); - - // Expect only data views with matching ID to be included - expect(result.current.length).toBe(1); - - // Verify the IDs of the filtered data views - result.current.forEach((dataView, i) => { - if (i <= 1) { - expect(dataView.id).toBe(DEFAULT_ALERT_DATA_VIEW_ID); - } - }); - - // Verify DataView constructor was called with correct arguments - expect(DataView).toHaveBeenCalledTimes(1); - expect(DataView).toHaveBeenCalledWith({ - spec: mockDataViews[3], - fieldFormats: mockFieldFormats, - }); - }); - - it('should return an empty array when no data views match the filter criteria', () => { - // Create mock data views with no matching IDs - const mockDataViews = [ - { id: 'some-id', title: 'Some Data View' }, - { id: 'another-id', title: 'Another Data View' }, - ]; - (useSelector as jest.Mock).mockReturnValue({ - dataViews: mockDataViews, - adhocDataViews: [], - defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - }); - - const { result } = renderHook(() => useManagedDataViews()); - - // Expect no data views to be included - expect(result.current).toEqual([]); - expect(result.current.length).toBe(0); - - // Verify DataView constructor was not called - expect(DataView).not.toHaveBeenCalled(); - }); -}); diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts deleted file mode 100644 index cf182c93d2fc0..0000000000000 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_managed_data_views.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataView } from '@kbn/data-views-plugin/public'; -import { useSelector } from 'react-redux'; -import { useMemo } from 'react'; -import { EXPLORE_DATA_VIEW_PREFIX } from '../../../common/constants'; -import { useKibana } from '../../common/lib/kibana'; -import { sharedStateSelector } from '../redux/selectors'; - -/** - * Returns the default security solution data view and alert data view - */ -export const useManagedDataViews = (): DataView[] => { - const { - dataViews: dataViewSpecs, - adhocDataViews, - alertDataViewId, - } = useSelector(sharedStateSelector); - const { - services: { fieldFormats }, - } = useKibana(); - - return useMemo( - () => - [...dataViewSpecs, ...adhocDataViews] - .filter((dv) => dv.id === alertDataViewId || dv.id?.startsWith(EXPLORE_DATA_VIEW_PREFIX)) - .map((spec) => new DataView({ spec, fieldFormats })), - [dataViewSpecs, adhocDataViews, alertDataViewId, fieldFormats] - ); -}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts index 94d3205c97de9..f1c2f7fd18522 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts @@ -20,18 +20,20 @@ describe('useSavedDataViews', () => { jest.clearAllMocks(); }); - it('should not filter out the default data view and transform the remaining ones', () => { + it('should not transform saved data views', () => { // Mock data to be returned by the selector const mockDataViews = [ { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, // This should not be filtered out title: 'Default View', name: 'default_view', + managed: true, }, { id: DEFAULT_ALERT_DATA_VIEW_ID, // This should be filtered out title: 'Default Alert View', name: 'default_alert_view', + managed: true, }, { id: 'custom-view-1', @@ -48,19 +50,13 @@ describe('useSavedDataViews', () => { // Mock the useSelector to return our test data (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews, - defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - alertDataViewId: DEFAULT_ALERT_DATA_VIEW_ID, }); // Render the hook const { result } = renderHook(() => useSavedDataViews()); - // Expect the alert view to be filtered out - expect(result.current).toHaveLength(3); - expect( - result.current.find((item) => item.id === DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID) - ).not.toBeUndefined(); - expect(result.current.find((item) => item.id === DEFAULT_ALERT_DATA_VIEW_ID)).toBeUndefined(); + // Expect the default view to be filtered out + expect(result.current).toHaveLength(4); // Expect the custom views to be correctly transformed expect(result.current).toEqual([ @@ -69,6 +65,12 @@ describe('useSavedDataViews', () => { title: 'Default View', name: 'default_view', }, + { + id: DEFAULT_ALERT_DATA_VIEW_ID, + title: 'Default Alert View', + name: 'default_alert_view', + managed: true, + }, { id: 'custom-view-1', title: 'Custom View 1', @@ -86,7 +88,6 @@ describe('useSavedDataViews', () => { // Mock the useSelector to return an empty array (useSelector as jest.Mock).mockReturnValue({ dataViews: [], - defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, }); // Render the hook @@ -114,8 +115,6 @@ describe('useSavedDataViews', () => { // Mock the useSelector (useSelector as jest.Mock).mockReturnValue({ dataViews: mockDataViews, - defaultDataViewId: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, - alertDataViewId: DEFAULT_ALERT_DATA_VIEW_ID, }); // Render the hook diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts index fdde44eb554bf..6ed389f8985c6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.ts @@ -15,17 +15,16 @@ import { sharedStateSelector } from '../redux/selectors'; * The list excludes managed data views (default security solution data view and alert data view) */ export const useSavedDataViews = (): DataViewListItem[] => { - const { dataViews: dataViewSpecs, alertDataViewId } = useSelector(sharedStateSelector); + const { dataViews: dataViewSpecs } = useSelector(sharedStateSelector); return useMemo( () => - dataViewSpecs - .filter((dv) => dv.id !== alertDataViewId) - .map((spec) => ({ - id: spec.id ?? '', - title: spec.title ?? '', - name: spec.name, - })), - [dataViewSpecs, alertDataViewId] + dataViewSpecs.map((spec) => ({ + id: spec.id ?? '', + title: spec.title ?? '', + name: spec.name, + managed: spec.managed, + })), + [dataViewSpecs] ); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_explore_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_explore_data_view.ts index c94bd887aee2b..5a0905f5fcdda 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_explore_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/utils/create_explore_data_view.ts @@ -26,5 +26,6 @@ export const createExploreDataView = async ( id: `${EXPLORE_DATA_VIEW_PREFIX}-${(await dependencies.spaces.getActiveSpace()).id}`, name: SECURITY_SOLUTION_EXPLORE_DATA_VIEW, title: exploreDataViewPattern, + managed: true, }); }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts index b23c8a7a993fc..29d0635672fd2 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts @@ -63,6 +63,7 @@ export const createSourcererDataView = async ({ title: patternListAsTitle, timeFieldName: DEFAULT_TIME_FIELD, name: DEFAULT_SECURITY_DATA_VIEW, + managed: true, }, // Override property - if a data view exists with the security solution pattern // delete it and replace it with our data view @@ -108,6 +109,7 @@ export const createSourcererDataView = async ({ title: signalIndexName, timeFieldName: DEFAULT_TIME_FIELD, name: DEFAULT_SECURITY_ALERT_DATA_VIEW, + managed: true, }, // Override property - if a data view exists with the security solution pattern // delete it and replace it with our data view From b727b887a761c96a23c6806207e0a9043436844b Mon Sep 17 00:00:00 2001 From: christineweng Date: Mon, 28 Jul 2025 21:39:51 -0500 Subject: [PATCH 02/12] comments and fixes --- packages/kbn-optimizer/limits.yml | 2 +- .../public/components/data_view_editor.tsx | 4 +- .../data_view_editor_flyout_content.tsx | 42 ++++--- .../data_view_flyout_content_container.tsx | 6 +- .../public/components/footer/footer.tsx | 29 +++-- .../components/form_fields/name_field.tsx | 4 +- .../form_fields/timestamp_field.tsx | 10 +- .../components/form_fields/title_field.tsx | 3 + .../data_view_editor/public/open_editor.tsx | 4 +- .../shared/data_view_editor/public/types.ts | 7 +- .../field_editor_flyout_content.tsx | 30 +++-- .../__snapshots__/utils.test.ts.snap | 2 + .../edit_index_pattern/edit_index_pattern.tsx | 4 +- .../index_pattern_table.tsx | 4 +- .../public/components/types.ts | 1 + .../public/components/utils.ts | 17 ++- .../__snapshots__/data_view.test.ts.snap | 3 + .../__snapshots__/data_view_lazy.test.ts.snap | 3 + .../__snapshots__/data_views.test.ts.snap | 1 + .../common/data_views/abstract_data_views.ts | 1 - .../common/data_views/data_views.ts | 1 - .../plugins/shared/data_views/common/types.ts | 2 +- .../server/rest_api_routes/schema.ts | 1 + .../sidebar/discover_sidebar_responsive.tsx | 18 +-- .../components/top_nav/discover_topnav.tsx | 15 +-- .../main/state_management/redux/hooks.tsx | 8 +- .../classic_nav_root_profile/profile.test.ts | 1 + .../profile.test.ts | 1 + .../dataview_picker/change_dataview.test.tsx | 8 +- .../dataview_picker/change_dataview.tsx | 118 ++++++++++-------- .../dataview_picker/data_view_picker.tsx | 3 +- .../public/dataview_picker/dataview_list.tsx | 13 +- .../_get_default_ad_hoc_data_views.ts | 12 +- .../lens/public/app_plugin/lens_top_nav.tsx | 44 +++---- .../log_views/log_views_client.test.ts | 1 + .../lib/fetch_search_source_query.test.ts | 1 + .../data_views/integration/integration.ts | 1 + .../_get_default_ad_hoc_data_views.ts | 12 +- .../public/logs_explorer_url_schema.test.ts | 1 + .../custom_common/index_selection.tsx | 28 ++--- .../data_view_picker/index.test.tsx | 42 +------ .../components/data_view_picker/index.tsx | 29 ++--- .../hooks/use_saved_data_views.test.ts | 2 - .../public/data_view_manager/redux/slices.ts | 8 +- .../containers/create_sourcerer_data_view.ts | 1 - .../components/fields_browser/index.tsx | 2 +- 46 files changed, 281 insertions(+), 269 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index e2359a59dcbd1..4c251d93b45ee 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -174,7 +174,7 @@ pageLoadAssetSize: uiActions: 24712 uiActionsEnhanced: 20189 unifiedDocViewer: 14513 - unifiedSearch: 23000 + unifiedSearch: 23500 upgradeAssistant: 6898 uptime: 48184 urlDrilldown: 21563 diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx index 2b32201781a99..79daa0c3b5181 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx @@ -26,7 +26,7 @@ export const DataViewEditor = ({ editData, allowAdHocDataView, onDuplicate, - isEdit, + isDuplicatingManaged, getDataViewHelpText, }: DataViewEditorPropsWithServices) => { const { Provider: KibanaReactContextProvider } = @@ -48,7 +48,7 @@ export const DataViewEditor = ({ editData={editData} allowAdHocDataView={allowAdHocDataView} onDuplicate={onDuplicate} - isEdit={isEdit} + isDuplicatingManaged={isDuplicatingManaged} getDataViewHelpText={getDataViewHelpText} /> diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 295deb41bf4e0..e8baaa1b0a30f 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -8,7 +8,7 @@ */ import type { ReactNode } from 'react'; -import React, { useEffect, useCallback, useMemo, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import { css } from '@emotion/react'; import { EuiTitle, @@ -72,7 +72,7 @@ export interface Props { showManagementLink?: boolean; allowAdHoc: boolean; dataViewEditorService: DataViewEditorService; - isEdit: boolean; + isDuplicatingManaged: boolean; onDuplicate?: () => void; getDataViewHelpText?: (dataView: DataView) => ReactNode | string | undefined; } @@ -95,7 +95,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ getDataViewHelpText, dataViewEditorService, onDuplicate, - isEdit, + isDuplicatingManaged, }: Props) => { const styles = useMemoCss(componentStyles); const isMobile = useIsWithinBreakpoints(['s', 'xs']); @@ -105,7 +105,10 @@ const IndexPatternEditorFlyoutContentComponent = ({ } = useKibana(); const canSave = dataViews.getCanSaveSync(); - const isManaged = isEdit ? !!editData?.managed : false; + // Edit form is populated and disabled if the current data view is managed + // and the data view is not being duplicated + const isFormDisabled = !!editData?.managed && !isDuplicatingManaged; + const isEditingExisting = editData && !editData.managed && !isDuplicatingManaged; const { form } = useForm({ // Prefill with data if editData exists @@ -118,8 +121,8 @@ const IndexPatternEditorFlyoutContentComponent = ({ ...(editData ? { title: editData.getIndexPattern(), - id: isEdit ? editData.id : undefined, - name: isEdit ? editData.name : undefined, + id: isDuplicatingManaged ? undefined : editData.id, + name: isDuplicatingManaged ? undefined : editData.name, allowHidden: editData.getAllowHidden(), ...(editData.timeFieldName === noTimeFieldValue ? { timestampField: { label: noTimeFieldLabel, value: noTimeFieldValue } } @@ -155,7 +158,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ }; } - if (editData && editData.getIndexPattern() !== formData.title) { + if (isEditingExisting && editData.getIndexPattern() !== formData.title) { await editDataViewModal({ dataViewName: formData.name || formData.title, overlays, @@ -189,13 +192,13 @@ const IndexPatternEditorFlyoutContentComponent = ({ const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {}); const namesNotAllowed = useMemo(() => { - // if form is popoluated but not editing an existing data view - // add the editData data view name to the not allowed names - if (!isEdit && editData) { + // When duplicating a managed data view, add the existing name + // to the not allowed names list + if (isDuplicatingManaged && editData) { return [editData.name, ...(existingDataViewNames || [])]; } return existingDataViewNames || []; - }, [existingDataViewNames, isEdit, editData]); + }, [existingDataViewNames, isDuplicatingManaged, editData]); useDebounce( () => { @@ -299,12 +302,13 @@ const IndexPatternEditorFlyoutContentComponent = ({ : SubmittingType.persisting : undefined } - isEdit={isEdit} + hasEditData={!!editData} isPersisted={Boolean(editData && editData.isPersisted())} allowAdHoc={allowAdHoc} canSave={canSave} - isManaged={isManaged} + isManaged={!!editData?.managed} onDuplicate={onDuplicate} + isDuplicatingManaged={isDuplicatingManaged} /> ); @@ -313,9 +317,11 @@ const IndexPatternEditorFlyoutContentComponent = ({ -

{editData && isEdit ? editorTitleEditMode : editorTitle}

+

+ {editData && !isDuplicatingManaged ? editorTitleEditMode : editorTitle} +

- {showManagementLink && isEdit && editData && editData.id && ( + {showManagementLink && !isDuplicatingManaged && editData && editData.id && ( - + @@ -353,6 +359,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ dataViewEditorService.indexPatternValidationProvider } titleHelpText={titleHelpText} + disabled={isFormDisabled} /> @@ -363,12 +370,13 @@ const IndexPatternEditorFlyoutContentComponent = ({ options$={dataViewEditorService.timestampFieldOptions$} isLoadingOptions$={dataViewEditorService.loadingTimestampFields$} matchedIndices$={dataViewEditorService.matchedIndices$} + disabled={isFormDisabled} /> { form.getFields().title.validate(); }} diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx index 0039ce6e3c55a..db1943dd7c6a8 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -25,8 +25,8 @@ const DataViewFlyoutContentContainer = ({ editData, allowAdHocDataView, showManagementLink, - isEdit, onDuplicate, + isDuplicatingManaged, getDataViewHelpText, }: DataViewEditorProps) => { const { @@ -54,7 +54,7 @@ const DataViewFlyoutContentContainer = ({ const onSaveClick = async (dataViewSpec: DataViewSpec, persist: boolean = true) => { try { let saveResponse; - if (editData) { + if (editData && !editData.managed && !isDuplicatingManaged) { const { name = '', timeFieldName, title = '', allowHidden = false } = dataViewSpec; editData.setIndexPattern(title); editData.name = name; @@ -104,7 +104,7 @@ const DataViewFlyoutContentContainer = ({ allowAdHoc={allowAdHocDataView || false} dataViewEditorService={dataViewEditorService} onDuplicate={onDuplicate} - isEdit={isEdit || false} + isDuplicatingManaged={isDuplicatingManaged || false} getDataViewHelpText={getDataViewHelpText} /> ); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx index f1e819b82b898..5b1de19497cfd 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx @@ -29,11 +29,12 @@ interface FooterProps { onDuplicate?: () => void; submittingType: SubmittingType | undefined; submitDisabled: boolean; - isEdit: boolean; + hasEditData: boolean; isPersisted: boolean; allowAdHoc: boolean; canSave: boolean; isManaged: boolean; + isDuplicatingManaged: boolean; } const closeButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutCloseButtonLabel', { @@ -48,6 +49,13 @@ const editButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutEditButt defaultMessage: 'Save', }); +const duplicateButtonLabel = i18n.translate( + 'indexPatternEditor.editor.flyoutDuplicateButtonLabel', + { + defaultMessage: 'Duplicate', + } +); + const editUnpersistedButtonLabel = i18n.translate( 'indexPatternEditor.editor.flyoutEditUnpersistedButtonLabel', { @@ -64,14 +72,19 @@ export const Footer = ({ onSubmit, submittingType, submitDisabled, - isEdit, + hasEditData, allowAdHoc, isPersisted, canSave, - isManaged, onDuplicate, + isManaged, + isDuplicatingManaged, }: FooterProps) => { - const isEditingAdHoc = isEdit && !isPersisted; + const isEditingAdHoc = hasEditData && !isPersisted; + + const isEditing = (canSave || isEditingAdHoc) && !isManaged; + const showDuplicateButton = (canSave || isEditingAdHoc) && isManaged && onDuplicate; + const submitPersisted = () => { onSubmit(false); }; @@ -112,7 +125,7 @@ export const Footer = ({ )} - {(canSave || isEditingAdHoc) && !isManaged && ( + {isEditing && ( - {isEdit + {hasEditData && !isDuplicatingManaged ? isPersisted ? editButtonLabel : editUnpersistedButtonLabel @@ -134,10 +147,10 @@ export const Footer = ({ )} - {(canSave || isEditingAdHoc) && isManaged && onDuplicate && ( + {showDuplicateButton && ( - {'Duplicate'} + {duplicateButtonLabel} )} diff --git a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx index 8b4a5236627a9..6759abf9f72f3 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/name_field.tsx @@ -18,6 +18,7 @@ import { schema } from '../form_schema'; interface NameFieldProps { namesNotAllowed: string[]; + disabled?: boolean; } interface GetNameConfigArgs { @@ -49,7 +50,7 @@ const getNameConfig = ({ namesNotAllowed }: GetNameConfigArgs): FieldConfig { +export const NameField = ({ namesNotAllowed, disabled }: NameFieldProps) => { const config = useMemo( () => getNameConfig({ @@ -76,6 +77,7 @@ export const NameField = ({ namesNotAllowed }: NameFieldProps) => { ) => { field.setValue(e.target.value); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx index 7e82d87ea7547..6d6c64b24c518 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/timestamp_field.tsx @@ -25,6 +25,7 @@ interface Props { options$: Observable; isLoadingOptions$: Observable; matchedIndices$: Observable; + disabled?: boolean; } export const requireTimestampOptionValidator = ( @@ -71,7 +72,12 @@ const timestampFieldHelp = i18n.translate('indexPatternEditor.editor.form.timeFi defaultMessage: 'Select a timestamp field for use with the global time filter.', }); -export const TimestampField = ({ options$, isLoadingOptions$, matchedIndices$ }: Props) => { +export const TimestampField = ({ + options$, + isLoadingOptions$, + matchedIndices$, + disabled, +}: Props) => { const options = useObservable(options$, []); const isLoadingOptions = useObservable(isLoadingOptions$, false); const hasMatchedIndices = !!useObservable(matchedIndices$, matchedIndiciesDefault) @@ -98,7 +104,7 @@ export const TimestampField = ({ options$, isLoadingOptions$, matchedIndices$ }: } const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const isDisabled = !optionsAsComboBoxOptions.length || isLoadingOptions; + const isDisabled = !optionsAsComboBoxOptions.length || isLoadingOptions || disabled; // if the value isn't in the list then don't use it. const valueInList = !!optionsAsComboBoxOptions.find( (option) => option.value === value.value diff --git a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/title_field.tsx b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/title_field.tsx index 596e6f6b1ae79..1a7874dc071be 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/form_fields/title_field.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/form_fields/title_field.tsx @@ -30,6 +30,7 @@ interface TitleFieldProps { matchedIndices: MatchedIndicesSet; rollupIndex: string | null | undefined; }>; + disabled?: boolean; titleHelpText?: ReactNode | string; } @@ -146,6 +147,7 @@ export const TitleField = ({ matchedIndices$, rollupIndicesCapabilities, indexPatternValidationProvider, + disabled, titleHelpText, }: TitleFieldProps) => { const [appendedWildcard, setAppendedWildcard] = useState(false); @@ -188,6 +190,7 @@ export const TitleField = ({ > ) => { let query = e.target.value; diff --git a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx index 5037e89ab4b2c..5f82559aaaf14 100644 --- a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx @@ -51,7 +51,7 @@ export const getEditorOpener = allowAdHocDataView = false, editData, onDuplicate, - isEdit, + isDuplicatingManaged, getDataViewHelpText, }: DataViewEditorProps): CloseEditor => { const closeEditor = () => { @@ -84,7 +84,7 @@ export const getEditorOpener = allowAdHocDataView={allowAdHocDataView} showManagementLink={Boolean(editData && editData.isPersisted())} onDuplicate={onDuplicate} - isEdit={isEdit} + isDuplicatingManaged={isDuplicatingManaged} getDataViewHelpText={getDataViewHelpText} /> , diff --git a/src/platform/plugins/shared/data_view_editor/public/types.ts b/src/platform/plugins/shared/data_view_editor/public/types.ts index 49550d10922fe..c20e70fdb7d8e 100644 --- a/src/platform/plugins/shared/data_view_editor/public/types.ts +++ b/src/platform/plugins/shared/data_view_editor/public/types.ts @@ -72,13 +72,13 @@ export interface DataViewEditorProps { */ showManagementLink?: boolean; /** - * if set to true a duplicate button is shown + * if editing a managed data view and onDuplicate is defined, a duplicate button is shown */ onDuplicate?: () => void; /** - * if set to true an existing data view is being edited + * if editing a managed data view and onDuplicate is defined, a duplicate button is shown */ - isEdit?: boolean; + isDuplicatingManaged?: boolean; /** * Optional callback to get help text based on the active data view */ @@ -148,6 +148,7 @@ export interface FormInternal extends Omit export interface TimestampOption { display: string; fieldName?: string; + isDisabled?: boolean; } export interface MatchedIndicesSet { diff --git a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx index c92fb21aa2778..e91c25da8c1aa 100644 --- a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx @@ -14,6 +14,7 @@ import { EuiFlexItem, EuiText, EuiTitle, + EuiToolTip, useIsWithinMaxBreakpoint, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -38,6 +39,13 @@ const i18nTexts = { saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { defaultMessage: 'Save', }), + disabledSaveButtonTooltip: i18n.translate( + 'indexPatternFieldEditor.editor.flyoutDisabledSaveButtonTooltip', + { + defaultMessage: + 'Fields cannot be edited on managed data views. Duplicate the data view in order to make changes.', + } + ), }; const defaultModalVisibility = { @@ -267,16 +275,18 @@ const FieldEditorFlyoutContentComponent = ({ - - {i18nTexts.saveButtonLabel} - + + + {i18nTexts.saveButtonLabel} + + diff --git a/src/platform/plugins/shared/data_view_management/public/components/__snapshots__/utils.test.ts.snap b/src/platform/plugins/shared/data_view_management/public/components/__snapshots__/utils.test.ts.snap index 5329da0bd89fb..1c094b8781c67 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/__snapshots__/utils.test.ts.snap +++ b/src/platform/plugins/shared/data_view_management/public/components/__snapshots__/utils.test.ts.snap @@ -6,6 +6,7 @@ Array [ "default": true, "getName": [Function], "id": "test", + "managed": undefined, "name": undefined, "namespaces": undefined, "sort": "0test name", @@ -22,6 +23,7 @@ Array [ "default": false, "getName": [Function], "id": "test1", + "managed": undefined, "name": "Test Name 1", "namespaces": undefined, "sort": "1Test Name 1", diff --git a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 08eb885232329..5e7628b279e15 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -58,7 +58,7 @@ const mappingConflictHeader = i18n.translate( const securityDataView = i18n.translate( 'indexPatternManagement.editIndexPattern.badge.securityDataViewTitle', { - defaultMessage: 'Security Data View', + defaultMessage: 'Security Solution', } ); @@ -210,7 +210,7 @@ export const EditIndexPattern = withRouter( )} {indexPattern.id && indexPattern.id.indexOf(securitySolution) === 0 && ( - {securityDataView} + {securityDataView} )} {tags.map((tag) => ( diff --git a/src/platform/plugins/shared/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/platform/plugins/shared/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 89ebb218dfba6..3600c76089bf9 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/platform/plugins/shared/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -59,7 +59,7 @@ const sorting = { const securityDataView = i18n.translate( 'indexPatternManagement.indexPatternTable.badge.securityDataViewTitle', { - defaultMessage: 'Security Data View', + defaultMessage: 'Security Solution', } ); @@ -286,7 +286,7 @@ export const IndexPatternTable = ({ history, canSave, setShowCreateDialog, title {dataView?.id?.indexOf(securitySolution) === 0 && ( <> -  {securityDataView} +  {securityDataView} )} {dataView?.tags?.map(({ key: tagKey, name: tagName }) => ( diff --git a/src/platform/plugins/shared/data_view_management/public/components/types.ts b/src/platform/plugins/shared/data_view_management/public/components/types.ts index cb6c48bd80481..07b6ec1395ba0 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/types.ts +++ b/src/platform/plugins/shared/data_view_management/public/components/types.ts @@ -23,4 +23,5 @@ export interface IndexPatternTableItem { sort: string; namespaces?: string[]; getName: () => string; + managed?: boolean; } diff --git a/src/platform/plugins/shared/data_view_management/public/components/utils.ts b/src/platform/plugins/shared/data_view_management/public/components/utils.ts index 8291711428bc1..a4dc348df1fed 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/utils.ts +++ b/src/platform/plugins/shared/data_view_management/public/components/utils.ts @@ -24,6 +24,13 @@ const defaultIndexPatternListName = i18n.translate( } ); +const managedDataViewTagName = i18n.translate( + 'indexPatternManagement.editIndexPattern.list.managedDataViewTagName', + { + defaultMessage: 'Managed', + } +); + export const isRollup = (indexPatternType: string = '') => { return indexPatternType === DataViewType.ROLLUP; }; @@ -31,7 +38,7 @@ export const isRollup = (indexPatternType: string = '') => { export async function getIndexPatterns(defaultIndex: string, dataViewsService: DataViewsContract) { const existingIndexPatterns = await dataViewsService.getIdsWithTitle(true); const indexPatternsListItems = existingIndexPatterns.map((idxPattern) => { - const { id, title, namespaces, name } = idxPattern; + const { id, title, namespaces, name, managed } = idxPattern; const isDefault = defaultIndex === id; const tags = getTags(idxPattern, isDefault, dataViewsService.getRollupsEnabled()); const displayName = name ? name : title; @@ -48,6 +55,7 @@ export async function getIndexPatterns(defaultIndex: string, dataViewsService: D // or on bottom of the table sort: `${isDefault ? '0' : '1'}${displayName}`, getName: () => displayName, + managed, }; }); @@ -70,6 +78,13 @@ export const getTags = ( rollupsEnabled: boolean ) => { const tags = []; + if (indexPattern.managed) { + tags.push({ + key: DataViewType.MANAGED, + name: managedDataViewTagName, + 'data-test-subj': 'managed-tag', + }); + } if (isDefault) { tags.push({ key: DataViewType.DEFAULT, diff --git a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view.test.ts.snap b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view.test.ts.snap index 3df45ed00e450..0a5e320862679 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view.test.ts.snap +++ b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view.test.ts.snap @@ -33,6 +33,7 @@ Object { "fieldAttrs": undefined, "fieldFormats": Object {}, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { @@ -57,6 +58,7 @@ Object { "fieldAttrs": Object {}, "fieldFormats": Object {}, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { @@ -707,6 +709,7 @@ Object { }, }, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { diff --git a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view_lazy.test.ts.snap b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view_lazy.test.ts.snap index af25681927c6a..82afa0e3d29c2 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view_lazy.test.ts.snap +++ b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_view_lazy.test.ts.snap @@ -32,6 +32,7 @@ Object { "allowNoIndex": false, "fieldFormats": Object {}, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { @@ -56,6 +57,7 @@ Object { "fieldAttrs": Object {}, "fieldFormats": Object {}, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { @@ -702,6 +704,7 @@ Object { }, }, "id": "test-pattern", + "managed": false, "name": "Name", "runtimeFieldMap": Object { "runtime_field": Object { diff --git a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_views.test.ts.snap b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_views.test.ts.snap index 7ea6cafaaacbc..5489dc9f3951e 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_views.test.ts.snap +++ b/src/platform/plugins/shared/data_views/common/data_views/__snapshots__/data_views.test.ts.snap @@ -67,6 +67,7 @@ Object { }, "fields": Object {}, "id": "id", + "managed": undefined, "name": "Kibana *", "namespaces": undefined, "runtimeFieldMap": Object { diff --git a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts index 4f42aeab87556..8c6d888a7a9a4 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/abstract_data_views.ts @@ -393,7 +393,6 @@ export abstract class AbstractDataView { runtimeFieldMap: stringifyOrUndefined(this.runtimeFieldMap), name: this.name, allowHidden: this.allowHidden, - // managed: this.managed, }; } diff --git a/src/platform/plugins/shared/data_views/common/data_views/data_views.ts b/src/platform/plugins/shared/data_views/common/data_views/data_views.ts index d4fad32b15c43..2dd7f37072b18 100644 --- a/src/platform/plugins/shared/data_views/common/data_views/data_views.ts +++ b/src/platform/plugins/shared/data_views/common/data_views/data_views.ts @@ -972,7 +972,6 @@ export class DataViewsService { spec.fieldFormats = savedObject.attributes.fieldFormatMap ? JSON.parse(savedObject.attributes.fieldFormatMap) : {}; - spec.managed = savedObject.managed; const indexPattern = await this.createFromSpec(spec, true, displayErrors); indexPattern.setEtag(etag); diff --git a/src/platform/plugins/shared/data_views/common/types.ts b/src/platform/plugins/shared/data_views/common/types.ts index 1e1f72754381d..184098adc8848 100644 --- a/src/platform/plugins/shared/data_views/common/types.ts +++ b/src/platform/plugins/shared/data_views/common/types.ts @@ -326,7 +326,6 @@ export interface GetFieldsOptions { includeEmptyFields?: boolean; abortSignal?: AbortSignal; runtimeMappings?: estypes.MappingRuntimeFields; - // managed?: boolean; } /** @@ -386,6 +385,7 @@ export type TypeMeta = { export enum DataViewType { DEFAULT = 'default', ROLLUP = 'rollup', + MANAGED = 'managed', } export type FieldSpecConflictDescriptions = Record; diff --git a/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts b/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts index 7b144e647f1f6..13f1473e5f4a4 100644 --- a/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts +++ b/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts @@ -54,6 +54,7 @@ export const dataViewSpecSchema = schema.object({ name: schema.maybe(schema.string()), namespaces: schema.maybe(schema.arrayOf(schema.string())), allowHidden: schema.maybe(schema.boolean()), + managed: schema.maybe(schema.boolean()), }); export const dataViewsRuntimeResponseSchema = () => diff --git a/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index b24f50e3a5253..96c75be95771a 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -296,10 +296,6 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) }; }, []); - const setDataViewEditorRef = useCallback((ref: () => void | undefined) => { - closeDataViewEditor.current = ref; - }, []); - const { dataViewEditor } = services; const canEditDataView = @@ -309,20 +305,12 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) const createNewDataView = useMemo( () => canEditDataView - ? () => { - const ref = dataViewEditor.openEditor({ - onSave: async (dataView) => { - onDataViewCreated(dataView); - }, - isEdit: true, - }); - if (setDataViewEditorRef) { - setDataViewEditorRef(ref); - } + ? (dataView: DataView) => { + onDataViewCreated(dataView); closeFieldListFlyout?.(); } : undefined, - [canEditDataView, dataViewEditor, setDataViewEditorRef, onDataViewCreated, closeFieldListFlyout] + [canEditDataView, onDataViewCreated, closeFieldListFlyout] ); const searchBarCustomization = useDiscoverCustomization('search_bar'); diff --git a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx index b0efd8d08d5f0..8497fb179bfa6 100644 --- a/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -76,7 +76,6 @@ export const DiscoverTopNav = ({ }, [dataView, isEsqlMode]); const closeFieldEditor = useRef<() => void | undefined>(); - const closeDataViewEditor = useRef<() => void | undefined>(); useEffect(() => { return () => { @@ -84,9 +83,6 @@ export const DiscoverTopNav = ({ if (closeFieldEditor.current) { closeFieldEditor.current(); } - if (closeDataViewEditor.current) { - closeDataViewEditor.current(); - } }; }, []); @@ -119,13 +115,6 @@ export const DiscoverTopNav = ({ [editField, canEditDataView] ); - const createNewDataView = useCallback(() => { - closeDataViewEditor.current = dataViewEditor.openEditor({ - onSave: stateContainer.actions.onDataViewCreated, - allowAdHocDataView: true, - }); - }, [dataViewEditor, stateContainer]); - const updateSavedQueryId = (newSavedQueryId: string | undefined) => { const { appState } = stateContainer; if (newSavedQueryId) { @@ -187,14 +176,14 @@ export const DiscoverTopNav = ({ }, currentDataViewId: dataView?.id, onAddField: addField, - onDataViewCreated: createNewDataView, + onDataViewCreated: stateContainer.actions.onDataViewCreated, onCreateDefaultAdHocDataView: stateContainer.actions.createAndAppendAdHocDataView, onChangeDataView: stateContainer.actions.onChangeDataView, adHocDataViews, savedDataViews, onEditDataView: stateContainer.actions.onDataViewEdited, }; - }, [adHocDataViews, addField, createNewDataView, dataView, savedDataViews, stateContainer]); + }, [adHocDataViews, addField, dataView, savedDataViews, stateContainer]); const onESQLDocsFlyoutVisibilityChanged = useCallback((isOpen: boolean) => { if (isOpen) { diff --git a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx index 95a05c19c8ba5..ce655b7857b65 100644 --- a/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx +++ b/src/platform/plugins/shared/discover/public/application/main/state_management/redux/hooks.tsx @@ -7,7 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { differenceBy } from 'lodash'; import { type TypedUseSelectorHook, type ReactReduxContextValue, @@ -100,12 +99,9 @@ export const useCurrentTabAction = ( export const useCurrentChartPortalNode = () => useCurrentTabContext().currentChartPortalNode; export const useDataViewsForPicker = () => { - const originalAdHocDataViews = useAdHocDataViews(); + const adHocDataViews = useAdHocDataViews(); const savedDataViews = useInternalStateSelector((state) => state.savedDataViews); - return useMemo(() => { - const adHocDataViews = differenceBy(originalAdHocDataViews, savedDataViews, 'id'); - return { savedDataViews, adHocDataViews }; - }, [originalAdHocDataViews, savedDataViews]); + }, [adHocDataViews, savedDataViews]); }; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts index 22cf03cbc83c7..b44493ff65e79 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts @@ -57,6 +57,7 @@ describe('classicNavRootProfileProvider', () => { name: 'All logs', timeFieldName: '@timestamp', title: 'logs-*', + managed: true, }, ]); }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts index cc31cc44bff48..e41dcfd2fa818 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts @@ -70,6 +70,7 @@ describe('observabilityRootProfileProvider', () => { name: 'All logs', timeFieldName: '@timestamp', title: 'logs-*', + managed: true, }, ]); }); diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx index f68695926f517..a00943488bc01 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -127,16 +127,13 @@ describe('DataView component', () => { }); it('should render the add dataview menu if onDataViewCreated is given', async () => { - const addDataViewSpy = jest.fn(); const component = mount( - wrapDataViewComponentInContext({ ...props, onDataViewCreated: addDataViewSpy }, false) + wrapDataViewComponentInContext({ ...props, onDataViewCreated: jest.fn() }, false) ); findTestSubject(component, 'dataview-trigger').simulate('click'); expect(component.find('[data-test-subj="dataview-create-new"]').at(0).text()).toContain( 'Create a data view' ); - component.find('[data-test-subj="dataview-create-new"]').first().simulate('click'); - expect(addDataViewSpy).toHaveBeenCalled(); }); it('should properly handle ad hoc data views', async () => { @@ -168,6 +165,7 @@ describe('DataView component', () => { name: 'the-data-view', type: 'default', isAdhoc: true, + managed: undefined, }, ]); }); @@ -201,6 +199,7 @@ describe('DataView component', () => { name: 'the-data-view-esql', type: 'esql', isAdhoc: true, + managed: undefined, }, ]); }); @@ -240,7 +239,6 @@ describe('DataView component', () => { name: 'the-data-view', type: 'default', managed: true, - isManaged: true, }, ]); }); diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx index a9223b8cefb4c..1f84380b54059 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx @@ -26,7 +26,8 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/public'; +import { DataView } from '@kbn/data-views-plugin/public'; +import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common'; import type { IUnifiedSearchPluginServices } from '../types'; import { type DataViewPickerProps } from './data_view_picker'; import type { DataViewListItemEnhanced } from './dataview_list'; @@ -34,28 +35,15 @@ import adhoc from './assets/adhoc.svg'; import { changeDataViewStyles } from './change_dataview.styles'; import { DataViewSelector } from './data_view_selector'; -const mapDataViewListItem = ( - dataView: DataView, - partial: Partial -): DataViewListItemEnhanced => ({ - title: dataView.title, - name: dataView.name, - id: dataView.id!, - type: dataView.type, - ...partial, +const mapAdHocDataView = (adHocDataView: DataView): DataViewListItemEnhanced => ({ + title: adHocDataView.title, + name: adHocDataView.name, + id: adHocDataView.id!, + type: adHocDataView.type, + isAdhoc: true, + managed: adHocDataView.managed, }); -const mapAdHocDataView = (adHocDataView: DataView) => { - if (adHocDataView.managed) { - return mapDataViewListItem(adHocDataView, { isManaged: true }); - } - return mapDataViewListItem(adHocDataView, { isAdhoc: true }); -}; - -const mapManagedDataView = (managedDataView: DataViewListItem) => { - return { ...managedDataView, isManaged: true }; -}; - const shrinkableContainerCss = css` min-width: 0; `; @@ -104,14 +92,13 @@ export function ChangeDataView({ useEffect(() => { const fetchDataViews = async () => { - const availableDataViewRefs = savedDataViews + const savedDataViewRefs = savedDataViews ? savedDataViews : (await data.dataViews.getIdsWithTitle()) ?? []; - const savedDataViewRefs = availableDataViewRefs.filter((dataView) => !dataView.managed); - const managedDataViewRefs = savedDataViews?.filter((dataView) => dataView.managed === true).map(mapManagedDataView) ?? []; const adHocDataViewRefs = adHocDataViews?.map(mapAdHocDataView) ?? []; - setDataViewsList([...savedDataViewRefs, ...adHocDataViewRefs, ...managedDataViewRefs]); + setDataViewsList([...savedDataViewRefs, ...adHocDataViewRefs]); }; + fetchDataViews(); }, [data, currentDataViewId, adHocDataViews, savedDataViews]); @@ -153,17 +140,64 @@ export function ChangeDataView({ ); }; const onDuplicate = useCallback(async () => { - const dataView = await dataViews.get(currentDataViewId!); + if (!currentDataViewId || !onDataViewCreated) { + return; + } + const dataView = await dataViews.get(currentDataViewId); + const editData = new DataView({ + spec: { title: dataView.getIndexPattern() }, + fieldFormats: {} as FieldFormatsStartCommon, + }); + + dataViewEditor.openEditor({ + editData, + onSave: (newDataView) => { + onDataViewCreated(newDataView); + }, + allowAdHocDataView: true, + isDuplicatingManaged: true, + }); + }, [currentDataViewId, dataViews, dataViewEditor, onDataViewCreated]); + + const onEdit = useCallback(async () => { if (onEditDataView) { + const dataView = await dataViews.get(currentDataViewId!); dataViewEditor.openEditor({ editData: dataView, onSave: (updatedDataView) => { onEditDataView(updatedDataView); }, + onDuplicate, + getDataViewHelpText, + }); + } else { + application.navigateToApp('management', { + path: `/kibana/indexPatterns/patterns/${currentDataViewId}`, + }); + } + closePopover(); + }, [ + currentDataViewId, + dataViews, + onEditDataView, + dataViewEditor, + application, + closePopover, + onDuplicate, + getDataViewHelpText, + ]); + + const onCreate = useCallback(() => { + if (onDataViewCreated) { + dataViewEditor.openEditor({ + onSave: (newDataView) => { + onDataViewCreated(newDataView); + }, allowAdHocDataView: true, }); } - }, [currentDataViewId, dataViews, onEditDataView, dataViewEditor]); + closePopover(); + }, [onDataViewCreated, dataViewEditor, closePopover]); const items = useMemo(() => { const panelItems: EuiContextMenuPanelProps['items'] = []; @@ -187,25 +221,7 @@ export function ChangeDataView({ key="manage" icon="indexSettings" data-test-subj="indexPattern-manage-field" - onClick={async () => { - if (onEditDataView) { - const dataView = await dataViews.get(currentDataViewId!); - dataViewEditor.openEditor({ - editData: dataView, - onSave: (updatedDataView) => { - onEditDataView(updatedDataView); - }, - onDuplicate, - isEdit: true, - getDataViewHelpText, - }); - } else { - application.navigateToApp('management', { - path: `/kibana/indexPatterns/patterns/${currentDataViewId}`, - }); - } - closePopover(); - }} + onClick={onEdit} > {i18n.translate('unifiedSearch.query.queryBar.indexPattern.manageFieldButton', { defaultMessage: 'Manage this data view', @@ -245,10 +261,7 @@ export function ChangeDataView({ { - closePopover(); - onDataViewCreated(); - }} + onClick={onCreate} size="xs" iconType="plusInCircleFilled" iconSide="left" @@ -278,21 +291,20 @@ export function ChangeDataView({ return panelItems; }, [ - application, closePopover, currentDataViewId, dataViewEditor, - dataViews, dataViewsList, euiTheme.size.s, onAddField, onChangeDataView, onCreateDefaultAdHocDataView, + onCreate, onDataViewCreated, + onEdit, onEditDataView, searchListInputId, selectableProps, - getDataViewHelpText, ]); return ( diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx index 8abc62cb4b321..fca87dd69cf0d 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx @@ -60,9 +60,10 @@ export interface DataViewPickerProps { onAddField?: () => void; /** * Callback that is called when the user clicks the create dataview option. + * The first parameter is the created data view * Also works as a flag to show the create dataview button. */ - onDataViewCreated?: () => void; + onDataViewCreated?: (createdDataView: DataView) => void; onCreateDefaultAdHocDataView?: (dataViewSpec: DataViewSpec) => void; /** diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/dataview_list.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/dataview_list.tsx index 629823b0b7cb3..6fa39ed431699 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/dataview_list.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/dataview_list.tsx @@ -68,7 +68,6 @@ const strings = { export interface DataViewListItemEnhanced extends DataViewListItem { isAdhoc?: boolean; - isManaged?: boolean; } export interface DataViewsListProps { @@ -137,19 +136,19 @@ export function DataViewsList({ data-test-subj="indexPattern-switcher" searchable singleSelection="always" - options={sortedDataViewsList?.map(({ title, id, name, isAdhoc, isManaged }) => ({ + options={sortedDataViewsList?.map(({ title, id, name, isAdhoc, managed }) => ({ key: id, label: name ? name : title, value: id, checked: id === currentDataViewId ? 'on' : undefined, - append: isAdhoc ? ( - - {strings.editorAndPopover.adhoc.getTemporaryDataviewLabel()} - - ) : isManaged ? ( + append: managed ? ( {strings.editorAndPopover.managed.getManagedDataviewLabel()} + ) : isAdhoc ? ( + + {strings.editorAndPopover.adhoc.getTemporaryDataviewLabel()} + ) : null, }))} onChange={(choices) => { diff --git a/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts b/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts index 45c7d6d3a3e70..981469cf2ac02 100644 --- a/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts +++ b/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts @@ -36,7 +36,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataViews.getSelectedName()).not.to.be('Example profile data view'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -51,7 +52,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); expect(await toasts.getCount({ timeout: 2000 })).to.be(0); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); }); it('should create a copy of the profile data view when saving the Discover session', async () => { @@ -70,7 +72,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).to.be('2024-06-10T14:00:00.000Z'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -99,7 +102,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() diff --git a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx index 6ed5d3f484088..f2c871189ffb7 100644 --- a/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/platform/plugins/shared/lens/public/app_plugin/lens_top_nav.tsx @@ -394,7 +394,6 @@ export const LensTopNavMenu = ({ const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView()) || !currentIndexPattern?.isPersisted(); const closeFieldEditor = useRef<() => void | undefined>(); - const closeDataViewEditor = useRef<() => void | undefined>(); const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); @@ -491,7 +490,6 @@ export const LensTopNavMenu = ({ return () => { // Make sure to close the editors when unmounting closeFieldEditor.current?.(); - closeDataViewEditor.current?.(); }; }, []); @@ -1107,32 +1105,24 @@ export const LensTopNavMenu = ({ [editField, canEditDataView] ); - const createNewDataView = useCallback(() => { - closeDataViewEditor.current = dataViewEditor.openEditor({ - onSave: async (dataView) => { - if (dataView.id) { - if (isOnTextBasedMode) { - dispatch( - switchAndCleanDatasource({ - newDatasourceId: 'formBased', - visualizationId: visualization?.activeId, - currentIndexPatternId: dataView?.id, - }) - ); - } - dispatchChangeIndexPattern(dataView); - setCurrentIndexPattern(dataView); + const createNewDataView = useCallback( + async (dataView: DataView) => { + if (dataView.id) { + if (isOnTextBasedMode) { + dispatch( + switchAndCleanDatasource({ + newDatasourceId: 'formBased', + visualizationId: visualization?.activeId, + currentIndexPatternId: dataView?.id, + }) + ); } - }, - allowAdHocDataView: true, - }); - }, [ - dataViewEditor, - dispatch, - dispatchChangeIndexPattern, - isOnTextBasedMode, - visualization?.activeId, - ]); + dispatchChangeIndexPattern(dataView); + setCurrentIndexPattern(dataView); + } + }, + [dispatch, dispatchChangeIndexPattern, isOnTextBasedMode, visualization?.activeId] + ); const onCreateDefaultAdHocDataView = useCallback( async (dataViewSpec: DataViewSpec) => { diff --git a/x-pack/platform/plugins/shared/logs_shared/server/services/log_views/log_views_client.test.ts b/x-pack/platform/plugins/shared/logs_shared/server/services/log_views/log_views_client.test.ts index 206bb0d9f416f..f31a7d293e5e5 100644 --- a/x-pack/platform/plugins/shared/logs_shared/server/services/log_views/log_views_client.test.ts +++ b/x-pack/platform/plugins/shared/logs_shared/server/services/log_views/log_views_client.test.ts @@ -285,6 +285,7 @@ describe('LogViewsClient class', () => { "getTimeField": [Function], "id": "LOG_DATA_VIEW", "isTimeBased": [Function], + "managed": false, "matchedIndices": Array [], "metaFields": Array [ "_id", diff --git a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts index 43eb7d82a27ea..1d9bf8ce461a0 100644 --- a/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts +++ b/x-pack/platform/plugins/shared/stack_alerts/server/rule_types/es_query/lib/fetch_search_source_query.test.ts @@ -839,6 +839,7 @@ describe('fetchSearchSourceQuery', () => { title: 'title', type: 'index-pattern', version: undefined, + managed: false, }, true // skipFetchFields flag ); diff --git a/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts b/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts index c77b58b8e5cf8..d8ea9e5b20e63 100644 --- a/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts +++ b/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts @@ -48,6 +48,7 @@ export default function ({ getService }: FtrProviderContext) { index_pattern: { title, }, + managed: false, }); const id = response1.body.index_pattern.id; const response2 = await supertestWithoutAuth diff --git a/x-pack/platform/test/serverless/functional/test_suites/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts b/x-pack/platform/test/serverless/functional/test_suites/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts index 8732101199528..8f079d01367e6 100644 --- a/x-pack/platform/test/serverless/functional/test_suites/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts +++ b/x-pack/platform/test/serverless/functional/test_suites/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts @@ -39,7 +39,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataViews.getSelectedName()).not.to.be('Example profile data view'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -54,7 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); expect(await toasts.getCount({ timeout: 2000 })).to.be(0); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); }); it('should create a copy of the profile data view when saving the Discover session', async () => { @@ -73,7 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).to.be('Jun 10, 2024 @ 14:00:00.000'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -105,7 +108,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(true); + expect(await dataViews.isManaged()).to.be(false); + expect(await dataViews.isAdHoc()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() diff --git a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.test.ts b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.test.ts index 5c9190dc12945..cb9b4a0f0ba32 100644 --- a/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.test.ts +++ b/x-pack/solutions/observability/plugins/observability_logs_explorer/public/logs_explorer_url_schema.test.ts @@ -129,6 +129,7 @@ describe('logs_explorer_url_schema', () => { name: 'All logs', title: 'logs-*,dataset-logs-*-*', timeFieldName: '@timestamp', + managed: true, }); }); }); diff --git a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/indicator_section/custom_common/index_selection.tsx b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/indicator_section/custom_common/index_selection.tsx index d15a716a92c6f..14c7fc6984f3b 100644 --- a/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/indicator_section/custom_common/index_selection.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/pages/slo_edit/components/indicator_section/custom_common/index_selection.tsx @@ -27,11 +27,8 @@ const SETTINGS_SYNC_FIELD = 'settings.syncField'; export function IndexSelection({ selectedDataView }: { selectedDataView?: DataView }) { const { control, getFieldState, setValue, watch } = useFormContext(); - const { - dataViews: dataViewsService, - dataViewFieldEditor, - dataViewEditor, - } = useKibana().services; + const { dataViews: dataViewsService, dataViewFieldEditor } = + useKibana().services; const currentIndexPattern = watch(INDEX_FIELD); const currentDataViewId = watch(DATA_VIEW_FIELD); @@ -122,20 +119,15 @@ export function IndexSelection({ selectedDataView }: { selectedDataView?: DataVi adHocDataViews, }) } - onDataViewCreated={() => { - dataViewEditor.openEditor({ - allowAdHocDataView: true, - onSave: (dataView: DataView) => { - if (!dataView.isPersisted()) { - setAdHocDataViews((prev) => [...prev, dataView]); - } else { - refetchDataViewsList(); - } + onDataViewCreated={(dataView: DataView) => { + if (!dataView.isPersisted()) { + setAdHocDataViews((prev) => [...prev, dataView]); + } else { + refetchDataViewsList(); + } - field.onChange(dataView.id); - updateDataViewDependantFields(dataView.getIndexPattern(), dataView.timeFieldName); - }, - }); + field.onChange(dataView.id); + updateDataViewDependantFields(dataView.getIndexPattern(), dataView.timeFieldName); }} /> )} diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx index 2465e37a88704..3fbaec265927d 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.test.tsx @@ -8,11 +8,9 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { DataViewPicker } from '.'; -import { sharedDataViewManagerSlice } from '../../redux/slices'; import { useDispatch } from 'react-redux'; import { useKibana } from '../../../common/lib/kibana'; -import { DataView } from '@kbn/data-views-plugin/common'; -import { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/common'; import { TestProviders } from '../../../common/mock/test_providers'; import { useSelectDataView } from '../../hooks/use_select_data_view'; import { useUpdateUrlParam } from '../../../common/utils/global_query_string'; @@ -50,7 +48,11 @@ jest.mock('@kbn/unified-search-plugin/public', () => ({ > {'Change Data View'} - {props.onAddField && ( @@ -84,7 +86,6 @@ describe('DataViewPicker', () => { ...mockUseKibana().services, dataViewFieldEditor: { openEditor: jest.fn() }, dataViewEditor: { - openEditor: jest.fn(), userPermissions: { editDataView: jest.fn().mockReturnValue(true) }, }, }, @@ -138,37 +139,6 @@ describe('DataViewPicker', () => { }); }); - it('opens data view editor when creating a new data view', async () => { - render( - - - - ); - - fireEvent.click(screen.getByTestId('createDataView')); - - expect(jest.mocked(useKibana().services.dataViewEditor.openEditor)).toHaveBeenCalled(); - - // Test the onSave callback - const onSaveCallback = jest.mocked(useKibana().services.dataViewEditor.openEditor).mock - .calls[0][0].onSave; - - const newDataView = new DataView({ - spec: { id: 'new-data-view-id', name: 'New Data View' }, - fieldFormats: new FieldFormatsRegistry(), - }); - - onSaveCallback(newDataView); - - expect(mockDispatch).toHaveBeenCalledWith( - sharedDataViewManagerSlice.actions.addDataView(newDataView) - ); - expect(jest.mocked(useSelectDataView())).toHaveBeenCalledWith({ - id: 'new-data-view-id', - scope: 'default', - }); - }); - it('opens field editor when adding a field', async () => { const mockFieldEditorClose = jest.fn(); jest diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index 812fddb9deb8b..063171e266f93 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -11,7 +11,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { DataView } from '@kbn/data-views-plugin/public'; import { EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EXPLORE_DATA_VIEW_PREFIX } from '../../../../common/constants'; import type { SourcererUrlState } from '../../../sourcerer/store/model'; import { useUpdateUrlParam } from '../../../common/utils/global_query_string'; import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state'; @@ -54,7 +53,6 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie [dataViewEditor] ); - const closeDataViewEditor = useRef<() => void | undefined>(); const closeFieldEditor = useRef<() => void | undefined>(); const { dataView, status } = useDataView(scope); @@ -62,9 +60,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie const { adhocDataViews: adhocDataViewSpecs, defaultDataViewId } = useSelector(sharedStateSelector); const adhocDataViews = useMemo(() => { - return adhocDataViewSpecs - .filter((spec) => !spec.id?.startsWith(EXPLORE_DATA_VIEW_PREFIX)) - .map((spec) => new DataView({ spec, fieldFormats })); + return adhocDataViewSpecs.map((spec) => new DataView({ spec, fieldFormats })); }, [adhocDataViewSpecs, fieldFormats]); const savedDataViews = useSavedDataViews(); @@ -94,19 +90,16 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie [isDefaultSourcerer, scope, selectDataView, updateUrlParam] ); - const createNewDataView = useCallback(() => { - closeDataViewEditor.current = dataViewEditor.openEditor({ - onSave: async (newDataView: DataView) => { - if (!newDataView.id) { - return; - } + const handleCreateNewDataView = useMemo(() => { + return (newDataView: DataView) => { + if (!newDataView.id) { + return; + } - dispatch(sharedDataViewManagerSlice.actions.addDataView(newDataView)); - handleChangeDataView(newDataView.id, newDataView.getIndexPattern()); - }, - allowAdHocDataView: true, - }); - }, [dataViewEditor, dispatch, handleChangeDataView]); + dispatch(sharedDataViewManagerSlice.actions.addDataView(newDataView)); + handleChangeDataView(newDataView.id, newDataView.getIndexPattern()); + }; + }, [dispatch, handleChangeDataView]); const editField = useCallback( async (fieldName?: string, _uiAction: 'edit' | 'add' = 'edit') => { @@ -189,7 +182,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie onChangeDataView={handleChangeDataView} onEditDataView={handleDataViewModified} onAddField={handleAddField} - onDataViewCreated={createNewDataView} + onDataViewCreated={handleCreateNewDataView} adHocDataViews={adhocDataViews} savedDataViews={savedDataViews} onClosePopover={onClosePopover} diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts index f1c2f7fd18522..da0de5d181ef8 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/hooks/use_saved_data_views.test.ts @@ -27,7 +27,6 @@ describe('useSavedDataViews', () => { id: DEFAULT_SECURITY_SOLUTION_DATA_VIEW_ID, // This should not be filtered out title: 'Default View', name: 'default_view', - managed: true, }, { id: DEFAULT_ALERT_DATA_VIEW_ID, // This should be filtered out @@ -55,7 +54,6 @@ describe('useSavedDataViews', () => { // Render the hook const { result } = renderHook(() => useSavedDataViews()); - // Expect the default view to be filtered out expect(result.current).toHaveLength(4); // Expect the custom views to be correctly transformed diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts index 63ab3be87a0d2..45c7ceb42ce42 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts @@ -59,7 +59,13 @@ export const sharedDataViewManagerSlice = createSlice({ state.dataViews.push(dataViewSpec); } else { - if (state.adhocDataViews.find((dv) => dv.title === dataViewSpec.title)) { + if ( + // NOTE: user is allowed to duplicate a managed data view and + // we want both to show up in the list + state.adhocDataViews.find( + (dv) => dv.title === dataViewSpec.title && dv.name === dataViewSpec.managed + ) + ) { return; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts index 29d0635672fd2..c369409cfd8ff 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/sourcerer/containers/create_sourcerer_data_view.ts @@ -63,7 +63,6 @@ export const createSourcererDataView = async ({ title: patternListAsTitle, timeFieldName: DEFAULT_TIME_FIELD, name: DEFAULT_SECURITY_DATA_VIEW, - managed: true, }, // Override property - if a data view exists with the security solution pattern // delete it and replace it with our data view diff --git a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index a8e849dc5da8b..1e95a9fd236ec 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -206,7 +206,7 @@ export const useFieldBrowserOptions: UseFieldBrowserOptions = ({ ); const createFieldButton = useCreateFieldButton({ - isAllowed: hasFieldEditPermission && !!selectedDataViewId, + isAllowed: hasFieldEditPermission && !!selectedDataViewId && !dataView?.managed, loading: !dataView, openFieldEditor, }); From ee47155ef13a0e858c6fd3cbd226c46a903f8380 Mon Sep 17 00:00:00 2001 From: christineweng Date: Tue, 19 Aug 2025 15:13:37 -0500 Subject: [PATCH 03/12] comments --- .../public/components/data_view_editor_flyout_content.tsx | 7 ++++--- .../components/data_view_flyout_content_container.tsx | 4 ++-- .../data_view_editor/public/components/footer/footer.tsx | 1 - .../public/dataview_picker/change_dataview.tsx | 4 ++-- .../components/data_view_picker/index.tsx | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx index e8baaa1b0a30f..8b1f1aa4976c4 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -105,10 +105,11 @@ const IndexPatternEditorFlyoutContentComponent = ({ } = useKibana(); const canSave = dataViews.getCanSaveSync(); + const isManaged = !!editData?.managed; // Edit form is populated and disabled if the current data view is managed // and the data view is not being duplicated - const isFormDisabled = !!editData?.managed && !isDuplicatingManaged; - const isEditingExisting = editData && !editData.managed && !isDuplicatingManaged; + const isFormDisabled = isManaged && !isDuplicatingManaged; + const isEditingExisting = editData && !isManaged && !isDuplicatingManaged; const { form } = useForm({ // Prefill with data if editData exists @@ -306,7 +307,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ isPersisted={Boolean(editData && editData.isPersisted())} allowAdHoc={allowAdHoc} canSave={canSave} - isManaged={!!editData?.managed} + isManaged={isManaged} onDuplicate={onDuplicate} isDuplicatingManaged={isDuplicatingManaged} /> diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx index db1943dd7c6a8..9659394bc812b 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -26,7 +26,7 @@ const DataViewFlyoutContentContainer = ({ allowAdHocDataView, showManagementLink, onDuplicate, - isDuplicatingManaged, + isDuplicatingManaged = false, getDataViewHelpText, }: DataViewEditorProps) => { const { @@ -104,7 +104,7 @@ const DataViewFlyoutContentContainer = ({ allowAdHoc={allowAdHocDataView || false} dataViewEditorService={dataViewEditorService} onDuplicate={onDuplicate} - isDuplicatingManaged={isDuplicatingManaged || false} + isDuplicatingManaged={isDuplicatingManaged} getDataViewHelpText={getDataViewHelpText} /> ); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx index 5b1de19497cfd..46930a85f6ffd 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx @@ -146,7 +146,6 @@ export const Footer = ({ )} - {showDuplicateButton && ( diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx index 1f84380b54059..af732865bd751 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx @@ -160,8 +160,8 @@ export function ChangeDataView({ }, [currentDataViewId, dataViews, dataViewEditor, onDataViewCreated]); const onEdit = useCallback(async () => { - if (onEditDataView) { - const dataView = await dataViews.get(currentDataViewId!); + if (onEditDataView && currentDataViewId) { + const dataView = await dataViews.get(currentDataViewId); dataViewEditor.openEditor({ editData: dataView, onSave: (updatedDataView) => { diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx index 063171e266f93..deed67577561b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/components/data_view_picker/index.tsx @@ -134,7 +134,7 @@ export const DataViewPicker = memo(({ scope, onClosePopover, disabled }: DataVie dv.id === defaultDataViewId ? ( {'securitySolution:defaultIndex'}, }} From 1776b008e9c38c68bd266b97125bb5705e90e1cf Mon Sep 17 00:00:00 2001 From: christineweng Date: Wed, 27 Aug 2025 17:03:58 -0500 Subject: [PATCH 04/12] comments and make duplicate available to all data views --- .../logs/utils/get_all_logs_data_view_spec.ts | 1 - .../advanced_params_content.tsx | 2 +- .../public/components/data_view_editor.tsx | 4 +- .../data_view_editor_flyout_content.tsx | 48 ++++++++++++------- .../data_view_flyout_content_container.tsx | 6 +-- .../public/components/footer/footer.tsx | 8 ++-- .../data_view_editor/public/open_editor.tsx | 4 +- .../shared/data_view_editor/public/types.ts | 6 +-- .../components/field_editor/field_detail.tsx | 7 ++- .../components/field_editor/field_editor.tsx | 10 ++-- .../components/field_editor/form_row.tsx | 3 ++ .../field_editor_flyout_content.tsx | 38 +++++++++------ .../public/components/utils.ts | 2 +- .../plugins/shared/data_views/common/types.ts | 1 - .../public/create_data_view.ts | 1 - .../server/rest_api_routes/schema.ts | 1 - .../use_default_ad_hoc_data_views.test.tsx | 4 +- .../hooks/use_default_ad_hoc_data_views.ts | 2 +- .../classic_nav_root_profile/profile.test.ts | 1 - .../profile.test.ts | 1 - .../dataview_picker/change_dataview.test.tsx | 8 +--- .../dataview_picker/change_dataview.tsx | 12 ++--- .../dataview_picker/data_view_picker.tsx | 2 +- .../_get_default_ad_hoc_data_views.ts | 12 ++--- .../data_views/integration/integration.ts | 1 - .../public/data_view_manager/redux/slices.ts | 2 +- 26 files changed, 101 insertions(+), 86 deletions(-) diff --git a/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts b/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts index eb36e1a05fbe4..41a72405698c6 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/data_types/logs/utils/get_all_logs_data_view_spec.ts @@ -22,5 +22,4 @@ export const getAllLogsDataViewSpec = ({ }), title: allLogsIndexPattern, timeFieldName: '@timestamp', - managed: true, }); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx b/src/platform/plugins/shared/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx index a1aafb4f4b63b..3151005389295 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/advanced_params_content/advanced_params_content.tsx @@ -50,7 +50,7 @@ export const AdvancedParamsContent = ({ componentProps={{ euiFieldProps: { 'aria-label': allowHiddenAriaLabel, - disabled: disableAllowHidden, + disabled: disableAllowHidden || disableId, }, }} onChange={onAllowHiddenChange} diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx index 79daa0c3b5181..43a3b72efb517 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor.tsx @@ -26,7 +26,7 @@ export const DataViewEditor = ({ editData, allowAdHocDataView, onDuplicate, - isDuplicatingManaged, + isDuplicating, getDataViewHelpText, }: DataViewEditorPropsWithServices) => { const { Provider: KibanaReactContextProvider } = @@ -48,7 +48,7 @@ export const DataViewEditor = ({ editData={editData} allowAdHocDataView={allowAdHocDataView} onDuplicate={onDuplicate} - isDuplicatingManaged={isDuplicatingManaged} + isDuplicating={isDuplicating} getDataViewHelpText={getDataViewHelpText} /> diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx index 8b1f1aa4976c4..0e1dc2e2e3382 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_editor_flyout_content.tsx @@ -18,6 +18,7 @@ import { EuiLink, EuiSkeletonRectangle, EuiSkeletonTitle, + EuiCallOut, type UseEuiTheme, useIsWithinBreakpoints, } from '@elastic/eui'; @@ -72,7 +73,7 @@ export interface Props { showManagementLink?: boolean; allowAdHoc: boolean; dataViewEditorService: DataViewEditorService; - isDuplicatingManaged: boolean; + isDuplicating: boolean; onDuplicate?: () => void; getDataViewHelpText?: (dataView: DataView) => ReactNode | string | undefined; } @@ -95,7 +96,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ getDataViewHelpText, dataViewEditorService, onDuplicate, - isDuplicatingManaged, + isDuplicating, }: Props) => { const styles = useMemoCss(componentStyles); const isMobile = useIsWithinBreakpoints(['s', 'xs']); @@ -106,10 +107,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ const canSave = dataViews.getCanSaveSync(); const isManaged = !!editData?.managed; - // Edit form is populated and disabled if the current data view is managed - // and the data view is not being duplicated - const isFormDisabled = isManaged && !isDuplicatingManaged; - const isEditingExisting = editData && !isManaged && !isDuplicatingManaged; + // onDuplicate is only provided when editing an existing data view + // if onDuplicate is undefined we are duplicating a new data view + const isEditingExisting = editData && !isDuplicating; const { form } = useForm({ // Prefill with data if editData exists @@ -122,8 +122,8 @@ const IndexPatternEditorFlyoutContentComponent = ({ ...(editData ? { title: editData.getIndexPattern(), - id: isDuplicatingManaged ? undefined : editData.id, - name: isDuplicatingManaged ? undefined : editData.name, + id: isDuplicating ? undefined : editData.id, + name: isDuplicating ? undefined : editData.name, allowHidden: editData.getAllowHidden(), ...(editData.timeFieldName === noTimeFieldValue ? { timestampField: { label: noTimeFieldLabel, value: noTimeFieldValue } } @@ -193,13 +193,13 @@ const IndexPatternEditorFlyoutContentComponent = ({ const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {}); const namesNotAllowed = useMemo(() => { - // When duplicating a managed data view, add the existing name + // When duplicating a data view, add the existing name // to the not allowed names list - if (isDuplicatingManaged && editData) { + if (isDuplicating && editData) { return [editData.name, ...(existingDataViewNames || [])]; } return existingDataViewNames || []; - }, [existingDataViewNames, isDuplicatingManaged, editData]); + }, [existingDataViewNames, isDuplicating, editData]); useDebounce( () => { @@ -309,7 +309,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ canSave={canSave} isManaged={isManaged} onDuplicate={onDuplicate} - isDuplicatingManaged={isDuplicatingManaged} + isDuplicating={isDuplicating} /> ); @@ -319,10 +319,10 @@ const IndexPatternEditorFlyoutContentComponent = ({

- {editData && !isDuplicatingManaged ? editorTitleEditMode : editorTitle} + {isEditingExisting ? editorTitleEditMode : editorTitle}

- {showManagementLink && !isDuplicatingManaged && editData && editData.id && ( + {showManagementLink && isEditingExisting && editData && editData.id && ( )} + {isManaged && ( + + )}
- + @@ -360,7 +372,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ dataViewEditorService.indexPatternValidationProvider } titleHelpText={titleHelpText} - disabled={isFormDisabled} + disabled={isManaged} /> @@ -371,13 +383,13 @@ const IndexPatternEditorFlyoutContentComponent = ({ options$={dataViewEditorService.timestampFieldOptions$} isLoadingOptions$={dataViewEditorService.loadingTimestampFields$} matchedIndices$={dataViewEditorService.matchedIndices$} - disabled={isFormDisabled} + disabled={isManaged} /> { form.getFields().title.validate(); }} diff --git a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx index 9659394bc812b..8f49ac4cb14f1 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/data_view_flyout_content_container.tsx @@ -26,7 +26,7 @@ const DataViewFlyoutContentContainer = ({ allowAdHocDataView, showManagementLink, onDuplicate, - isDuplicatingManaged = false, + isDuplicating = false, getDataViewHelpText, }: DataViewEditorProps) => { const { @@ -54,7 +54,7 @@ const DataViewFlyoutContentContainer = ({ const onSaveClick = async (dataViewSpec: DataViewSpec, persist: boolean = true) => { try { let saveResponse; - if (editData && !editData.managed && !isDuplicatingManaged) { + if (editData && !editData.managed && !isDuplicating) { const { name = '', timeFieldName, title = '', allowHidden = false } = dataViewSpec; editData.setIndexPattern(title); editData.name = name; @@ -104,7 +104,7 @@ const DataViewFlyoutContentContainer = ({ allowAdHoc={allowAdHocDataView || false} dataViewEditorService={dataViewEditorService} onDuplicate={onDuplicate} - isDuplicatingManaged={isDuplicatingManaged} + isDuplicating={isDuplicating} getDataViewHelpText={getDataViewHelpText} /> ); diff --git a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx index 46930a85f6ffd..5d633f4c2af66 100644 --- a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.tsx @@ -34,7 +34,7 @@ interface FooterProps { allowAdHoc: boolean; canSave: boolean; isManaged: boolean; - isDuplicatingManaged: boolean; + isDuplicating: boolean; } const closeButtonLabel = i18n.translate('indexPatternEditor.editor.flyoutCloseButtonLabel', { @@ -78,12 +78,12 @@ export const Footer = ({ canSave, onDuplicate, isManaged, - isDuplicatingManaged, + isDuplicating, }: FooterProps) => { const isEditingAdHoc = hasEditData && !isPersisted; const isEditing = (canSave || isEditingAdHoc) && !isManaged; - const showDuplicateButton = (canSave || isEditingAdHoc) && isManaged && onDuplicate; + const showDuplicateButton = (canSave || isEditingAdHoc) && onDuplicate; const submitPersisted = () => { onSubmit(false); @@ -138,7 +138,7 @@ export const Footer = ({ (submittingType === SubmittingType.savingAsAdHoc && isEditingAdHoc) } > - {hasEditData && !isDuplicatingManaged + {hasEditData && !isDuplicating ? isPersisted ? editButtonLabel : editUnpersistedButtonLabel diff --git a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx index 5f82559aaaf14..f1e7189f6babe 100644 --- a/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx +++ b/src/platform/plugins/shared/data_view_editor/public/open_editor.tsx @@ -51,7 +51,7 @@ export const getEditorOpener = allowAdHocDataView = false, editData, onDuplicate, - isDuplicatingManaged, + isDuplicating, getDataViewHelpText, }: DataViewEditorProps): CloseEditor => { const closeEditor = () => { @@ -84,7 +84,7 @@ export const getEditorOpener = allowAdHocDataView={allowAdHocDataView} showManagementLink={Boolean(editData && editData.isPersisted())} onDuplicate={onDuplicate} - isDuplicatingManaged={isDuplicatingManaged} + isDuplicating={isDuplicating} getDataViewHelpText={getDataViewHelpText} /> , diff --git a/src/platform/plugins/shared/data_view_editor/public/types.ts b/src/platform/plugins/shared/data_view_editor/public/types.ts index c20e70fdb7d8e..af96f240393e6 100644 --- a/src/platform/plugins/shared/data_view_editor/public/types.ts +++ b/src/platform/plugins/shared/data_view_editor/public/types.ts @@ -72,13 +72,13 @@ export interface DataViewEditorProps { */ showManagementLink?: boolean; /** - * if editing a managed data view and onDuplicate is defined, a duplicate button is shown + * If editing a data view and onDuplicate is defined, a duplicate button is shown */ onDuplicate?: () => void; /** - * if editing a managed data view and onDuplicate is defined, a duplicate button is shown + * Optional boolean to indicate if the data view is being duplicated */ - isDuplicatingManaged?: boolean; + isDuplicating?: boolean; /** * Optional callback to get help text based on the active data view */ diff --git a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/field_detail.tsx b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/field_detail.tsx index b0d78067d59f8..57b0e7e7c3cf1 100644 --- a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/field_detail.tsx +++ b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/field_detail.tsx @@ -79,7 +79,7 @@ const geti18nTexts = (): { }, }); -export const FieldDetail = ({}) => { +export const FieldDetail = ({ isDisabled }: { isDisabled?: boolean }) => { const { links, fieldTypeToProcess } = useFieldEditorContext(); const i18nTexts = geti18nTexts(); return ( @@ -88,6 +88,7 @@ export const FieldDetail = ({}) => { { { { { void; /** Handler to receive update on the form "isModified" state */ onFormModifiedChange?: (isModified: boolean) => void; + /** If disabled, the field editor will not be editable */ + isDisabled?: boolean; } const changeWarning = i18n.translate('indexPatternFieldEditor.editor.form.changeWarning', { @@ -111,7 +113,7 @@ const formSerializer = (field: FieldFormInternal): Field => { }; }; -const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) => { +const FieldEditorComponent = ({ field, onChange, onFormModifiedChange, isDisabled }: Props) => { const { fieldTypeToProcess, fieldName$, subfields$, dataView } = useFieldEditorContext(); const { params: { update: updatePreviewParams }, @@ -260,7 +262,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) data-test-subj="nameField" componentProps={{ euiFieldProps: { - disabled: fieldTypeToProcess === 'concrete', + disabled: fieldTypeToProcess === 'concrete' || isDisabled, 'aria-label': i18n.translate('indexPatternFieldEditor.editor.form.nameAriaLabel', { defaultMessage: 'Name field', }), @@ -272,7 +274,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) {/* Type */} @@ -304,7 +306,7 @@ const FieldEditorComponent = ({ field, onChange, onFormModifiedChange }: Props) )} {updatedType && updatedType[0].value !== 'composite' ? ( - + ) : ( )} diff --git a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/form_row.tsx b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/form_row.tsx index ea12517e885ac..b66630e30ef8e 100644 --- a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/form_row.tsx +++ b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor/form_row.tsx @@ -26,6 +26,7 @@ interface Props { children: React.ReactNode; description?: string | JSX.Element; withDividerRule?: boolean; + disabled?: boolean; 'data-test-subj'?: string; } @@ -34,6 +35,7 @@ export const FormRow = ({ description, children, formFieldPath, + disabled, withDividerRule = false, 'data-test-subj': dataTestSubj, }: Props) => { @@ -52,6 +54,7 @@ export const FormRow = ({ label: title, showLabel: false, 'data-test-subj': 'toggle', + disabled, }, }} /> diff --git a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx index e91c25da8c1aa..0aae120b9881e 100644 --- a/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/platform/plugins/shared/data_view_field_editor/public/components/field_editor_flyout_content.tsx @@ -10,11 +10,11 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, - EuiToolTip, useIsWithinMaxBreakpoint, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -39,8 +39,8 @@ const i18nTexts = { saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { defaultMessage: 'Save', }), - disabledSaveButtonTooltip: i18n.translate( - 'indexPatternFieldEditor.editor.flyoutDisabledSaveButtonTooltip', + disabledSaveCalloutMessage: i18n.translate( + 'indexPatternFieldEditor.editor.flyoutDisabledSaveCalloutMessage', { defaultMessage: 'Fields cannot be edited on managed data views. Duplicate the data view in order to make changes.', @@ -251,12 +251,22 @@ const FieldEditorFlyoutContentComponent = ({ />

+ {dataView.managed && ( + + )} @@ -275,18 +285,16 @@ const FieldEditorFlyoutContentComponent = ({
- - - {i18nTexts.saveButtonLabel} - - + + {i18nTexts.saveButtonLabel} + diff --git a/src/platform/plugins/shared/data_view_management/public/components/utils.ts b/src/platform/plugins/shared/data_view_management/public/components/utils.ts index a4dc348df1fed..4e118f2f5a642 100644 --- a/src/platform/plugins/shared/data_view_management/public/components/utils.ts +++ b/src/platform/plugins/shared/data_view_management/public/components/utils.ts @@ -80,7 +80,7 @@ export const getTags = ( const tags = []; if (indexPattern.managed) { tags.push({ - key: DataViewType.MANAGED, + key: 'managed', name: managedDataViewTagName, 'data-test-subj': 'managed-tag', }); diff --git a/src/platform/plugins/shared/data_views/common/types.ts b/src/platform/plugins/shared/data_views/common/types.ts index 184098adc8848..402219f496836 100644 --- a/src/platform/plugins/shared/data_views/common/types.ts +++ b/src/platform/plugins/shared/data_views/common/types.ts @@ -385,7 +385,6 @@ export type TypeMeta = { export enum DataViewType { DEFAULT = 'default', ROLLUP = 'rollup', - MANAGED = 'managed', } export type FieldSpecConflictDescriptions = Record; diff --git a/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts b/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts index 38078687bec97..eb1bc9590ee4b 100644 --- a/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts +++ b/src/platform/plugins/shared/data_views/server/rest_api_routes/public/create_data_view.ts @@ -80,7 +80,6 @@ const registerCreateDataViewRouteFactory = data_view: serviceKey === SERVICE_KEY ? dataViewSpecSchema : schema.never(), index_pattern: serviceKey === SERVICE_KEY_LEGACY ? dataViewSpecSchema : schema.never(), - managed: schema.maybe(schema.boolean({ defaultValue: false })), }), }, response: { diff --git a/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts b/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts index 13f1473e5f4a4..7b144e647f1f6 100644 --- a/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts +++ b/src/platform/plugins/shared/data_views/server/rest_api_routes/schema.ts @@ -54,7 +54,6 @@ export const dataViewSpecSchema = schema.object({ name: schema.maybe(schema.string()), namespaces: schema.maybe(schema.arrayOf(schema.string())), allowHidden: schema.maybe(schema.boolean()), - managed: schema.maybe(schema.boolean()), }); export const dataViewsRuntimeResponseSchema = () => diff --git a/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.test.tsx b/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.test.tsx index 80dfff526415c..c471d70a7716e 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.test.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.test.tsx @@ -82,7 +82,9 @@ describe('useDefaultAdHocDataViews', () => { ); await result.current.initializeProfileDataViews(rootProfileState); expect(clearInstanceCache.mock.calls).toEqual(previousDataViews.map((dv) => [dv.id])); - expect(createDataView.mock.calls).toEqual(newDataViews.map((dv) => [dv.toSpec(), true])); + expect(createDataView.mock.calls).toEqual( + newDataViews.map((dv) => [{ ...dv.toSpec(), managed: true }, true]) + ); expect( stateContainer.runtimeStateManager.adHocDataViews$.getValue().map((dv) => dv.id) ).toEqual([existingAdHocDataVew.id, ...newDataViews.map((dv) => dv.id)]); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.ts b/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.ts index 005d31aa5df73..79fb7158e9422 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/hooks/use_default_ad_hoc_data_views.ts @@ -40,7 +40,7 @@ export const useDefaultAdHocDataViews = () => { const profileDataViewSpecs = rootProfileState.getDefaultAdHocDataViews(); const profileDataViews = await Promise.all( - profileDataViewSpecs.map((spec) => dataViews.create(spec, true)) + profileDataViewSpecs.map((spec) => dataViews.create({ ...spec, managed: true }, true)) ); dispatch(internalStateActions.setDefaultProfileAdHocDataViews(profileDataViews)); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts index b44493ff65e79..22cf03cbc83c7 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/common/classic_nav_root_profile/profile.test.ts @@ -57,7 +57,6 @@ describe('classicNavRootProfileProvider', () => { name: 'All logs', timeFieldName: '@timestamp', title: 'logs-*', - managed: true, }, ]); }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts index e41dcfd2fa818..cc31cc44bff48 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/observability_root_profile/profile.test.ts @@ -70,7 +70,6 @@ describe('observabilityRootProfileProvider', () => { name: 'All logs', timeFieldName: '@timestamp', title: 'logs-*', - managed: true, }, ]); }); diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx index a00943488bc01..4607164bcb75f 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.test.tsx @@ -20,9 +20,6 @@ import { DataViewSelector } from './data_view_selector'; import { dataViewMock, dataViewMockEsql } from './mocks/dataview'; import type { DataViewPickerProps } from './data_view_picker'; -const dataViewEditorMock = dataViewEditorPluginMock.createStartContract(); -const openEditorMock = jest.fn(); - describe('DataView component', () => { const createMockWebStorage = () => ({ clear: jest.fn(), @@ -51,8 +48,8 @@ describe('DataView component', () => { storageValue: boolean, uiSettingValue: boolean = false ) { + const dataViewEditorMock = dataViewEditorPluginMock.createStartContract(); (dataViewEditorMock.userPermissions.editDataView as jest.Mock).mockReturnValue(true); - (dataViewEditorMock.openEditor as jest.Mock).mockImplementation(openEditorMock); let dataMock = dataPluginMock.createStartContract(); dataMock = { ...dataMock, @@ -69,9 +66,6 @@ describe('DataView component', () => { uiSettings: { get: jest.fn(() => uiSettingValue), }, - application: { - navigateToApp: jest.fn(), - }, }; return ( diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx index af732865bd751..a6878346ac320 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/change_dataview.tsx @@ -26,8 +26,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { DataView } from '@kbn/data-views-plugin/public'; -import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common'; +import type { DataView } from '@kbn/data-views-plugin/public'; import type { IUnifiedSearchPluginServices } from '../types'; import { type DataViewPickerProps } from './data_view_picker'; import type { DataViewListItemEnhanced } from './dataview_list'; @@ -144,9 +143,10 @@ export function ChangeDataView({ return; } const dataView = await dataViews.get(currentDataViewId); - const editData = new DataView({ - spec: { title: dataView.getIndexPattern() }, - fieldFormats: {} as FieldFormatsStartCommon, + const editData = await dataViews.create({ + ...dataView.toSpec(), + id: undefined, + managed: false, }); dataViewEditor.openEditor({ @@ -155,7 +155,7 @@ export function ChangeDataView({ onDataViewCreated(newDataView); }, allowAdHocDataView: true, - isDuplicatingManaged: true, + isDuplicating: true, }); }, [currentDataViewId, dataViews, dataViewEditor, onDataViewCreated]); diff --git a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx index fca87dd69cf0d..5f4a98680fac3 100644 --- a/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx +++ b/src/platform/plugins/shared/unified_search/public/dataview_picker/data_view_picker.tsx @@ -59,7 +59,7 @@ export interface DataViewPickerProps { */ onAddField?: () => void; /** - * Callback that is called when the user clicks the create dataview option. + * Callback that is called when the user creates a new data view through the picker menu. * The first parameter is the created data view * Also works as a flag to show the create dataview button. */ diff --git a/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts b/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts index 981469cf2ac02..45c7d6d3a3e70 100644 --- a/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts +++ b/src/platform/test/functional/apps/discover/context_awareness/extensions/_get_default_ad_hoc_data_views.ts @@ -36,8 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataViews.getSelectedName()).not.to.be('Example profile data view'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(false); - expect(await dataViews.isAdHoc()).to.be(true); + expect(await dataViews.isManaged()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -52,8 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.waitUntilSearchingHasFinished(); expect(await toasts.getCount({ timeout: 2000 })).to.be(0); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); - expect(await dataViews.isManaged()).to.be(false); - expect(await dataViews.isAdHoc()).to.be(true); + expect(await dataViews.isManaged()).to.be(true); }); it('should create a copy of the profile data view when saving the Discover session', async () => { @@ -72,8 +70,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ).to.be('2024-06-10T14:00:00.000Z'); await dataViews.switchTo('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(false); - expect(await dataViews.isAdHoc()).to.be(true); + expect(await dataViews.isManaged()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() @@ -102,8 +99,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await dataViews.getSelectedName()).to.be('Example profile data view'); await discover.waitUntilSearchingHasFinished(); - expect(await dataViews.isManaged()).to.be(false); - expect(await dataViews.isAdHoc()).to.be(true); + expect(await dataViews.isManaged()).to.be(true); expect(await unifiedFieldList.getSidebarSectionFieldNames('available')).to.have.length(7); expect( await (await dataGrid.getCellElementByColumnName(0, '@timestamp')).getVisibleText() diff --git a/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts b/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts index d8ea9e5b20e63..c77b58b8e5cf8 100644 --- a/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts +++ b/x-pack/platform/test/serverless/api_integration/test_suites/data_views/integration/integration.ts @@ -48,7 +48,6 @@ export default function ({ getService }: FtrProviderContext) { index_pattern: { title, }, - managed: false, }); const id = response1.body.index_pattern.id; const response2 = await supertestWithoutAuth diff --git a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts index 45c7ceb42ce42..f771f5e4883f3 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/data_view_manager/redux/slices.ts @@ -63,7 +63,7 @@ export const sharedDataViewManagerSlice = createSlice({ // NOTE: user is allowed to duplicate a managed data view and // we want both to show up in the list state.adhocDataViews.find( - (dv) => dv.title === dataViewSpec.title && dv.name === dataViewSpec.managed + (dv) => dv.title === dataViewSpec.title && dv.managed === dataViewSpec.managed ) ) { return; From e9872f3f4fd8464e1e78805971c0ebd22dfe010b Mon Sep 17 00:00:00 2001 From: christineweng Date: Thu, 28 Aug 2025 15:59:04 -0500 Subject: [PATCH 05/12] add unit tests --- .../public/components/footer/footer.test.tsx | 161 ++++++++++++++++++ .../public/components/footer/footer.tsx | 2 +- .../_get_default_ad_hoc_data_views.ts | 2 +- .../_get_default_ad_hoc_data_views.ts | 5 +- 4 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 src/platform/plugins/shared/data_view_editor/public/components/footer/footer.test.tsx diff --git a/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.test.tsx b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.test.tsx new file mode 100644 index 0000000000000..6cd82e241b816 --- /dev/null +++ b/src/platform/plugins/shared/data_view_editor/public/components/footer/footer.test.tsx @@ -0,0 +1,161 @@ +/* + * 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 React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { Footer } from './footer'; + +const onSubmit = jest.fn(); +const props = { + onCancel: jest.fn(), + onSubmit, + submitDisabled: false, + submittingType: undefined, + hasEditData: true, + allowAdHoc: false, + isPersisted: true, + canSave: true, + isManaged: false, + isDuplicating: false, +}; + +const SAVE_AS_AD_HOC_BUTTON_TEST_ID = 'exploreIndexPatternButton'; +const SAVE_AS_PERSISTED_BUTTON_TEST_ID = 'saveIndexPatternButton'; +const DUPLICATE_BUTTON_TEST_ID = 'duplicateButton'; + +describe('Footer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Create new dataview', () => { + it('renders footer buttons correctly', () => { + const { getByTestId, queryByTestId } = render(