Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update onboarding interstitial to handle default Fleet assets #108193

Merged
merged 14 commits into from
Aug 17, 2021
Merged

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 @@ -75,15 +75,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 @@ -256,7 +256,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.badRequest({
body: e,
});
joshdover marked this conversation as resolved.
Show resolved Hide resolved
}
})
);
};
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);
};
121 changes: 121 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,121 @@
/*
* 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('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: 'metrics-foo', docsCount: '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: 'metrics-elastic_agent', docsCount: '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: 'logs-elastic_agent', docsCount: '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: 'metrics-foo', docsCount: '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 = 'metrics-*';
const METRICS_INDEX_PATTERN = 'logs-*';
joshdover marked this conversation as resolved.
Show resolved Hide resolved

const INDEX_PREFIXES_TO_IGNORE = [
'metrics-elastic_agent', // ignore index created by Fleet server itself
'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: 1,
joshdover marked this conversation as resolved.
Show resolved Hide resolved
});

// 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({
joshdover marked this conversation as resolved.
Show resolved Hide resolved
index: `${LOGS_INDEX_PATTERN},${METRICS_INDEX_PATTERN}`,
format: 'json',
});

const anyIndicesContainerUserData = logsAndMetricsIndices.body
joshdover marked this conversation as resolved.
Show resolved Hide resolved
// 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(({ docsCount }) => parseInt(docsCount ?? '0', 10) > 0);
joshdover marked this conversation as resolved.
Show resolved Hide resolved

return !anyIndicesContainerUserData;
} catch (e) {
// If any errors are encoutnered 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`,
Copy link
Contributor Author

@joshdover joshdover Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can remove this configuration now. We now have some logic that sets the localStorage value in the FTR when attempting to login to Kibana to handle this case in Cloud where we can't set this config value. This makes this unnecessary now.

I'll tackle that as a follow up task.

// Needed for async search functional tests to introduce a delay
`--data.search.aggs.shardDelay.enabled=true`,
`--security.showInsecureClusterWarning=false`,
Expand Down
48 changes: 48 additions & 0 deletions test/functional/apps/home/_welcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 path from 'path';
import { FtrProviderContext } from '../../ftr_provider_context';
const FLEET_ONBOARDING_ARCHIVE = path.join(
__dirname,
'..',
'..',
'fixtures',
'kbn_archiver',
'fleet-onboarding.json'
);

export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'home']);
const kibanaServer = getService('kibanaServer');

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);
});

it('is displayed on a fresh install with Fleet system package index-patterns installed', async () => {
// Load index-patterns installed by default in Cloud
await kibanaServer.importExport.load(FLEET_ONBOARDING_ARCHIVE);
await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false });
expect(await PageObjects.home.isWelcomeInterstitialDisplayed()).to.be(true);
await kibanaServer.importExport.unload(FLEET_ONBOARDING_ARCHIVE);
});
});
}
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'));
});
}
37 changes: 37 additions & 0 deletions test/functional/fixtures/kbn_archiver/fleet-onboarding.json

Large diffs are not rendered by default.

Loading