Skip to content

Commit

Permalink
Merge branch 'master' of github.com:elastic/kibana into 108119_filter…
Browse files Browse the repository at this point in the history
…_actions
  • Loading branch information
mgiota committed Aug 17, 2021
2 parents 09d16c7 + 3013e10 commit 505dfa3
Show file tree
Hide file tree
Showing 116 changed files with 4,157 additions and 1,429 deletions.
31 changes: 29 additions & 2 deletions src/core/server/status/plugins_status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,6 @@ describe('PluginStatusService', () => {
subscription.unsubscribe();

expect(statusUpdates).toEqual([
{ a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
{ a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
{ a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
Expand All @@ -274,7 +273,6 @@ describe('PluginStatusService', () => {
subscription.unsubscribe();

expect(statusUpdates).toEqual([
{ a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
{ a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
{ a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
Expand Down Expand Up @@ -357,6 +355,35 @@ describe('PluginStatusService', () => {
}).toThrowError();
});

it('debounces plugins custom status registration', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies,
});
const available: ServiceStatus = {
level: ServiceStatusLevels.available,
summary: 'a available',
};

const statusUpdates: Array<Record<string, ServiceStatus>> = [];
const subscription = service
.getDependenciesStatus$('b')
.subscribe((status) => statusUpdates.push(status));

const pluginA$ = new BehaviorSubject(available);
service.set('a', pluginA$);

expect(statusUpdates).toStrictEqual([]);

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// Waiting for the debounce timeout should cut a new update
await delay(25);
subscription.unsubscribe();

expect(statusUpdates).toStrictEqual([{ a: available }]);
});

it('debounces events in quick succession', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
Expand Down
1 change: 1 addition & 0 deletions src/core/server/status/plugins_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class PluginsStatusService {

public getDerivedStatus$(plugin: PluginName): Observable<ServiceStatus> {
return this.update$.pipe(
debounceTime(25), // Avoid calling the plugin's custom status logic for every plugin that depends on it.
switchMap(() => {
// Only go up the dependency tree if any of this plugin's dependencies have a custom status
// Helps eliminate memory overhead of creating thousands of Observables unnecessarily.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions src/plugins/home/public/application/components/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,9 @@ export class Home extends Component {
}
}, 500);

const resp = await this.props.find({
type: 'index-pattern',
fields: ['title'],
search: `*`,
search_fields: ['title'],
perPage: 1,
});
const { isNewInstance } = await this.props.http.get('/internal/home/new_instance_status');

this.endLoading({ isNewKibanaInstance: resp.total === 0 });
this.endLoading({ isNewKibanaInstance: isNewInstance });
} catch (err) {
// An error here is relatively unimportant, as it only means we don't provide
// some UI niceties.
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/home/public/application/components/home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ describe('home', () => {
defaultProps.localStorage.getItem = sinon.spy(() => 'true');

const component = await renderHome({
find: () => Promise.resolve({ total: 0 }),
http: {
get: () => Promise.resolve({ isNewInstance: true }),
},
});

sinon.assert.calledOnce(defaultProps.localStorage.getItem);
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/home/public/application/components/home_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function HomeApp({ directories, solutions }) {
addBasePath,
environmentService,
telemetry,
http,
} = getServices();
const environment = environmentService.getEnvironment();
const isCloudEnabled = environment.cloud;
Expand Down Expand Up @@ -71,10 +72,10 @@ export function HomeApp({ directories, solutions }) {
addBasePath={addBasePath}
directories={directories}
solutions={solutions}
find={savedObjectsClient.find}
localStorage={localStorage}
urlBasePath={getBasePath()}
telemetry={telemetry}
http={http}
/>
</Route>
<Route path="*" exact={true} component={RedirectToDefaultApp} />
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/home/public/application/components/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class Welcome extends React.Component<Props> {
const { urlBasePath, telemetry } = this.props;
return (
<EuiPortal>
<div className="homWelcome">
<div className="homWelcome" data-test-subj="homeWelcomeInterstitial">
<header className="homWelcome__header">
<div className="homWelcome__content eui-textCenter">
<EuiSpacer size="xl" />
Expand Down
35 changes: 35 additions & 0 deletions src/plugins/home/server/routes/fetch_new_instance_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { IRouter } from 'src/core/server';
import { isNewInstance } from '../services/new_instance_status';

export const registerNewInstanceStatusRoute = (router: IRouter) => {
router.get(
{
path: '/internal/home/new_instance_status',
validate: false,
},
router.handleLegacyErrors(async (context, req, res) => {
const { client: soClient } = context.core.savedObjects;
const { client: esClient } = context.core.elasticsearch;

try {
return res.ok({
body: {
isNewInstance: await isNewInstance({ esClient, soClient }),
},
});
} catch (e) {
return res.customError({
statusCode: 500,
});
}
})
);
};
2 changes: 2 additions & 0 deletions src/plugins/home/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import { IRouter } from 'src/core/server';
import { registerHitsStatusRoute } from './fetch_es_hits_status';
import { registerNewInstanceStatusRoute } from './fetch_new_instance_status';

export const registerRoutes = (router: IRouter) => {
registerHitsStatusRoute(router);
registerNewInstanceStatusRoute(router);
};
129 changes: 129 additions & 0 deletions src/plugins/home/server/services/new_instance_status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 { isNewInstance } from './new_instance_status';
import { elasticsearchServiceMock, savedObjectsClientMock } from '../../../../core/server/mocks';

describe('isNewInstance', () => {
const esClient = elasticsearchServiceMock.createScopedClusterClient();
const soClient = savedObjectsClientMock.create();

beforeEach(() => jest.resetAllMocks());

it('returns true when there are no index patterns', async () => {
soClient.find.mockResolvedValue({
page: 1,
per_page: 100,
total: 0,
saved_objects: [],
});
expect(await isNewInstance({ esClient, soClient })).toEqual(true);
});

it('returns false 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 isNewInstance({ esClient, soClient })).toEqual(false);
});

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 /_cat/indices for the index patterns', async () => {
await isNewInstance({ esClient, soClient });
expect(esClient.asCurrentUser.cat.indices).toHaveBeenCalledWith({
index: 'logs-*,metrics-*',
format: 'json',
});
});

it('returns true if no logs or metrics indices exist', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise([])
);
expect(await isNewInstance({ esClient, soClient })).toEqual(true);
});

it('returns true if no logs or metrics indices contain data', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise([
{ index: '.ds-metrics-foo', 'docs.count': '0' },
])
);
expect(await isNewInstance({ esClient, soClient })).toEqual(true);
});

it('returns true if only metrics-elastic_agent index contains data', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise([
{ index: '.ds-metrics-elastic_agent', 'docs.count': '100' },
])
);
expect(await isNewInstance({ esClient, soClient })).toEqual(true);
});

it('returns true if only logs-elastic_agent index contains data', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise([
{ index: '.ds-logs-elastic_agent', 'docs.count': '100' },
])
);
expect(await isNewInstance({ esClient, soClient })).toEqual(true);
});

it('returns false if any other logs or metrics indices contain data', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createSuccessTransportRequestPromise([
{ index: '.ds-metrics-foo', 'docs.count': '100' },
])
);
expect(await isNewInstance({ esClient, soClient })).toEqual(false);
});

it('returns false if an authentication error is thrown', async () => {
esClient.asCurrentUser.cat.indices.mockReturnValue(
elasticsearchServiceMock.createErrorTransportRequestPromise({})
);
expect(await isNewInstance({ esClient, soClient })).toEqual(false);
});
});
});
67 changes: 67 additions & 0 deletions src/plugins/home/server/services/new_instance_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 type { IScopedClusterClient, SavedObjectsClientContract } from '../../../../core/server';
import type { IndexPatternSavedObjectAttrs } from '../../../data/common/index_patterns/index_patterns';

const LOGS_INDEX_PATTERN = 'logs-*';
const METRICS_INDEX_PATTERN = 'metrics-*';

const INDEX_PREFIXES_TO_IGNORE = [
'.ds-metrics-elastic_agent', // ignore index created by Fleet server itself
'.ds-logs-elastic_agent', // ignore index created by Fleet server itself
];

interface Deps {
esClient: IScopedClusterClient;
soClient: SavedObjectsClientContract;
}

export const isNewInstance = async ({ esClient, soClient }: Deps): Promise<boolean> => {
const indexPatterns = await soClient.find<IndexPatternSavedObjectAttrs>({
type: 'index-pattern',
fields: ['title'],
search: `*`,
searchFields: ['title'],
perPage: 100,
});

// If there are no index patterns, assume this is a new instance
if (indexPatterns.total === 0) {
return true;
}

// If there are any index patterns that are not the default metrics-* and logs-* ones created by Fleet, assume this
// is not a new instance
if (
indexPatterns.saved_objects.some(
(ip) =>
ip.attributes.title !== LOGS_INDEX_PATTERN && ip.attributes.title !== METRICS_INDEX_PATTERN
)
) {
return false;
}

try {
const logsAndMetricsIndices = await esClient.asCurrentUser.cat.indices({
index: `${LOGS_INDEX_PATTERN},${METRICS_INDEX_PATTERN}`,
format: 'json',
});

const anyIndicesContainerUserData = logsAndMetricsIndices.body
// Ignore some data that is shipped by default
.filter(({ index }) => !INDEX_PREFIXES_TO_IGNORE.some((prefix) => index?.startsWith(prefix)))
// If any other logs and metrics indices have data, return false
.some((catResult) => (catResult['docs.count'] ?? '0') !== '0');

return !anyIndicesContainerUserData;
} catch (e) {
// If any errors are encountered return false to be safe
return false;
}
};
Loading

0 comments on commit 505dfa3

Please sign in to comment.