Skip to content

Commit

Permalink
Update onboarding interstitial to handle default Fleet assets (#108193)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover authored Aug 17, 2021
1 parent 468daeb commit 66a06f9
Show file tree
Hide file tree
Showing 19 changed files with 380 additions and 19 deletions.

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;
}
};
1 change: 0 additions & 1 deletion test/common/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export default function () {
)}`,
`--elasticsearch.username=${kibanaServerTestUser.username}`,
`--elasticsearch.password=${kibanaServerTestUser.password}`,
`--home.disableWelcomeScreen=true`,
// Needed for async search functional tests to introduce a delay
`--data.search.aggs.shardDelay.enabled=true`,
`--security.showInsecureClusterWarning=false`,
Expand Down
30 changes: 30 additions & 0 deletions test/functional/apps/home/_welcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 browser = getService('browser');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'home']);

describe('Welcome interstitial', () => {
before(async () => {
// Need to navigate to page first to clear storage before test can be run
await PageObjects.common.navigateToUrl('home', undefined);
await browser.clearLocalStorage();
await esArchiver.emptyKibanaIndex();
});

it('is displayed on a fresh on-prem install', async () => {
await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false });
expect(await PageObjects.home.isWelcomeInterstitialDisplayed()).to.be(true);
});
});
}
1 change: 1 addition & 0 deletions test/functional/apps/home/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export default function ({ getService, loadTestFile }) {
loadTestFile(require.resolve('./_newsfeed'));
loadTestFile(require.resolve('./_add_data'));
loadTestFile(require.resolve('./_sample_data'));
loadTestFile(require.resolve('./_welcome'));
});
}
Loading

0 comments on commit 66a06f9

Please sign in to comment.