[IndexPatterns] No data experience to handle default Fleet assets (#108887)
Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
This commit is contained in:
parent
a8c2a43834
commit
cd7f26dd81
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [hasUserIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md)
|
||||
|
||||
## IndexPatternsService.hasUserIndexPattern() method
|
||||
|
||||
Checks if current user has a user created index pattern ignoring fleet's server default index patterns
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasUserIndexPattern(): Promise<boolean>;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<boolean>`
|
||||
|
|
@ -45,5 +45,6 @@ export declare class IndexPatternsService
|
|||
| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away |
|
||||
| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) | | Save a new index pattern |
|
||||
| [delete(indexPatternId)](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index |
|
||||
| [hasUserIndexPattern()](./kibana-plugin-plugins-data-public.indexpatternsservice.hasuserindexpattern.md) | | Checks if current user has a user created index pattern ignoring fleet's server default index patterns |
|
||||
| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts |
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [hasUserIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md)
|
||||
|
||||
## IndexPatternsService.hasUserIndexPattern() method
|
||||
|
||||
Checks if current user has a user created index pattern ignoring fleet's server default index patterns
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
hasUserIndexPattern(): Promise<boolean>;
|
||||
```
|
||||
<b>Returns:</b>
|
||||
|
||||
`Promise<boolean>`
|
||||
|
|
@ -45,5 +45,6 @@ export declare class IndexPatternsService
|
|||
| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-server.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away |
|
||||
| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-server.indexpatternsservice.createsavedobject.md) | | Save a new index pattern |
|
||||
| [delete(indexPatternId)](./kibana-plugin-plugins-data-server.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index |
|
||||
| [hasUserIndexPattern()](./kibana-plugin-plugins-data-server.indexpatternsservice.hasuserindexpattern.md) | | Checks if current user has a user created index pattern ignoring fleet's server default index patterns |
|
||||
| [updateSavedObject(indexPattern, saveAttempts, ignoreErrors)](./kibana-plugin-plugins-data-server.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts |
|
||||
|
||||
|
|
|
@ -15,3 +15,17 @@ export const RUNTIME_FIELD_TYPES = [
|
|||
'boolean',
|
||||
'geo_point',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Used to determine if the instance has any user created index patterns by filtering index patterns
|
||||
* that are created and backed only by Fleet server data
|
||||
* Should be revised after https://github.com/elastic/kibana/issues/82851 is fixed
|
||||
* For more background see: https://github.com/elastic/kibana/issues/107020
|
||||
*/
|
||||
export const FLEET_ASSETS_TO_IGNORE = {
|
||||
LOGS_INDEX_PATTERN: 'logs-*',
|
||||
METRICS_INDEX_PATTERN: 'metrics-*',
|
||||
LOGS_DATA_STREAM_TO_IGNORE: 'logs-elastic_agent', // ignore ds created by Fleet server itself
|
||||
METRICS_DATA_STREAM_TO_IGNORE: 'metrics-elastic_agent', // ignore ds created by Fleet server itself
|
||||
METRICS_ENDPOINT_INDEX_TO_IGNORE: 'metrics-endpoint.metadata_current_default', // ignore index created by Fleet endpoint package installed by default in Cloud
|
||||
};
|
||||
|
|
|
@ -35,8 +35,9 @@ export const createEnsureDefaultIndexPattern = (
|
|||
return;
|
||||
}
|
||||
|
||||
// If there is any index pattern created, set the first as default
|
||||
if (patterns.length >= 1) {
|
||||
// If there is any user index pattern created, set the first as default
|
||||
// if there is 0 patterns, then don't even call `hasUserIndexPattern()`
|
||||
if (patterns.length >= 1 && (await this.hasUserIndexPattern().catch(() => true))) {
|
||||
defaultId = patterns[0];
|
||||
await uiSettings.set('defaultIndex', defaultId);
|
||||
} else {
|
||||
|
|
|
@ -232,6 +232,13 @@ export class IndexPatternsService {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if current user has a user created index pattern ignoring fleet's server default index patterns
|
||||
*/
|
||||
async hasUserIndexPattern(): Promise<boolean> {
|
||||
return this.apiClient.hasUserIndexPattern();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field list by providing { pattern }
|
||||
* @param options
|
||||
|
|
|
@ -136,6 +136,7 @@ export interface GetFieldsOptionsTimePattern {
|
|||
export interface IIndexPatternsApiClient {
|
||||
getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise<any>;
|
||||
getFieldsForWildcard: (options: GetFieldsOptions) => Promise<any>;
|
||||
hasUserIndexPattern: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type { SavedObject };
|
||||
|
|
|
@ -23,7 +23,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient {
|
|||
this.http = http;
|
||||
}
|
||||
|
||||
private _request(url: string, query: any) {
|
||||
private _request(url: string, query?: any) {
|
||||
return this.http
|
||||
.fetch(url, {
|
||||
query,
|
||||
|
@ -62,4 +62,9 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient {
|
|||
allow_no_index: allowNoIndex,
|
||||
}).then((resp: any) => resp.fields || []);
|
||||
}
|
||||
|
||||
async hasUserIndexPattern(): Promise<boolean> {
|
||||
const response = await this._request(this._getUrl(['has_user_index_pattern']));
|
||||
return response.result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1479,6 +1479,7 @@ export class IndexPatternsService {
|
|||
getIds: (refresh?: boolean) => Promise<string[]>;
|
||||
getIdsWithTitle: (refresh?: boolean) => Promise<IndexPatternListItem[]>;
|
||||
getTitles: (refresh?: boolean) => Promise<string[]>;
|
||||
hasUserIndexPattern(): Promise<boolean>;
|
||||
refreshFields: (indexPattern: IndexPattern) => Promise<void>;
|
||||
savedObjectToSpec: (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec;
|
||||
setDefault: (id: string | null, force?: boolean) => Promise<void>;
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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 { hasUserIndexPattern } from './has_user_index_pattern';
|
||||
import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks';
|
||||
|
||||
describe('hasUserIndexPattern', () => {
|
||||
const esClient = elasticsearchServiceMock.createScopedClusterClient().asCurrentUser;
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
it('returns false when there are no index patterns', async () => {
|
||||
soClient.find.mockResolvedValue({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
});
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when there are any index patterns other than metrics-* or logs-*', async () => {
|
||||
soClient.find.mockResolvedValue({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
score: 99,
|
||||
attributes: { title: 'my-pattern-*' },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true);
|
||||
});
|
||||
|
||||
describe('when only metrics-* and logs-* index patterns exist', () => {
|
||||
beforeEach(() => {
|
||||
soClient.find.mockResolvedValue({
|
||||
page: 1,
|
||||
per_page: 100,
|
||||
total: 2,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
score: 99,
|
||||
attributes: { title: 'metrics-*' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
references: [],
|
||||
type: 'index-pattern',
|
||||
score: 99,
|
||||
attributes: { title: 'logs-*' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('calls indices.resolveIndex for the index patterns', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [],
|
||||
data_streams: [],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
await hasUserIndexPattern({ esClient, soClient });
|
||||
expect(esClient.indices.resolveIndex).toHaveBeenCalledWith({
|
||||
name: 'logs-*,metrics-*',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false if no logs or metrics data_streams exist', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [],
|
||||
data_streams: [],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true if any index exists', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [{ name: 'logs', attributes: [] }],
|
||||
data_streams: [],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if only metrics-elastic_agent data stream exists', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [],
|
||||
data_streams: [
|
||||
{
|
||||
name: 'metrics-elastic_agent',
|
||||
timestamp_field: '@timestamp',
|
||||
backing_indices: ['.ds-metrics-elastic_agent'],
|
||||
},
|
||||
],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false if only logs-elastic_agent data stream exists', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [],
|
||||
data_streams: [
|
||||
{
|
||||
name: 'logs-elastic_agent',
|
||||
timestamp_field: '@timestamp',
|
||||
backing_indices: ['.ds-logs-elastic_agent'],
|
||||
},
|
||||
],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false if only metrics-endpoint.metadata_current_default index exists', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [
|
||||
{
|
||||
name: 'metrics-endpoint.metadata_current_default',
|
||||
attributes: ['open'],
|
||||
},
|
||||
],
|
||||
aliases: [],
|
||||
data_streams: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true if any other data stream exists', async () => {
|
||||
esClient.indices.resolveIndex.mockReturnValue(
|
||||
elasticsearchServiceMock.createSuccessTransportRequestPromise({
|
||||
indices: [],
|
||||
data_streams: [
|
||||
{
|
||||
name: 'other',
|
||||
timestamp_field: '@timestamp',
|
||||
backing_indices: ['.ds-other'],
|
||||
},
|
||||
],
|
||||
aliases: [],
|
||||
})
|
||||
);
|
||||
expect(await hasUserIndexPattern({ esClient, soClient })).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient, SavedObjectsClientContract } from '../../../../core/server';
|
||||
import { IndexPatternSavedObjectAttrs } from '../../common/index_patterns/index_patterns';
|
||||
import { FLEET_ASSETS_TO_IGNORE } from '../../common/index_patterns/constants';
|
||||
|
||||
interface Deps {
|
||||
esClient: ElasticsearchClient;
|
||||
soClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
export const hasUserIndexPattern = async ({ esClient, soClient }: Deps): Promise<boolean> => {
|
||||
const indexPatterns = await soClient.find<IndexPatternSavedObjectAttrs>({
|
||||
type: 'index-pattern',
|
||||
fields: ['title'],
|
||||
search: `*`,
|
||||
searchFields: ['title'],
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
if (indexPatterns.total === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are any index patterns that are not the default metrics-* and logs-* ones created by Fleet,
|
||||
// assume there are user created index patterns
|
||||
if (
|
||||
indexPatterns.saved_objects.some(
|
||||
(ip) =>
|
||||
ip.attributes.title !== FLEET_ASSETS_TO_IGNORE.METRICS_INDEX_PATTERN &&
|
||||
ip.attributes.title !== FLEET_ASSETS_TO_IGNORE.LOGS_INDEX_PATTERN
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const resolveResponse = await esClient.indices.resolveIndex({
|
||||
name: `${FLEET_ASSETS_TO_IGNORE.LOGS_INDEX_PATTERN},${FLEET_ASSETS_TO_IGNORE.METRICS_INDEX_PATTERN}`,
|
||||
});
|
||||
|
||||
const hasAnyNonDefaultFleetIndices = resolveResponse.body.indices.some(
|
||||
(ds) => ds.name !== FLEET_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE
|
||||
);
|
||||
|
||||
if (hasAnyNonDefaultFleetIndices) return true;
|
||||
|
||||
const hasAnyNonDefaultFleetDataStreams = resolveResponse.body.data_streams.some(
|
||||
(ds) =>
|
||||
ds.name !== FLEET_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE &&
|
||||
ds.name !== FLEET_ASSETS_TO_IGNORE.LOGS_DATA_STREAM_TO_IGNORE
|
||||
);
|
||||
|
||||
if (hasAnyNonDefaultFleetDataStreams) return true;
|
||||
|
||||
return false;
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ElasticsearchClient } from 'kibana/server';
|
||||
import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server';
|
||||
import {
|
||||
GetFieldsOptions,
|
||||
IIndexPatternsApiClient,
|
||||
|
@ -14,10 +14,14 @@ import {
|
|||
} from '../../common/index_patterns/types';
|
||||
import { IndexPatternMissingIndices } from '../../common/index_patterns/lib';
|
||||
import { IndexPatternsFetcher } from './fetcher';
|
||||
import { hasUserIndexPattern } from './has_user_index_pattern';
|
||||
|
||||
export class IndexPatternsApiServer implements IIndexPatternsApiClient {
|
||||
esClient: ElasticsearchClient;
|
||||
constructor(elasticsearchClient: ElasticsearchClient) {
|
||||
constructor(
|
||||
elasticsearchClient: ElasticsearchClient,
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract
|
||||
) {
|
||||
this.esClient = elasticsearchClient;
|
||||
}
|
||||
async getFieldsForWildcard({
|
||||
|
@ -50,4 +54,11 @@ export class IndexPatternsApiServer implements IIndexPatternsApiClient {
|
|||
const indexPatterns = new IndexPatternsFetcher(this.esClient);
|
||||
return await indexPatterns.getFieldsForTimePattern(options);
|
||||
}
|
||||
|
||||
async hasUserIndexPattern() {
|
||||
return hasUserIndexPattern({
|
||||
esClient: this.esClient,
|
||||
soClient: this.savedObjectsClient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export const indexPatternsServiceFactory = ({
|
|||
return new IndexPatternsCommonService({
|
||||
uiSettings: new UiSettingsServerToCommon(uiSettingsClient),
|
||||
savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient),
|
||||
apiClient: new IndexPatternsApiServer(elasticsearchClient),
|
||||
apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient),
|
||||
fieldFormats: formats,
|
||||
onError: (error) => {
|
||||
logger.error(error);
|
||||
|
|
|
@ -26,6 +26,7 @@ import { registerGetRuntimeFieldRoute } from './routes/runtime_fields/get_runtim
|
|||
import { registerDeleteRuntimeFieldRoute } from './routes/runtime_fields/delete_runtime_field';
|
||||
import { registerPutRuntimeFieldRoute } from './routes/runtime_fields/put_runtime_field';
|
||||
import { registerUpdateRuntimeFieldRoute } from './routes/runtime_fields/update_runtime_field';
|
||||
import { registerHasUserIndexPatternRoute } from './routes/has_user_index_pattern';
|
||||
|
||||
export function registerRoutes(
|
||||
http: HttpServiceSetup,
|
||||
|
@ -49,6 +50,7 @@ export function registerRoutes(
|
|||
registerDeleteIndexPatternRoute(router, getStartServices);
|
||||
registerUpdateIndexPatternRoute(router, getStartServices);
|
||||
registerManageDefaultIndexPatternRoutes(router, getStartServices);
|
||||
registerHasUserIndexPatternRoute(router, getStartServices);
|
||||
|
||||
// Fields API
|
||||
registerUpdateFieldsRoute(router, getStartServices);
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { handleErrors } from './util/handle_errors';
|
||||
import { IRouter, StartServicesAccessor } from '../../../../../core/server';
|
||||
import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
|
||||
|
||||
export const registerHasUserIndexPatternRoute = (
|
||||
router: IRouter,
|
||||
getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/index_patterns/has_user_index_pattern',
|
||||
validate: {},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
handleErrors(async (ctx, req, res) => {
|
||||
const savedObjectsClient = ctx.core.savedObjects.client;
|
||||
const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser;
|
||||
const [, , { indexPatterns }] = await getStartServices();
|
||||
const indexPatternsService = await indexPatterns.indexPatternsServiceFactory(
|
||||
savedObjectsClient,
|
||||
elasticsearchClient
|
||||
);
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
result: await indexPatternsService.hasUserIndexPattern(),
|
||||
},
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -528,6 +528,7 @@ class IndexPatternsService {
|
|||
// Warning: (ae-forgotten-export) The symbol "IndexPatternListItem" needs to be exported by the entry point index.d.ts
|
||||
getIdsWithTitle: (refresh?: boolean) => Promise<IndexPatternListItem[]>;
|
||||
getTitles: (refresh?: boolean) => Promise<string[]>;
|
||||
hasUserIndexPattern(): Promise<boolean>;
|
||||
refreshFields: (indexPattern: IndexPattern) => Promise<void>;
|
||||
savedObjectToSpec: (savedObject: SavedObject_2<IndexPatternAttributes>) => IndexPatternSpec;
|
||||
setDefault: (id: string | null, force?: boolean) => Promise<void>;
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { isUserDataIndex } from './empty_prompts';
|
||||
import { MatchedItem, ResolveIndexResponseItemIndexAttrs } from '../../types';
|
||||
|
||||
describe('isUserDataIndex', () => {
|
||||
test('system index is not data index', () => {
|
||||
const systemIndexes: MatchedItem[] = [
|
||||
{
|
||||
name: '.apm-agent-configuration',
|
||||
tags: [
|
||||
{
|
||||
key: 'index',
|
||||
name: 'Index',
|
||||
color: 'default',
|
||||
},
|
||||
],
|
||||
item: {
|
||||
name: '.apm-agent-configuration',
|
||||
attributes: [ResolveIndexResponseItemIndexAttrs.OPEN],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '.kibana',
|
||||
tags: [
|
||||
{
|
||||
key: 'alias',
|
||||
name: 'Alias',
|
||||
color: 'default',
|
||||
},
|
||||
],
|
||||
item: {
|
||||
name: '.kibana',
|
||||
indices: ['.kibana_8.0.0_001'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(systemIndexes.some(isUserDataIndex)).toBe(false);
|
||||
});
|
||||
|
||||
test('data index is data index', () => {
|
||||
const dataIndex: MatchedItem = {
|
||||
name: 'kibana_sample_data_ecommerce',
|
||||
tags: [
|
||||
{
|
||||
key: 'index',
|
||||
name: 'Index',
|
||||
color: 'default',
|
||||
},
|
||||
],
|
||||
item: {
|
||||
name: 'kibana_sample_data_ecommerce',
|
||||
attributes: [ResolveIndexResponseItemIndexAttrs.OPEN],
|
||||
},
|
||||
};
|
||||
|
||||
expect(isUserDataIndex(dataIndex)).toBe(true);
|
||||
});
|
||||
|
||||
test('fleet asset is not data index', () => {
|
||||
const fleetAssetIndex: MatchedItem = {
|
||||
name: 'logs-elastic_agent',
|
||||
tags: [
|
||||
{
|
||||
key: 'data_stream',
|
||||
name: 'Data stream',
|
||||
color: 'primary',
|
||||
},
|
||||
],
|
||||
item: {
|
||||
name: 'logs-elastic_agent',
|
||||
backing_indices: ['.ds-logs-elastic_agent-2021.08.18-000001'],
|
||||
timestamp_field: '@timestamp',
|
||||
},
|
||||
};
|
||||
|
||||
expect(isUserDataIndex(fleetAssetIndex)).toBe(false);
|
||||
});
|
||||
|
||||
test('metrics-endpoint.metadata_current_default is not data index', () => {
|
||||
const fleetAssetIndex: MatchedItem = {
|
||||
name: 'metrics-endpoint.metadata_current_default',
|
||||
tags: [{ key: 'index', name: 'Index', color: 'default' }],
|
||||
item: {
|
||||
name: 'metrics-endpoint.metadata_current_default',
|
||||
attributes: [ResolveIndexResponseItemIndexAttrs.OPEN],
|
||||
},
|
||||
};
|
||||
expect(isUserDataIndex(fleetAssetIndex)).toBe(false);
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useCallback, FC } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { useKibana } from '../../shared_imports';
|
||||
|
||||
|
@ -17,6 +18,7 @@ import { getIndices } from '../../lib';
|
|||
import { EmptyIndexListPrompt } from './empty_index_list_prompt';
|
||||
import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt';
|
||||
import { PromptFooter } from './prompt_footer';
|
||||
import { FLEET_ASSETS_TO_IGNORE } from '../../../../data/common';
|
||||
|
||||
const removeAliases = (item: MatchedItem) =>
|
||||
!((item as unknown) as ResolveIndexResponseItemAlias).indices;
|
||||
|
@ -24,25 +26,33 @@ const removeAliases = (item: MatchedItem) =>
|
|||
interface Props {
|
||||
onCancel: () => void;
|
||||
allSources: MatchedItem[];
|
||||
hasExistingIndexPatterns: boolean;
|
||||
loadSources: () => void;
|
||||
}
|
||||
|
||||
export const EmptyPrompts: FC<Props> = ({
|
||||
hasExistingIndexPatterns,
|
||||
allSources,
|
||||
onCancel,
|
||||
children,
|
||||
loadSources,
|
||||
}) => {
|
||||
export function isUserDataIndex(source: MatchedItem) {
|
||||
// filter out indices that start with `.`
|
||||
if (source.name.startsWith('.')) return false;
|
||||
|
||||
// filter out sources from FLEET_ASSETS_TO_IGNORE
|
||||
if (source.name === FLEET_ASSETS_TO_IGNORE.LOGS_DATA_STREAM_TO_IGNORE) return false;
|
||||
if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE) return false;
|
||||
if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const EmptyPrompts: FC<Props> = ({ allSources, onCancel, children, loadSources }) => {
|
||||
const {
|
||||
services: { docLinks, application, http, searchClient },
|
||||
services: { docLinks, application, http, searchClient, indexPatternService },
|
||||
} = useKibana<IndexPatternEditorContext>();
|
||||
|
||||
const [remoteClustersExist, setRemoteClustersExist] = useState<boolean>(false);
|
||||
const [goToForm, setGoToForm] = useState<boolean>(false);
|
||||
|
||||
const hasDataIndices = allSources.some(({ name }: MatchedItem) => !name.startsWith('.'));
|
||||
const hasDataIndices = allSources.some(isUserDataIndex);
|
||||
const hasUserIndexPattern = useAsync(() =>
|
||||
indexPatternService.hasUserIndexPattern().catch(() => true)
|
||||
);
|
||||
|
||||
useCallback(() => {
|
||||
let isMounted = true;
|
||||
|
@ -63,7 +73,9 @@ export const EmptyPrompts: FC<Props> = ({
|
|||
};
|
||||
}, [http, hasDataIndices, searchClient]);
|
||||
|
||||
if (!hasExistingIndexPatterns && !goToForm) {
|
||||
if (hasUserIndexPattern.loading) return null; // return null to prevent UI flickering while loading
|
||||
|
||||
if (!hasUserIndexPattern.value && !goToForm) {
|
||||
if (!hasDataIndices && !remoteClustersExist) {
|
||||
// load data
|
||||
return (
|
||||
|
|
|
@ -338,12 +338,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EmptyPrompts
|
||||
onCancel={onCancel}
|
||||
allSources={allSources}
|
||||
hasExistingIndexPatterns={!!existingIndexPatterns.length}
|
||||
loadSources={loadSources}
|
||||
>
|
||||
<EmptyPrompts onCancel={onCancel} allSources={allSources} loadSources={loadSources}>
|
||||
<FlyoutPanels.Group flyoutClassName={'indexPatternEditorFlyout'} maxWidth={1180}>
|
||||
<FlyoutPanels.Item className="fieldEditor__mainFlyoutPanel" border="right">
|
||||
<EuiTitle data-test-subj="flyoutTitle">
|
||||
|
|
|
@ -81,7 +81,10 @@ export const IndexPatternTable = ({
|
|||
);
|
||||
setIndexPatterns(gettedIndexPatterns);
|
||||
setIsLoadingIndexPatterns(false);
|
||||
if (gettedIndexPatterns.length === 0) {
|
||||
if (
|
||||
gettedIndexPatterns.length === 0 ||
|
||||
!(await data.indexPatterns.hasUserIndexPattern().catch(() => false))
|
||||
) {
|
||||
setShowCreateDialog(true);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -93,9 +93,9 @@ export const Overview: FC<Props> = ({ newsFetchResult, solutions, features }) =>
|
|||
|
||||
useEffect(() => {
|
||||
const fetchIsNewKibanaInstance = async () => {
|
||||
const resp = await indexPatternService.getTitles();
|
||||
const hasUserIndexPattern = await indexPatternService.hasUserIndexPattern().catch(() => true);
|
||||
|
||||
setNewKibanaInstance(resp.length === 0);
|
||||
setNewKibanaInstance(!hasUserIndexPattern);
|
||||
};
|
||||
|
||||
fetchIsNewKibanaInstance();
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* 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 }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('has user index pattern API', () => {
|
||||
beforeEach(async () => {
|
||||
await esArchiver.emptyKibanaIndex();
|
||||
if ((await es.indices.exists({ index: 'metrics-test' })).body) {
|
||||
await es.indices.delete({ index: 'metrics-test' });
|
||||
}
|
||||
|
||||
if ((await es.indices.exists({ index: 'logs-test' })).body) {
|
||||
await es.indices.delete({ index: 'logs-test' });
|
||||
}
|
||||
});
|
||||
|
||||
it('should return false if no index patterns', async () => {
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(false);
|
||||
});
|
||||
|
||||
it('should return true if has index pattern with user data', async () => {
|
||||
await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index');
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'basic_index',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(true);
|
||||
|
||||
await esArchiver.unload(
|
||||
'test/api_integration/fixtures/es_archiver/index_patterns/basic_index'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true if has user index pattern without data', async () => {
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'basic_index',
|
||||
allowNoIndex: true,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(true);
|
||||
});
|
||||
|
||||
it('should return false if only metric-* index pattern without data', async () => {
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'metrics-*',
|
||||
allowNoIndex: true,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(false);
|
||||
});
|
||||
|
||||
it('should return true if metric-* index pattern with user data', async () => {
|
||||
await es.index({
|
||||
index: 'metrics-test',
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'metrics-*',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(true);
|
||||
});
|
||||
|
||||
it('should return false if only logs-* index pattern without data', async () => {
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'logs-*',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(false);
|
||||
});
|
||||
|
||||
it('should return true if logs-* index pattern with user data', async () => {
|
||||
await es.index({
|
||||
index: 'logs-test',
|
||||
body: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
await supertest.post('/api/index_patterns/index_pattern').send({
|
||||
override: true,
|
||||
index_pattern: {
|
||||
title: 'logs-*',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await supertest.get('/api/index_patterns/has_user_index_pattern');
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.result).to.be(true);
|
||||
});
|
||||
|
||||
// TODO: should setup fleet first similar to x-pack/test/fleet_functional/apps/home/welcome.ts
|
||||
// but it is skipped due to flakiness https://github.com/elastic/kibana/issues/109017
|
||||
it('should return false if logs-* with .ds-logs-elastic_agent only');
|
||||
it('should return false if metrics-* with .ds-metrics-elastic_agent only');
|
||||
});
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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('has user index pattern', () => {
|
||||
loadTestFile(require.resolve('./has_user_index_pattern'));
|
||||
});
|
||||
}
|
|
@ -18,5 +18,6 @@ export default function ({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./runtime_fields_crud'));
|
||||
loadTestFile(require.resolve('./integration'));
|
||||
loadTestFile(require.resolve('./deprecations'));
|
||||
loadTestFile(require.resolve('./has_user_index_pattern'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue