diff --git a/docs/concepts/data-views.asciidoc b/docs/concepts/data-views.asciidoc index 992741be518a7..2eba42aed3051 100644 --- a/docs/concepts/data-views.asciidoc +++ b/docs/concepts/data-views.asciidoc @@ -172,3 +172,14 @@ WARNING: Deleting a {data-source} breaks all visualizations, saved searches, and . Find the {data-source} that you want to delete, and then click image:management/index-patterns/images/delete.png[Delete icon] in the *Actions* column. + +[float] +[[data-view-field-cache]] +=== {data-source} field cache + +The browser caches {data-source} field lists for increased performance. This is particularly impactful +for {data-sources} with a high field count that span a large number of indices and clusters. The field +list is updated every couple of minutes in typical {kib} usage. Alternatively, use the refresh button on the {data-source} +management detail page to get an updated field list. A force reload of {kib} has the same effect. + +The field list may be impacted by changes in indices and user permissions. \ No newline at end of file diff --git a/packages/core/http/core-http-router-server-internal/src/response.ts b/packages/core/http/core-http-router-server-internal/src/response.ts index 1fc8d310233c7..d96eaa6571bf0 100644 --- a/packages/core/http/core-http-router-server-internal/src/response.ts +++ b/packages/core/http/core-http-router-server-internal/src/response.ts @@ -17,6 +17,7 @@ import type { ErrorHttpResponseOptions, KibanaErrorResponseFactory, KibanaRedirectionResponseFactory, + KibanaNotModifiedResponseFactory, KibanaSuccessResponseFactory, KibanaResponseFactory, LifecycleResponseFactory, @@ -51,6 +52,10 @@ const redirectionResponseFactory: KibanaRedirectionResponseFactory = { redirected: (options: RedirectResponseOptions) => new KibanaResponse(302, options.body, options), }; +const notModifiedResponseFactory: KibanaNotModifiedResponseFactory = { + notModified: (options: HttpResponseOptions = {}) => new KibanaResponse(304, undefined, options), +}; + const errorResponseFactory: KibanaErrorResponseFactory = { badRequest: (options: ErrorHttpResponseOptions = {}) => new KibanaResponse(400, options.body || 'Bad Request', options), @@ -120,6 +125,7 @@ export const fileResponseFactory = { export const kibanaResponseFactory: KibanaResponseFactory = { ...successResponseFactory, ...redirectionResponseFactory, + ...notModifiedResponseFactory, ...errorResponseFactory, ...fileResponseFactory, custom: ( diff --git a/packages/core/http/core-http-router-server-internal/src/response_adapter.ts b/packages/core/http/core-http-router-server-internal/src/response_adapter.ts index f6a00bb7e6b92..a5cc1ca1f8d96 100644 --- a/packages/core/http/core-http-router-server-internal/src/response_adapter.ts +++ b/packages/core/http/core-http-router-server-internal/src/response_adapter.ts @@ -33,7 +33,8 @@ function setHeaders(response: HapiResponseObject, headers: Record code >= 100 && code < 300, - isRedirect: (code: number) => code >= 300 && code < 400, + isNotModified: (code: number) => code === 304, + isRedirect: (code: number) => code >= 300 && code < 400 && code !== 304, isError: (code: number) => code >= 400 && code < 600, }; @@ -76,7 +77,10 @@ export class HapiResponseAdapter { if (statusHelpers.isError(kibanaResponse.status)) { return this.toError(kibanaResponse); } - if (statusHelpers.isSuccess(kibanaResponse.status)) { + if ( + statusHelpers.isSuccess(kibanaResponse.status) || + statusHelpers.isNotModified(kibanaResponse.status) + ) { return this.toSuccess(kibanaResponse); } if (statusHelpers.isRedirect(kibanaResponse.status)) { diff --git a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts index 4272cf130b38e..d5904699b5813 100644 --- a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts +++ b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts @@ -121,6 +121,7 @@ const createResponseFactoryMock = (): jest.Mocked => ({ ok: jest.fn(), accepted: jest.fn(), noContent: jest.fn(), + notModified: jest.fn(), custom: jest.fn(), redirected: jest.fn(), badRequest: jest.fn(), diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index 9a28a7be5033e..82022ce77d6e3 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -95,6 +95,7 @@ export type { IKibanaSocket, KibanaErrorResponseFactory, KibanaRedirectionResponseFactory, + KibanaNotModifiedResponseFactory, KibanaSuccessResponseFactory, KibanaResponseFactory, LifecycleResponseFactory, diff --git a/packages/core/http/core-http-server/src/router/index.ts b/packages/core/http/core-http-server/src/router/index.ts index c72d7386e867d..628bab27db54f 100644 --- a/packages/core/http/core-http-server/src/router/index.ts +++ b/packages/core/http/core-http-server/src/router/index.ts @@ -66,6 +66,7 @@ export type { IKibanaSocket } from './socket'; export type { KibanaErrorResponseFactory, KibanaRedirectionResponseFactory, + KibanaNotModifiedResponseFactory, KibanaSuccessResponseFactory, KibanaResponseFactory, LifecycleResponseFactory, diff --git a/packages/core/http/core-http-server/src/router/response.ts b/packages/core/http/core-http-server/src/router/response.ts index 07ec226e8c3a9..194333d42f9c9 100644 --- a/packages/core/http/core-http-server/src/router/response.ts +++ b/packages/core/http/core-http-server/src/router/response.ts @@ -95,9 +95,7 @@ export interface FileHttpResponseOptions !dataView.getFieldByName(field.name)); // refresh the data view in case there are new fields if (newFields.length) { - await dataViewsService.refreshFields(dataView, false); + await dataViewsService.refreshFields(dataView, false, true); } const allFields = buildFieldList(dataView, metaFields); return { diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 7ce1c9ccbc66d..f850ef4ec3671 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -95,6 +95,9 @@ export const EditIndexPattern = withRouter( const [showEditDialog, setShowEditDialog] = useState(false); const [relationships, setRelationships] = useState([]); const [allowedTypes, setAllowedTypes] = useState([]); + const [refreshCount, setRefreshCount] = useState(0); // used for forcing rerender of field list + const [isRefreshing, setIsRefreshing] = React.useState(false); + const conflictFieldsUrl = useMemo(() => { return setStateToKbnUrl( APP_STATE_STORAGE_KEY, @@ -144,7 +147,7 @@ export const EditIndexPattern = withRouter( setConflictedFields( indexPattern.fields.getAll().filter((field) => field.type === 'conflict') ); - }, [indexPattern]); + }, [indexPattern, refreshCount]); useEffect(() => { setTags( @@ -332,6 +335,13 @@ export const EditIndexPattern = withRouter( setFields(indexPattern.getNonScriptedFields()); setCompositeRuntimeFields(getCompositeRuntimeFields(indexPattern)); }} + refreshIndexPatternClick={async () => { + setIsRefreshing(true); + await dataViews.refreshFields(indexPattern, false, true); + setRefreshCount(refreshCount + 1); // rerender field list + setIsRefreshing(false); + }} + isRefreshing={isRefreshing} /> {displayIndexPatternEditor} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx index 83c965f2b9332..8b12ed59ed6fd 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -17,7 +17,9 @@ interface IndexHeaderProps { setDefault?: () => void; editIndexPatternClick?: () => void; deleteIndexPatternClick?: () => void; + refreshIndexPatternClick?: () => void; canSave: boolean; + isRefreshing?: boolean; } const setDefaultAriaLabel = i18n.translate('indexPatternManagement.editDataView.setDefaultAria', { @@ -63,6 +65,7 @@ export const IndexHeader: React.FC = ({ iconType="pencil" aria-label={editAriaLabel} data-test-subj="editIndexPatternButton" + color="primary" > {editTooltip} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx index 0128d013af6f5..426442bcfb031 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -19,9 +19,10 @@ import { EuiTabbedContentTab, EuiSpacer, EuiFieldSearch, - EuiButton, EuiFilterSelectItem, FilterChecked, + EuiToolTip, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '@kbn/kibana-utils-plugin/public'; @@ -60,6 +61,8 @@ interface TabsProps extends Pick { relationships: SavedObjectRelation[]; allowedTypes: SavedObjectManagementTypeInfo[]; compositeRuntimeFields: Record; + refreshIndexPatternClick: () => void; + isRefreshing?: boolean; } interface FilterItems { @@ -139,6 +142,14 @@ const addFieldButtonLabel = i18n.translate( } ); +const refreshAriaLabel = i18n.translate('indexPatternManagement.editDataView.refreshAria', { + defaultMessage: 'Refresh', +}); + +const refreshTooltip = i18n.translate('indexPatternManagement.editDataView.refreshTooltip', { + defaultMessage: 'Refresh local copy of data view field list', +}); + const SCHEMA_ITEMS: FilterItems[] = [ { value: 'runtime', @@ -159,6 +170,8 @@ export const Tabs: React.FC = ({ relationships, allowedTypes, compositeRuntimeFields, + refreshIndexPatternClick, + isRefreshing, }) => { const { uiSettings, @@ -284,7 +297,9 @@ export const Tabs: React.FC = ({ setIndexedFieldTypes(convertToEuiFilterOptions(tempIndexedFieldTypes)); setScriptedFieldLanguages(convertToEuiFilterOptions(tempScriptedFieldLanguages)); - }, [indexPattern]); + // need to reset based on changes to fields but indexPattern is the same + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern, fields]); const closeFieldEditor = useCallback(() => { if (closeEditorHandler.current) { @@ -321,11 +336,13 @@ export const Tabs: React.FC = ({ [uiSettings] ); + const refreshRef = useRef(null); + const userEditPermission = dataViews.getCanSaveSync(); const getFilterSection = useCallback( (type: string) => { return ( - + = ({ + + {refreshTooltip}

}> + { + refreshIndexPatternClick(); + // clear tooltip focus + if (refreshRef.current) { + refreshRef.current.blur(); + } + }} + iconType="refresh" + aria-label={refreshAriaLabel} + data-test-subj="refreshDataViewButton" + isLoading={isRefreshing} + isDisabled={isRefreshing} + size="m" + color="success" + className="eui-fullWidth" + > + {refreshAriaLabel} + +
+
{userEditPermission && ( - openFieldEditor()} data-test-subj="addField"> + openFieldEditor()} + data-test-subj="addField" + iconType="plusInCircle" + aria-label={addFieldButtonLabel} + color="primary" + fill + > {addFieldButtonLabel} @@ -503,6 +552,8 @@ export const Tabs: React.FC = ({ updateFieldFilter, updateFieldTypeFilter, updateSchemaFieldTypeFilter, + isRefreshing, + refreshIndexPatternClick, ] ); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 6f6df3e77bf09..3010dd41448ea 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -221,7 +221,7 @@ export const IndexPatternTable = ({
{dataView.getName()} {dataView.name ? ( diff --git a/src/plugins/data_views/common/constants.ts b/src/plugins/data_views/common/constants.ts index dc396f2772aa8..43dc67362bf26 100644 --- a/src/plugins/data_views/common/constants.ts +++ b/src/plugins/data_views/common/constants.ts @@ -58,6 +58,12 @@ export const PLUGIN_NAME = 'DataViews'; */ export const FIELDS_FOR_WILDCARD_PATH = '/internal/data_views/_fields_for_wildcard'; +/** + * Fields path. Like fields for wildcard but GET only + * @public + */ +export const FIELDS_PATH = '/internal/data_views/fields'; + /** * Existing indices path * @public diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index d60d0b4c1172c..9767b44b95a72 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -61,6 +61,8 @@ export class DataView extends AbstractDataView implements DataViewBase { */ public flattenHit: (hit: Record, deep?: boolean) => Record; + private etag: string | undefined; + /** * constructor * @param config - config data and dependencies @@ -77,6 +79,10 @@ export class DataView extends AbstractDataView implements DataViewBase { this.fields.replaceAll(Object.values(spec.fields || {})); } + getEtag = () => this.etag; + + setEtag = (etag: string | undefined) => (this.etag = etag); + /** * Returns scripted fields */ diff --git a/src/plugins/data_views/common/data_views/data_views.test.ts b/src/plugins/data_views/common/data_views/data_views.test.ts index e95e5c6736d17..4d0fa196e9ccc 100644 --- a/src/plugins/data_views/common/data_views/data_views.test.ts +++ b/src/plugins/data_views/common/data_views/data_views.test.ts @@ -158,10 +158,23 @@ describe('IndexPatterns', () => { test('force field refresh', async () => { const id = '1'; + // make sure initial load and subsequent reloads use same params + const args = { + allowHidden: undefined, + allowNoIndex: true, + indexFilter: undefined, + metaFields: false, + pattern: 'something', + rollupIndex: undefined, + type: undefined, + }; + await indexPatterns.get(id); expect(apiClient.getFieldsForWildcard).toBeCalledTimes(1); + expect(apiClient.getFieldsForWildcard).toBeCalledWith(args); await indexPatterns.get(id, undefined, true); expect(apiClient.getFieldsForWildcard).toBeCalledTimes(2); + expect(apiClient.getFieldsForWildcard).toBeCalledWith(args); }); test('getFieldsForWildcard called with allowNoIndex set to true as default ', async () => { diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 6052be19d4f99..a244868c0614d 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -261,7 +261,11 @@ export interface DataViewsServicePublicMethods { * Refresh fields for data view instance * @params dataView - Data view instance */ - refreshFields: (indexPattern: DataView, displayErrors?: boolean) => Promise; + refreshFields: ( + indexPattern: DataView, + displayErrors?: boolean, + forceRefresh?: boolean + ) => Promise; /** * Converts data view saved object to spec * @params savedObject - Data view saved object @@ -535,7 +539,10 @@ export class DataViewsService { (indexPattern as DataViewSpec).allowHidden || (indexPattern as DataView)?.getAllowHidden(), }); - private getFieldsAndIndicesForDataView = async (dataView: DataView) => { + private getFieldsAndIndicesForDataView = async ( + dataView: DataView, + forceRefresh: boolean = false + ) => { const metaFields = await this.config.get(META_FIELDS); return this.apiClient.getFieldsForWildcard({ type: dataView.type, @@ -543,7 +550,8 @@ export class DataViewsService { allowNoIndex: true, pattern: dataView.getIndexPattern(), metaFields, - allowHidden: dataView.getAllowHidden(), + forceRefresh, + allowHidden: dataView.getAllowHidden() || undefined, }); }; @@ -560,8 +568,18 @@ export class DataViewsService { }); }; - private refreshFieldsFn = async (indexPattern: DataView) => { - const { fields, indices } = await this.getFieldsAndIndicesForDataView(indexPattern); + private refreshFieldsFn = async (indexPattern: DataView, forceRefresh: boolean = false) => { + const { fields, indices, etag } = await this.getFieldsAndIndicesForDataView( + indexPattern, + forceRefresh + ); + + if (indexPattern.getEtag() && etag === indexPattern.getEtag()) { + return; + } else { + indexPattern.setEtag(etag); + } + fields.forEach((field) => (field.isMapped = true)); const scripted = this.scriptedFieldsEnabled ? indexPattern.getScriptedFields().map((field) => field.spec) @@ -587,13 +605,17 @@ export class DataViewsService { * @param dataView * @param displayErrors - If set false, API consumer is responsible for displaying and handling errors. */ - refreshFields = async (dataView: DataView, displayErrors: boolean = true) => { + refreshFields = async ( + dataView: DataView, + displayErrors: boolean = true, + forceRefresh: boolean = false + ) => { if (!displayErrors) { - return this.refreshFieldsFn(dataView); + return this.refreshFieldsFn(dataView, forceRefresh); } try { - await this.refreshFieldsFn(dataView); + await this.refreshFieldsFn(dataView, forceRefresh); } catch (err) { if (err instanceof DataViewMissingIndices) { // not considered an error, check dataView.matchedIndices.length to be 0 @@ -634,7 +656,11 @@ export class DataViewsService { : []; try { let updatedFieldList: FieldSpec[]; - const { fields: newFields, indices } = await this.getFieldsAndIndicesForWildcard(options); + const { + fields: newFields, + indices, + etag, + } = await this.getFieldsAndIndicesForWildcard(options); newFields.forEach((field) => (field.isMapped = true)); // If allowNoIndex, only update field list if field caps finds fields. To support @@ -645,7 +671,7 @@ export class DataViewsService { updatedFieldList = fieldsAsArr; } - return { fields: this.fieldArrayToMap(updatedFieldList, fieldAttrs), indices }; + return { fields: this.fieldArrayToMap(updatedFieldList, fieldAttrs), indices, etag }; } catch (err) { if (err instanceof DataViewMissingIndices) { // not considered an error, check dataView.matchedIndices.length to be 0 @@ -759,7 +785,7 @@ export class DataViewsService { displayErrors?: boolean; }) => { const { title, type, typeMeta, runtimeFieldMap } = spec; - const { fields, indices } = await this.refreshFieldSpecMap( + const { fields, indices, etag } = await this.refreshFieldSpecMap( spec.fields || {}, savedObjectId, spec.title as string, @@ -777,7 +803,7 @@ export class DataViewsService { const runtimeFieldSpecs = this.getRuntimeFields(runtimeFieldMap, spec.fieldAttrs); // mapped fields overwrite runtime fields - return { fields: { ...runtimeFieldSpecs, ...fields }, indices: indices || [] }; + return { fields: { ...runtimeFieldSpecs, ...fields }, indices: indices || [], etag }; }; private initFromSavedObject = async ( @@ -791,6 +817,7 @@ export class DataViewsService { let fields: Record = {}; let indices: string[] = []; + let etag: string | undefined; if (!displayErrors) { const fieldsAndIndices = await this.initFromSavedObjectLoadFields({ @@ -800,6 +827,7 @@ export class DataViewsService { }); fields = fieldsAndIndices.fields; indices = fieldsAndIndices.indices; + etag = fieldsAndIndices.etag; } else { try { const fieldsAndIndices = await this.initFromSavedObjectLoadFields({ @@ -809,6 +837,7 @@ export class DataViewsService { }); fields = fieldsAndIndices.fields; indices = fieldsAndIndices.indices; + etag = fieldsAndIndices.etag; } catch (err) { if (err instanceof DataViewMissingIndices) { // not considered an error, check dataView.matchedIndices.length to be 0 @@ -833,6 +862,7 @@ export class DataViewsService { : {}; const indexPattern = await this.createFromSpec(spec, true, displayErrors); + indexPattern.setEtag(etag); indexPattern.matchedIndices = indices; indexPattern.resetOriginalSavedObjectBody(); return indexPattern; diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index dc477d2fb5b83..7fb7ddedbccd4 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -314,6 +314,7 @@ export interface GetFieldsOptions { includeUnmapped?: boolean; fields?: string[]; allowHidden?: boolean; + forceRefresh?: boolean; } /** @@ -322,6 +323,7 @@ export interface GetFieldsOptions { export interface FieldsForWildcardResponse { fields: FieldSpec[]; indices: string[]; + etag?: string; } /** @@ -542,4 +544,5 @@ export interface HasDataService { export interface ClientConfigType { scriptedFieldsEnabled?: boolean; + fieldListCachingEnabled?: boolean; } diff --git a/src/plugins/data_views/common/utils.test.ts b/src/plugins/data_views/common/utils.test.ts index 3351a15da1a13..e8602def7f712 100644 --- a/src/plugins/data_views/common/utils.test.ts +++ b/src/plugins/data_views/common/utils.test.ts @@ -8,6 +8,7 @@ import { isFilterable } from '.'; import type { DataViewField } from './fields'; +import { unwrapEtag } from './utils'; const mockField = { name: 'foo', @@ -16,38 +17,55 @@ const mockField = { type: 'string', } as DataViewField; -describe('isFilterable', () => { - describe('types', () => { - it('should return true for filterable types', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { - expect(isFilterable({ ...mockField, type } as DataViewField)).toBe(true); +describe('common utils', () => { + describe('isFilterable', () => { + describe('types', () => { + it('should return true for filterable types', () => { + ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { + expect(isFilterable({ ...mockField, type } as DataViewField)).toBe(true); + }); }); - }); - it('should return false for filterable types if the field is not searchable', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { - expect(isFilterable({ ...mockField, type, searchable: false } as DataViewField)).toBe( - false - ); + it('should return false for filterable types if the field is not searchable', () => { + ['string', 'number', 'date', 'ip', 'boolean'].forEach((type) => { + expect(isFilterable({ ...mockField, type, searchable: false } as DataViewField)).toBe( + false + ); + }); }); - }); - it('should return false for un-filterable types', () => { - ['geo_point', 'geo_shape', 'attachment', 'murmur3', '_source', 'unknown', 'conflict'].forEach( - (type) => { + it('should return false for un-filterable types', () => { + [ + 'geo_point', + 'geo_shape', + 'attachment', + 'murmur3', + '_source', + 'unknown', + 'conflict', + ].forEach((type) => { expect(isFilterable({ ...mockField, type } as DataViewField)).toBe(false); - } - ); + }); + }); }); - }); - it('should return true for scripted fields', () => { - expect(isFilterable({ ...mockField, scripted: true, searchable: false } as DataViewField)).toBe( - true - ); + it('should return true for scripted fields', () => { + expect( + isFilterable({ ...mockField, scripted: true, searchable: false } as DataViewField) + ).toBe(true); + }); + + it('should return true for the _id field', () => { + expect(isFilterable({ ...mockField, name: '_id' } as DataViewField)).toBe(true); + }); }); - it('should return true for the _id field', () => { - expect(isFilterable({ ...mockField, name: '_id' } as DataViewField)).toBe(true); + describe('unwrapEtag', () => { + it('should return the etag without quotes', () => { + expect(unwrapEtag('"foo"')).toBe('foo'); + }); + it('should return the etag without quotes and without gzip', () => { + expect(unwrapEtag('"foo-gzip"')).toBe('foo'); + }); }); }); diff --git a/src/plugins/data_views/common/utils.ts b/src/plugins/data_views/common/utils.ts index e63167ff4d3ff..61de24dfcf0c3 100644 --- a/src/plugins/data_views/common/utils.ts +++ b/src/plugins/data_views/common/utils.ts @@ -27,3 +27,11 @@ export async function findByName(client: PersistenceAPI, name: string) { return savedObjects ? savedObjects[0] : undefined; } } + +export function unwrapEtag(ifNoneMatch: string) { + let requestHash = ifNoneMatch.replace(/^"(.+)"$/, '$1'); + if (requestHash.indexOf('-') > -1) { + requestHash = requestHash.split('-')[0]; + } + return requestHash; +} diff --git a/src/plugins/data_views/kibana.jsonc b/src/plugins/data_views/kibana.jsonc index 7595f8e68b3c4..7789383b48ba4 100644 --- a/src/plugins/data_views/kibana.jsonc +++ b/src/plugins/data_views/kibana.jsonc @@ -18,6 +18,7 @@ "requiredBundles": [ "kibanaUtils" ], + "runtimePluginDependencies" : ["security"], "extraPublicDirs": [ "common" ] diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.test.ts b/src/plugins/data_views/public/data_views/data_views_api_client.test.ts index f36776153ee3a..66f025dc07835 100644 --- a/src/plugins/data_views/public/data_views/data_views_api_client.test.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.test.ts @@ -9,7 +9,7 @@ import type { HttpSetup } from '@kbn/core/public'; import { http } from './data_views_api_client.test.mock'; import { DataViewsApiClient } from './data_views_api_client'; -import { FIELDS_FOR_WILDCARD_PATH as expectedPath } from '../../common/constants'; +import { FIELDS_PATH as expectedPath } from '../../common/constants'; describe('IndexPatternsApiClient', () => { let fetchSpy: jest.SpyInstance; @@ -17,12 +17,32 @@ describe('IndexPatternsApiClient', () => { beforeEach(() => { fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => Promise.resolve({})); - indexPatternsApiClient = new DataViewsApiClient(http as HttpSetup); + indexPatternsApiClient = new DataViewsApiClient(http as HttpSetup, () => + Promise.resolve(undefined) + ); }); test('uses the right URI to fetch fields for wildcard', async function () { - await indexPatternsApiClient.getFieldsForWildcard({ pattern: 'blah' }); + await indexPatternsApiClient.getFieldsForWildcard({ pattern: 'blah', allowHidden: false }); - expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, { + // not sure what asResponse is but the rest of the results are useful + asResponse: true, + headers: { + 'user-hash': '', + }, + query: { + allow_hidden: undefined, + allow_no_index: undefined, + apiVersion: '1', // version passed through query params for caching + fields: undefined, + include_unmapped: undefined, + meta_fields: undefined, + pattern: 'blah', + rollup_index: undefined, + type: undefined, + }, + version: '1', // version header + }); }); }); diff --git a/src/plugins/data_views/public/data_views/data_views_api_client.ts b/src/plugins/data_views/public/data_views/data_views_api_client.ts index ca5782736e3cd..65b418642b34f 100644 --- a/src/plugins/data_views/public/data_views/data_views_api_client.ts +++ b/src/plugins/data_views/public/data_views/data_views_api_client.ts @@ -6,33 +6,61 @@ * Side Public License, v 1. */ -import { HttpSetup } from '@kbn/core/public'; +import { HttpSetup, HttpResponse } from '@kbn/core/public'; import { DataViewMissingIndices } from '../../common/lib'; import { GetFieldsOptions, IDataViewsApiClient } from '../../common'; import { FieldsForWildcardResponse } from '../../common/types'; -import { FIELDS_FOR_WILDCARD_PATH } from '../../common/constants'; +import { FIELDS_FOR_WILDCARD_PATH, FIELDS_PATH } from '../../common/constants'; const API_BASE_URL: string = `/api/index_patterns/`; const version = '1'; +async function sha1(str: string) { + const enc = new TextEncoder(); + const hash = await crypto.subtle.digest('SHA-1', enc.encode(str)); + return Array.from(new Uint8Array(hash)) + .map((v) => v.toString(16).padStart(2, '0')) + .join(''); +} + /** * Data Views API Client - client implementation */ export class DataViewsApiClient implements IDataViewsApiClient { private http: HttpSetup; + private getCurrentUserId: () => Promise; /** * constructor * @param http http dependency */ - constructor(http: HttpSetup) { + constructor(http: HttpSetup, getCurrentUserId: () => Promise) { this.http = http; + this.getCurrentUserId = getCurrentUserId; } - private _request(url: string, query?: {}, body?: string): Promise { + private async _request( + url: string, + query?: {}, + body?: string, + forceRefresh?: boolean + ): Promise | undefined> { + const asResponse = true; + const cacheOptions: { cache?: RequestCache } = forceRefresh ? { cache: 'no-cache' } : {}; + const userId = await this.getCurrentUserId(); + + const userHash = userId ? await sha1(userId) : ''; + const request = body - ? this.http.post(url, { query, body, version }) - : this.http.fetch(url, { query, version }); + ? this.http.post(url, { query, body, version, asResponse }) + : this.http.fetch(url, { + query, + version, + ...cacheOptions, + asResponse, + headers: { 'user-hash': userHash }, + }); + return request.catch((resp) => { if (resp.body.statusCode === 404 && resp.body.attributes?.code === 'no_matching_indices') { throw new DataViewMissingIndices(resp.body.message); @@ -60,10 +88,14 @@ export class DataViewsApiClient implements IDataViewsApiClient { indexFilter, includeUnmapped, fields, + forceRefresh, allowHidden, } = options; + const path = indexFilter ? FIELDS_FOR_WILDCARD_PATH : FIELDS_PATH; + const versionQueryParam = indexFilter ? {} : { apiVersion: version }; + return this._request( - FIELDS_FOR_WILDCARD_PATH, + path, { pattern, meta_fields: metaFields, @@ -72,11 +104,18 @@ export class DataViewsApiClient implements IDataViewsApiClient { allow_no_index: allowNoIndex, include_unmapped: includeUnmapped, fields, - allow_hidden: allowHidden, + // default to undefined to keep value out of URL params and improve caching + allow_hidden: allowHidden || undefined, + ...versionQueryParam, }, - indexFilter ? JSON.stringify({ index_filter: indexFilter }) : undefined + indexFilter ? JSON.stringify({ index_filter: indexFilter }) : undefined, + forceRefresh ).then((response) => { - return response || { fields: [], indices: [] }; + return { + indices: response?.body?.indices || [], + fields: response?.body?.fields || [], + etag: response?.response?.headers?.get('etag') || '', + }; }); } @@ -87,6 +126,7 @@ export class DataViewsApiClient implements IDataViewsApiClient { const response = await this._request<{ result: boolean }>( this._getUrl(['has_user_index_pattern']) ); - return response?.result ?? false; + + return response?.body?.result ?? false; } } diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index f690552b5a147..96380665548dd 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -67,6 +67,7 @@ export type { DataViewsContract, HasDataViewsResponse, IndicesViaSearchResponse, + UserIdGetter, } from './types'; // Export plugin after all other imports diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index d00292c3f4fe8..8ff545d8585e1 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -8,6 +8,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import type { SecurityPluginStart } from '@kbn/security-plugin-types-public'; import { getIndexPatternLoad } from './expressions'; import type { ClientConfigType } from '../common/types'; import { @@ -15,6 +16,7 @@ import { DataViewsPublicPluginStart, DataViewsPublicSetupDependencies, DataViewsPublicStartDependencies, + UserIdGetter, } from './types'; import { DataViewsApiClient } from '.'; @@ -41,6 +43,7 @@ export class DataViewsPublicPlugin { private readonly hasData = new HasData(); private rollupsEnabled: boolean = false; + private userIdGetter: UserIdGetter = async () => undefined; constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -60,6 +63,18 @@ export class DataViewsPublicPlugin }), }); + core.plugins.onStart<{ security: SecurityPluginStart }>('security').then(({ security }) => { + if (security.found) { + const getUserId = async function getUserId(): Promise { + const currentUser = await security.contract.authc.getCurrentUser(); + return currentUser?.profile_uid; + }; + this.userIdGetter = getUserId; + } else { + throw new Error('Security plugin is not available, but is required for Data Views plugin'); + } + }); + return { enableRollups: () => (this.rollupsEnabled = true), }; @@ -86,7 +101,7 @@ export class DataViewsPublicPlugin hasData: this.hasData.start(core), uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new ContentMagementWrapper(contentManagement.client), - apiClient: new DataViewsApiClient(http), + apiClient: new DataViewsApiClient(http, () => this.userIdGetter()), fieldFormats, http, onNotification: (toastInputFields, key) => { diff --git a/src/plugins/data_views/public/types.ts b/src/plugins/data_views/public/types.ts index 7d6cbbc6cf533..98db0382f641f 100644 --- a/src/plugins/data_views/public/types.ts +++ b/src/plugins/data_views/public/types.ts @@ -106,6 +106,8 @@ export interface DataViewsPublicStartDependencies { contentManagement: ContentManagementPublicStart; } +export type UserIdGetter = () => Promise; + /** * Data plugin public Setup contract */ diff --git a/src/plugins/data_views/server/constants.ts b/src/plugins/data_views/server/constants.ts index 040a321978e17..67235ae3af16d 100644 --- a/src/plugins/data_views/server/constants.ts +++ b/src/plugins/data_views/server/constants.ts @@ -93,3 +93,8 @@ export const INITIAL_REST_VERSION = '2023-10-31'; */ export const INITIAL_REST_VERSION_INTERNAL = '1'; + +/** + * Default field caps cache max-age in seconds + */ +export const DEFAULT_FIELD_CACHE_FRESHNESS = 5; diff --git a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts index fdf3f1afe7c44..1ca5a6a734470 100644 --- a/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data_views/server/fetcher/lib/field_capabilities/field_capabilities.ts @@ -60,7 +60,7 @@ export async function getFieldCapabilities(params: FieldCapabilitiesParams) { const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) // not all meta fields are provided, so remove and manually add .filter((name) => !fieldsFromFieldCapsByName[name].metadata_field) - .concat(fieldCapsArr.length ? metaFields : []) + .concat(fieldCapsArr.length ? metaFields : []) // empty field lists should stay empty .reduce<{ names: string[]; map: Map }>( (agg, value) => { // This is intentionally using a Map to be highly optimized with very large indexes AND be safe for user provided data diff --git a/src/plugins/data_views/server/index.ts b/src/plugins/data_views/server/index.ts index eba0a83a8df49..d8382046183a3 100644 --- a/src/plugins/data_views/server/index.ts +++ b/src/plugins/data_views/server/index.ts @@ -46,6 +46,12 @@ const configSchema = schema.object({ schema.boolean({ defaultValue: false }), schema.never() ), + fieldListCachingEnabled: schema.conditional( + schema.contextRef('serverless'), + true, + schema.boolean({ defaultValue: false }), + schema.boolean({ defaultValue: true }) + ), }); type ConfigType = TypeOf; diff --git a/src/plugins/data_views/server/plugin.ts b/src/plugins/data_views/server/plugin.ts index bbcc5dafc81c2..2da7d128b837a 100644 --- a/src/plugins/data_views/server/plugin.ts +++ b/src/plugins/data_views/server/plugin.ts @@ -23,6 +23,7 @@ import { DataViewsServerPluginStartDependencies, } from './types'; import { DataViewsStorage } from './content_management'; +import { cacheMaxAge } from './ui_settings'; export class DataViewsServerPlugin implements @@ -46,14 +47,21 @@ export class DataViewsServerPlugin ) { core.savedObjects.registerType(dataViewSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); + + const config = this.initializerContext.config.get(); + + if (config.fieldListCachingEnabled) { + core.uiSettings.register(cacheMaxAge); + } + const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi'); - registerRoutes( - core.http, - core.getStartServices, - () => this.rollupsEnabled, - dataViewRestCounter - ); + registerRoutes({ + http: core.http, + getStartServices: core.getStartServices, + isRollupsEnabled: () => this.rollupsEnabled, + dataViewRestCounter, + }); expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); registerIndexPatternsUsageCollector(core.getStartServices, usageCollection); diff --git a/src/plugins/data_views/server/rest_api_routes/internal/fields.ts b/src/plugins/data_views/server/rest_api_routes/internal/fields.ts new file mode 100644 index 0000000000000..cee8efc8b6d1d --- /dev/null +++ b/src/plugins/data_views/server/rest_api_routes/internal/fields.ts @@ -0,0 +1,146 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { createHash } from 'crypto'; +import { IRouter, RequestHandler, StartServicesAccessor } from '@kbn/core/server'; +import { unwrapEtag } from '../../../common/utils'; +import { IndexPatternsFetcher } from '../../fetcher'; +import type { + DataViewsServerPluginStart, + DataViewsServerPluginStartDependencies, +} from '../../types'; +import type { FieldDescriptorRestResponse } from '../route_types'; +import { FIELDS_PATH as path } from '../../../common/constants'; +import { parseFields, IBody, IQuery, querySchema, validate } from './fields_for'; +import { DEFAULT_FIELD_CACHE_FRESHNESS } from '../../constants'; + +export function calculateHash(srcBuffer: Buffer) { + const hash = createHash('sha1'); + hash.update(srcBuffer); + return hash.digest('hex'); +} + +const handler: (isRollupsEnabled: () => boolean) => RequestHandler<{}, IQuery, IBody> = + (isRollupsEnabled) => async (context, request, response) => { + const core = await context.core; + const uiSettings = core.uiSettings.client; + const { asCurrentUser } = core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser, undefined, isRollupsEnabled()); + const { + pattern, + meta_fields: metaFields, + type, + rollup_index: rollupIndex, + allow_no_index: allowNoIndex, + include_unmapped: includeUnmapped, + } = request.query; + + let parsedFields: string[] = []; + let parsedMetaFields: string[] = []; + try { + parsedMetaFields = parseFields(metaFields); + parsedFields = parseFields(request.query.fields ?? []); + } catch (error) { + return response.badRequest(); + } + + try { + const { fields, indices } = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedMetaFields, + type, + rollupIndex, + fieldCapsOptions: { + allow_no_indices: allowNoIndex || false, + includeUnmapped, + }, + ...(parsedFields.length > 0 ? { fields: parsedFields } : {}), + }); + + const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = { + fields, + indices, + }; + + const bodyAsString = JSON.stringify(body); + + const etag = calculateHash(Buffer.from(bodyAsString)); + + const headers: Record = { + 'content-type': 'application/json', + etag, + vary: 'accept-encoding, user-hash', + }; + + // field cache is configurable in classic environment but not on serverless + let cacheMaxAge = DEFAULT_FIELD_CACHE_FRESHNESS; + const cacheMaxAgeSetting = await uiSettings.get( + 'data_views:cache_max_age' + ); + if (cacheMaxAgeSetting !== undefined) { + cacheMaxAge = cacheMaxAgeSetting; + } + + if (cacheMaxAge && fields.length) { + const stale = 365 * 24 * 60 * 60 - cacheMaxAge; + headers[ + 'cache-control' + ] = `private, max-age=${cacheMaxAge}, stale-while-revalidate=${stale}`; + } else { + headers['cache-control'] = 'private, no-cache'; + } + + const ifNoneMatch = request.headers['if-none-match']; + const ifNoneMatchString = Array.isArray(ifNoneMatch) ? ifNoneMatch[0] : ifNoneMatch; + + if (ifNoneMatchString) { + const requestHash = unwrapEtag(ifNoneMatchString); + if (etag === requestHash) { + return response.notModified({ headers }); + } + } + + return response.ok({ + body: bodyAsString, + headers, + }); + } catch (error) { + if ( + typeof error === 'object' && + !!error?.isBoom && + !!error?.output?.payload && + typeof error?.output?.payload === 'object' + ) { + const payload = error?.output?.payload; + return response.notFound({ + body: { + message: payload.message, + attributes: payload, + }, + }); + } else { + return response.notFound(); + } + } + }; + +export const registerFields = async ( + router: IRouter, + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >, + isRollupsEnabled: () => boolean +) => { + router.versioned + .get({ path, access: 'internal', enableQueryVersion: true }) + .addVersion( + { version: '1', validate: { request: { query: querySchema }, response: validate.response } }, + handler(isRollupsEnabled) + ); +}; diff --git a/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts b/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts index 7d39b41d5caee..3c89cdef17955 100644 --- a/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts +++ b/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts @@ -41,8 +41,8 @@ export const parseFields = (fields: string | string[]): string[] => { const access = 'internal'; -type IBody = { index_filter?: estypes.QueryDslQueryContainer } | undefined; -interface IQuery { +export type IBody = { index_filter?: estypes.QueryDslQueryContainer } | undefined; +export interface IQuery { pattern: string; meta_fields: string | string[]; type?: string; @@ -53,7 +53,7 @@ interface IQuery { allow_hidden?: boolean; } -const querySchema = schema.object({ +export const querySchema = schema.object({ pattern: schema.string(), meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { defaultValue: [], @@ -97,7 +97,7 @@ const FieldDescriptorSchema = schema.object({ ), }); -const validate: FullValidationConfig = { +export const validate: FullValidationConfig = { request: { query: querySchema, // not available to get request diff --git a/src/plugins/data_views/server/routes.ts b/src/plugins/data_views/server/routes.ts index c77dd9a8236cf..d6ee36927ff80 100644 --- a/src/plugins/data_views/server/routes.ts +++ b/src/plugins/data_views/server/routes.ts @@ -14,21 +14,30 @@ import type { DataViewsServerPluginStart, DataViewsServerPluginStartDependencies import { registerExistingIndicesPath } from './rest_api_routes/internal/existing_indices'; import { registerFieldForWildcard } from './rest_api_routes/internal/fields_for'; import { registerHasDataViewsRoute } from './rest_api_routes/internal/has_data_views'; +import { registerFields } from './rest_api_routes/internal/fields'; -export function registerRoutes( - http: HttpServiceSetup, +interface RegisterRoutesArgs { + http: HttpServiceSetup; getStartServices: StartServicesAccessor< DataViewsServerPluginStartDependencies, DataViewsServerPluginStart - >, - isRollupsEnabled: () => boolean, - dataViewRestCounter?: UsageCounter -) { + >; + isRollupsEnabled: () => boolean; + dataViewRestCounter?: UsageCounter; +} + +export function registerRoutes({ + http, + getStartServices, + dataViewRestCounter, + isRollupsEnabled, +}: RegisterRoutesArgs) { const router = http.createRouter(); routes.forEach((route) => route(router, getStartServices, dataViewRestCounter)); registerExistingIndicesPath(router); registerFieldForWildcard(router, getStartServices, isRollupsEnabled); + registerFields(router, getStartServices, isRollupsEnabled); registerHasDataViewsRoute(router); } diff --git a/src/plugins/data_views/server/ui_settings.ts b/src/plugins/data_views/server/ui_settings.ts new file mode 100644 index 0000000000000..3ba489a4b6b28 --- /dev/null +++ b/src/plugins/data_views/server/ui_settings.ts @@ -0,0 +1,25 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { DEFAULT_FIELD_CACHE_FRESHNESS } from './constants'; + +export const cacheMaxAge = { + 'data_views:cache_max_age': { + name: i18n.translate('dataViews.advancedSettings.cacheMaxAgeTitle', { + defaultMessage: 'Field cache max age (in seconds)', + }), + value: DEFAULT_FIELD_CACHE_FRESHNESS, + description: i18n.translate('dataViews.advancedSettings.cacheMaxAgeText', { + defaultMessage: + 'Sets how long data view fields API requests are cached in seconds. A value of 0 turns off caching. Modifying this value may not take immediate effect, users need to clear browser cache or wait until the current cache expires. To see immediate changes, try a hard reload of Kibana.', + }), + schema: schema.number(), + }, +}; diff --git a/src/plugins/data_views/tsconfig.json b/src/plugins/data_views/tsconfig.json index e5613323bc222..1d414cb98ee95 100644 --- a/src/plugins/data_views/tsconfig.json +++ b/src/plugins/data_views/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/object-versioning", "@kbn/core-saved-objects-server", "@kbn/logging", + "@kbn/security-plugin-types-public", ], "exclude": [ "target/**/*", diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 5e49b09b01d24..32b133f210dad 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -166,6 +166,13 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'data_views:cache_max_age': { + type: 'long', + _meta: { + description: + "Sets the 'max-age' cache header value for data view fields API requests. A value of 0 will disable caching.", + }, + }, 'discover:searchOnPageLoad': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index a1125a6d118b8..0832675cd54cf 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -81,6 +81,7 @@ export interface UsageStats { 'doc_table:highlight': boolean; 'discover:searchOnPageLoad': boolean; 'doc_table:hideTimeColumn': boolean; + 'data_views:cache_max_age': number; 'discover:sampleSize': number; 'discover:sampleRowsPerPage': number; defaultColumns: string[]; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 7e5a19d41d3d3..7fbf4ad7aeeb3 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9434,6 +9434,12 @@ "description": "Non-default value of setting." } }, + "data_views:cache_max_age": { + "type": "long", + "_meta": { + "description": "Sets the 'max-age' cache header value for data view fields API requests. A value of 0 will disable caching." + } + }, "discover:searchOnPageLoad": { "type": "boolean", "_meta": { diff --git a/test/api_integration/apis/data_views/fields_route/cache.ts b/test/api_integration/apis/data_views/fields_route/cache.ts new file mode 100644 index 0000000000000..c97be4b7411f3 --- /dev/null +++ b/test/api_integration/apis/data_views/fields_route/cache.ts @@ -0,0 +1,93 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('cache headers', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + + it('are present', async () => { + const response = await supertest.get(FIELDS_PATH).query({ + pattern: '*', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }); + + const cacheControlHeader = response.get('cache-control'); + + expect(cacheControlHeader).to.contain('private'); + expect(cacheControlHeader).to.contain('max-age'); + expect(cacheControlHeader).to.contain('stale-while-revalidate'); + expect(response.get('vary')).to.equal('accept-encoding, user-hash'); + expect(response.get('etag')).to.not.be.empty(); + }); + + it('no-cache when data_views:cache_max_age set to zero', async () => { + await kibanaServer.uiSettings.update({ 'data_views:cache_max_age': 0 }); + + const response = await supertest.get(FIELDS_PATH).query({ + pattern: 'b*', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }); + + const cacheControlHeader = response.get('cache-control'); + + expect(cacheControlHeader).to.contain('private'); + expect(cacheControlHeader).to.contain('no-cache'); + expect(cacheControlHeader).to.not.contain('max-age'); + expect(cacheControlHeader).to.not.contain('stale-while-revalidate'); + expect(response.get('vary')).to.equal('accept-encoding, user-hash'); + expect(response.get('etag')).to.not.be.empty(); + + kibanaServer.uiSettings.replace({ 'data_views:cache_max_age': 5 }); + }); + + it('returns 304 on matching etag', async () => { + const response = await supertest.get(FIELDS_PATH).query({ + pattern: '*', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }); + + await supertest + .get(FIELDS_PATH) + .set('If-None-Match', response.get('etag')) + .query({ + pattern: '*', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(304); + }); + + it('handles empty field lists', async () => { + const response = await supertest.get(FIELDS_PATH).query({ + pattern: 'xyz', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + allow_no_index: true, + }); + + expect(response.body.fields).to.be.empty(); + }); + }); +} diff --git a/test/api_integration/apis/data_views/fields_route/conflicts.ts b/test/api_integration/apis/data_views/fields_route/conflicts.ts new file mode 100644 index 0000000000000..026ca58b6dba4 --- /dev/null +++ b/test/api_integration/apis/data_views/fields_route/conflicts.ts @@ -0,0 +1,79 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('conflicts', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/conflicts') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/conflicts') + ); + + it('flags fields with mismatched types as conflicting', () => + supertest + .get(FIELDS_PATH) + .query({ pattern: 'logs-*', apiVersion: INITIAL_REST_VERSION_INTERNAL }) + .expect(200) + .then((resp) => { + expect(resp.body).to.eql({ + fields: [ + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'number_conflict', + type: 'number', + esTypes: ['float', 'integer'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'string_conflict', + type: 'string', + esTypes: ['keyword', 'text'], + aggregatable: true, + searchable: true, + readFromDocValues: true, + metadata_field: false, + }, + { + name: 'success', + type: 'conflict', + esTypes: ['keyword', 'boolean'], + aggregatable: true, + searchable: true, + readFromDocValues: false, + conflictDescriptions: { + boolean: ['logs-2017.01.02'], + keyword: ['logs-2017.01.01'], + }, + metadata_field: false, + }, + ], + indices: ['logs-2017.01.01', 'logs-2017.01.02'], + }); + })); + }); +} diff --git a/test/api_integration/apis/data_views/fields_route/index.ts b/test/api_integration/apis/data_views/fields_route/index.ts new file mode 100644 index 0000000000000..4b03c2b58afbe --- /dev/null +++ b/test/api_integration/apis/data_views/fields_route/index.ts @@ -0,0 +1,18 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('data_views/fields route', () => { + loadTestFile(require.resolve('./params')); + loadTestFile(require.resolve('./conflicts')); + loadTestFile(require.resolve('./response')); + loadTestFile(require.resolve('./cache')); + }); +} diff --git a/test/api_integration/apis/data_views/fields_route/params.ts b/test/api_integration/apis/data_views/fields_route/params.ts new file mode 100644 index 0000000000000..7d111936426bb --- /dev/null +++ b/test/api_integration/apis/data_views/fields_route/params.ts @@ -0,0 +1,148 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_PATH } from '@kbn/data-views-plugin/common/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const randomness = getService('randomness'); + + describe('params', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + + it('requires a pattern query param', () => + supertest + .get(FIELDS_PATH) + .query({ + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(400)); + + it('accepts include_unmapped param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + include_unmapped: true, + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('rejects unexpected query params', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: randomness.word(), + [randomness.word()]: randomness.word(), + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(400)); + + describe('fields', () => { + it('accepts a JSON formatted fields query param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + fields: JSON.stringify(['baz']), + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('accepts meta_fields query param in string array', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + fields: ['baz', 'foo'], + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('accepts single array fields query param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + fields: ['baz'], + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('accepts single fields query param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + fields: 'baz', + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('rejects a comma-separated list of fields', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + fields: 'foo,bar', + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(400)); + }); + + describe('meta_fields', () => { + it('accepts a JSON formatted meta_fields query param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + meta_fields: JSON.stringify(['meta']), + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('accepts meta_fields query param in string array', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + meta_fields: ['_id', 'meta'], + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('accepts single meta_fields query param', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + meta_fields: ['_id'], + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200)); + + it('rejects a comma-separated list of meta_fields', () => + supertest + .get(FIELDS_PATH) + .query({ + pattern: '*', + meta_fields: 'foo,bar', + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(400)); + }); + }); +} diff --git a/test/api_integration/apis/data_views/fields_route/response.ts b/test/api_integration/apis/data_views/fields_route/response.ts new file mode 100644 index 0000000000000..880206f6fb498 --- /dev/null +++ b/test/api_integration/apis/data_views/fields_route/response.ts @@ -0,0 +1,230 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { FIELDS_PATH } from '@kbn/data-views-plugin/common/constants'; +import expect from '@kbn/expect'; +import { sortBy } from 'lodash'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + const ensureFieldsAreSorted = (resp: { body: { fields: { name: string } } }) => { + expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); + }; + + const testFields = [ + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + metadata_field: false, + }, + { + type: 'string', + esTypes: ['text'], + searchable: true, + aggregatable: false, + name: 'baz', + readFromDocValues: false, + metadata_field: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + metadata_field: false, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + metadata_field: false, + }, + ]; + + describe('fields route response', () => { + before(() => + esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + after(() => + esArchiver.unload('test/api_integration/fixtures/es_archiver/index_patterns/basic_index') + ); + + it('returns a flattened version of the fields in es', async () => { + await supertest + .get(FIELDS_PATH) + .query({ pattern: 'basic_index', apiVersion: INITIAL_REST_VERSION_INTERNAL }) + .expect(200, { + fields: testFields, + indices: ['basic_index'], + }) + .then(ensureFieldsAreSorted); + }); + + it('returns a single field as requested', async () => { + await supertest + .get(FIELDS_PATH) + .query({ + pattern: 'basic_index', + fields: JSON.stringify(['bar']), + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200, { + fields: [testFields[0]], + indices: ['basic_index'], + }); + }); + + it('always returns a field for all passed meta fields', async () => { + await supertest + .get(FIELDS_PATH) + .query({ + pattern: 'basic_index', + meta_fields: JSON.stringify(['_id', '_source', 'crazy_meta_field']), + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(200, { + fields: [ + { + aggregatable: false, + name: '_id', + esTypes: ['_id'], + readFromDocValues: false, + searchable: true, + type: 'string', + metadata_field: true, + }, + { + aggregatable: false, + name: '_source', + esTypes: ['_source'], + readFromDocValues: false, + searchable: false, + type: '_source', + metadata_field: true, + }, + { + type: 'boolean', + esTypes: ['boolean'], + searchable: true, + aggregatable: true, + name: 'bar', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: false, + name: 'baz', + esTypes: ['text'], + readFromDocValues: false, + searchable: true, + type: 'string', + metadata_field: false, + }, + { + type: 'string', + esTypes: ['keyword'], + searchable: true, + aggregatable: true, + name: 'baz.keyword', + readFromDocValues: true, + subType: { multi: { parent: 'baz' } }, + metadata_field: false, + }, + { + aggregatable: false, + name: 'crazy_meta_field', + readFromDocValues: false, + searchable: false, + type: 'string', + metadata_field: true, + }, + { + type: 'number', + esTypes: ['long'], + searchable: true, + aggregatable: true, + name: 'foo', + readFromDocValues: true, + metadata_field: false, + }, + { + aggregatable: true, + esTypes: ['keyword'], + name: 'nestedField.child', + readFromDocValues: true, + searchable: true, + subType: { + nested: { + path: 'nestedField', + }, + }, + type: 'string', + metadata_field: false, + }, + ], + indices: ['basic_index'], + }) + .then(ensureFieldsAreSorted); + }); + + it('returns fields when one pattern exists and the other does not', async () => { + await supertest + .get(FIELDS_PATH) + .query({ pattern: 'bad_index,basic_index', apiVersion: INITIAL_REST_VERSION_INTERNAL }) + .expect(200, { + fields: testFields, + indices: ['basic_index'], + }); + }); + + it('returns 404 when neither exists', async () => { + await supertest + .get(FIELDS_PATH) + .query({ pattern: 'bad_index,bad_index_2', apiVersion: INITIAL_REST_VERSION_INTERNAL }) + .expect(404); + }); + + it('returns 404 when no patterns exist', async () => { + await supertest + .get(FIELDS_PATH) + .query({ + pattern: 'bad_index', + apiVersion: INITIAL_REST_VERSION_INTERNAL, + }) + .expect(404); + }); + }); +} diff --git a/test/api_integration/apis/data_views/index.ts b/test/api_integration/apis/data_views/index.ts index a6589d5680b3d..654421937f44b 100644 --- a/test/api_integration/apis/data_views/index.ts +++ b/test/api_integration/apis/data_views/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./has_user_index_pattern')); loadTestFile(require.resolve('./swap_references')); loadTestFile(require.resolve('./resolve_index')); + loadTestFile(require.resolve('./fields_route')); }); } diff --git a/test/functional/apps/management/data_views/_cache.ts b/test/functional/apps/management/data_views/_cache.ts new file mode 100644 index 0000000000000..447e44052bb8d --- /dev/null +++ b/test/functional/apps/management/data_views/_cache.ts @@ -0,0 +1,26 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['settings', 'common', 'header']); + const find = getService('find'); + + describe('Data view field caps cache advanced setting', async function () { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + }); + it('should have cache setting', async () => { + const cacheSetting = await find.byCssSelector('#data_views\\:cache_max_age-group'); + expect(cacheSetting).to.not.be(undefined); + }); + }); +} diff --git a/test/functional/apps/management/data_views/_field_formatter.ts b/test/functional/apps/management/data_views/_field_formatter.ts index fb016151cce5f..dfcdc0b877581 100644 --- a/test/functional/apps/management/data_views/_field_formatter.ts +++ b/test/functional/apps/management/data_views/_field_formatter.ts @@ -465,6 +465,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternByName(indexTitle); + await PageObjects.settings.refreshDataViewFieldList(); }); afterEach(async () => { diff --git a/test/functional/apps/management/data_views/_index_pattern_filter.ts b/test/functional/apps/management/data_views/_index_pattern_filter.ts index 81ff2b450755d..941451424dfb1 100644 --- a/test/functional/apps/management/data_views/_index_pattern_filter.ts +++ b/test/functional/apps/management/data_views/_index_pattern_filter.ts @@ -170,6 +170,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.refreshDataViewFieldList(); + await testSubjects.existOrFail('dataViewMappingConflict'); expect(await PageObjects.settings.getFieldTypes()).to.eql([ diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 85de9b5de0926..808a4aaccc9c2 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -47,5 +47,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./data_views/_edit_field')); loadTestFile(require.resolve('./_files')); loadTestFile(require.resolve('./_data_view_field_filters')); + loadTestFile(require.resolve('./data_views/_cache')); }); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index cd650086e9e1a..9fc802a03cc45 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -494,6 +494,25 @@ export class SettingsPageObject extends FtrService { await customDataViewIdInput.type(value); } + async refreshDataViewFieldList(dataViewName?: string) { + if (dataViewName) { + await this.common.navigateToApp('management/kibana/dataViews'); + await this.header.waitUntilLoadingHasFinished(); + await this.testSubjects.click(`detail-link-${dataViewName}`); + } + await this.testSubjects.click('refreshDataViewButton'); + + // wait for refresh to start + await new Promise((r) => setTimeout(r, 500)); + + // wait for refresh to finish + await this.retry.try(async () => { + const btn = await this.testSubjects.find('refreshDataViewButton'); + const disabled = await btn.getAttribute('disabled'); + expect(disabled).to.be(null); + }); + } + async allowHiddenClick() { await this.testSubjects.click('toggleAdvancedSetting'); const allowHiddenField = await this.testSubjects.find('allowHiddenField'); diff --git a/x-pack/plugins/logs_shared/server/services/log_views/log_views_client.test.ts b/x-pack/plugins/logs_shared/server/services/log_views/log_views_client.test.ts index e50d08c630eb7..e2741bea55931 100644 --- a/x-pack/plugins/logs_shared/server/services/log_views/log_views_client.test.ts +++ b/x-pack/plugins/logs_shared/server/services/log_views/log_views_client.test.ts @@ -253,6 +253,7 @@ describe('LogViewsClient class', () => { "allowNoIndex": false, "deleteFieldFormat": [Function], "deleteScriptedFieldInternal": [Function], + "etag": undefined, "fieldAttrs": Object {}, "fieldFormatMap": Object {}, "fieldFormats": Object { @@ -276,6 +277,7 @@ describe('LogViewsClient class', () => { "fields": FldList [], "flattenHit": [Function], "getAllowHidden": [Function], + "getEtag": [Function], "getFieldAttrs": [Function], "getIndexPattern": [Function], "getName": [Function], @@ -301,6 +303,7 @@ describe('LogViewsClient class', () => { }, "scriptedFields": Array [], "setAllowHidden": [Function], + "setEtag": [Function], "setFieldFormat": [Function], "setIndexPattern": [Function], "shortDotsEnable": false, diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts index fb1285fb89f05..27458ef09e15b 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/form.cy.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { FIELDS_FOR_WILDCARD_PATH } from '@kbn/data-views-plugin/common/constants'; import { addEndpointResponseAction, fillUpNewRule, @@ -186,9 +185,7 @@ describe( }); it('All response action controls are disabled', () => { - cy.intercept('GET', `${FIELDS_FOR_WILDCARD_PATH}*`).as('getFieldsForWildcard'); visitRuleActions(ruleId); - cy.wait('@getFieldsForWildcard'); cy.getByTestSubj('edit-rule-actions-tab').click(); cy.getByTestSubj('response-actions-wrapper').within(() => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json index 8a38566931281..20cb7534941b3 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/public/management/cypress/tsconfig.json @@ -30,7 +30,6 @@ "@kbn/cases-plugin", "@kbn/test", "@kbn/repo-info", - "@kbn/data-views-plugin", "@kbn/tooling-log", ] } diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts index 6b668f75d56d3..bffa564fba774 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts @@ -399,6 +399,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should navigate to alert results via link provided in notification', async () => { + await PageObjects.settings.refreshDataViewFieldList(OUTPUT_DATA_VIEW); await openAlertResults(RULE_NAME); await checkInitialRuleParamsState(SOURCE_DATA_VIEW); }); diff --git a/x-pack/test/scalability/apis/api.fields.32_fields.json b/x-pack/test/scalability/apis/api.fields.32_fields.json new file mode 100644 index 0000000000000..03837b238e44e --- /dev/null +++ b/x-pack/test/scalability/apis/api.fields.32_fields.json @@ -0,0 +1,50 @@ +{ + "journeyName": "GET /internal/data_views/fields - 32 fields", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 5000, + "threshold2": 10000, + "threshold3": 20000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 10, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 10, + "maxUsersCount": 375, + "duration": "140s" + } + ], + "maxDuration": "5m" + }, + "testData": { + "esArchives": ["test/functional/fixtures/es_archiver/kibana_sample_data_flights"] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/internal/data_views/fields", + "query": "?pattern=kibana*&apiVersion=1", + "headers": { + "Cookie": "", + "Kbn-Version": "", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json", + "x-elastic-internal-origin": "kibana" + }, + "statusCode": 200 + } + } + ] + } + ] +} diff --git a/x-pack/test/scalability/apis/api.fields.6800_fields.json b/x-pack/test/scalability/apis/api.fields.6800_fields.json new file mode 100644 index 0000000000000..362eaa211acae --- /dev/null +++ b/x-pack/test/scalability/apis/api.fields.6800_fields.json @@ -0,0 +1,50 @@ +{ + "journeyName": "GET /internal/data_views/fields - 6800 fields", + "scalabilitySetup": { + "responseTimeThreshold": { + "threshold1": 5000, + "threshold2": 10000, + "threshold3": 20000 + }, + "warmup": [ + { + "action": "constantUsersPerSec", + "userCount": 1, + "duration": "30s" + } + ], + "test": [ + { + "action": "rampUsersPerSec", + "minUsersCount": 1, + "maxUsersCount": 7, + "duration": "140s" + } + ], + "maxDuration": "5m" + }, + "testData": { + "esArchives": ["test/functional/fixtures/es_archiver/many_fields"] + }, + "streams": [ + { + "requests": [ + { + "http": { + "method": "GET", + "path": "/internal/data_views/fields", + "query": "?pattern=indices*&apiVersion=1", + "headers": { + "Cookie": "", + "Kbn-Version": "", + "Accept-Encoding": "gzip, deflate, br", + "Content-Type": "application/json", + "x-elastic-internal-origin": "kibana" + }, + "statusCode": 200 + } + } + ] + } + ] +} diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index f683e02237996..d2e8f863d5fe9 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -378,9 +378,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // should not have data view selected by default const dataViewSelector = await testSubjects.find('selectDataViewExpression'); - // TODO: Serverless Security has an existing data view by default + // TODO: Serverless Security and Search have an existing data view by default const dataViewSelectorText = await dataViewSelector.getVisibleText(); - if (!dataViewSelectorText.includes('.alerts-security')) { + if ( + !dataViewSelectorText.includes('.alerts-security') && + !dataViewSelectorText.includes('default:all-data') + ) { expect(await dataViewSelector.getVisibleText()).to.eql('DATA VIEW\nSelect a data view'); } @@ -444,6 +447,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should navigate to alert results via link provided in notification', async () => { + await PageObjects.settings.refreshDataViewFieldList(OUTPUT_DATA_VIEW); await openAlertResults(RULE_NAME); await checkInitialRuleParamsState(SOURCE_DATA_VIEW); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_cache.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_cache.ts new file mode 100644 index 0000000000000..3050ef2456fbf --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_cache.ts @@ -0,0 +1,25 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['settings', 'common', 'header']); + const testSubjects = getService('testSubjects'); + + describe('Data view field caps cache advanced setting', async function () { + before(async () => { + await PageObjects.settings.navigateTo(); + }); + + it('should not have cache setting', async () => { + await testSubjects.missingOrFail( + 'advancedSetting-editField-data_views\\:cache_max_age-group' + ); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_index_pattern_filter.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_index_pattern_filter.ts index e5d51f55aa7c1..9b8a51feac10f 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/_index_pattern_filter.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/_index_pattern_filter.ts @@ -174,6 +174,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.refreshDataViewFieldList(); + await testSubjects.existOrFail('dataViewMappingConflict'); expect(await PageObjects.settings.getFieldTypes()).to.eql([ diff --git a/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts b/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts index 6a512b6d6e273..69fec71b2ad22 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/data_views/index.ts @@ -30,5 +30,6 @@ export default ({ getService, loadTestFile, getPageObject }: FtrProviderContext) loadTestFile(require.resolve('./_exclude_index_pattern')); loadTestFile(require.resolve('./_index_pattern_filter')); loadTestFile(require.resolve('./_edit_field')); + loadTestFile(require.resolve('./_cache')); }); }; diff --git a/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts index 3cf087eb1ae9b..e74262f60ac3e 100644 --- a/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/search/cases/attachment_framework.ts @@ -16,6 +16,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const svlSearchNavigation = getService('svlSearchNavigation'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const svlCommonPage = getPageObject('svlCommonPage'); + const settings = getPageObject('settings'); describe('persistable attachment', () => { before(async () => { @@ -33,6 +34,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { 'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' ); + await settings.refreshDataViewFieldList('default:all-data'); + await svlSearchNavigation.navigateToLandingPage(); await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' });