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

add button link to ingest #70142

Merged
merged 23 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ddc3f39
add button link to ingest
parkiino Jun 26, 2020
1c078cf
link does not do full refresh
parkiino Jun 29, 2020
604316a
use queryparam initially, navigate to correct url
parkiino Jun 30, 2020
460c5f8
hook to check ingest enabled
parkiino Jul 1, 2020
36b934f
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 1, 2020
71c9aac
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 1, 2020
2113b99
tests
parkiino Jul 1, 2020
415ae91
checks if metadata index is present
parkiino Jul 1, 2020
ac75c49
modify ingest url hook naming
parkiino Jul 1, 2020
55261bc
modify hook to be more robust
parkiino Jul 2, 2020
504f82b
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 2, 2020
b79f6d7
add beta to button
parkiino Jul 2, 2020
1844e6b
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 2, 2020
7c932c6
remove tertiary action, make secondary action
parkiino Jul 2, 2020
a65f1a5
comments
parkiino Jul 2, 2020
a443859
make network, hosts, alerts/detection use overview empty
parkiino Jul 2, 2020
295225e
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 2, 2020
e119a76
type check
parkiino Jul 2, 2020
613f90e
fix tests
parkiino Jul 6, 2020
e655571
Merge remote-tracking branch 'upstream/master' into task/onboarding-s…
parkiino Jul 6, 2020
a30978c
i18n
parkiino Jul 6, 2020
50307f8
text changes
parkiino Jul 6, 2020
7609a82
Merge branch 'master' into task/onboarding-splash
elasticmachine Jul 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, { useState } from 'react';
import { useRouteMatch, Switch, Route } from 'react-router-dom';
import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom';
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
import { i18n } from '@kbn/i18n';
import { PAGE_ROUTING_PATHS } from '../../../../constants';
Expand Down Expand Up @@ -114,7 +114,11 @@ function InstalledPackages() {

function AvailablePackages() {
useBreadcrumbs('integrations_all');
const [selectedCategory, setSelectedCategory] = useState('');
const history = useHistory();
const queryParams = new URLSearchParams(useLocation().search);
const initialCategory =
queryParams.get('category') !== null ? (queryParams.get('category') as string) : '';
Copy link
Contributor

Choose a reason for hiding this comment

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

Is null significant? If possible, I'd prefer

Suggested change
const initialCategory =
queryParams.get('category') !== null ? (queryParams.get('category') as string) : '';
const initialCategory = queryParams.get('category') || '';

I find it more straightforward and we don't lose any safety, afaict.

const [selectedCategory, setSelectedCategory] = useState(initialCategory);
const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({
category: selectedCategory,
});
Expand All @@ -141,7 +145,13 @@ function AvailablePackages() {
isLoading={isLoadingCategories}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)}
onCategoryChange={({ id }: CategorySummaryItem) => {
// clear category query param in the url
if (queryParams.get('category') !== null) {
history.push({});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

@parkiino is null significant here? Can we rely on "truthy"?

Suggested change
// clear category query param in the url
if (queryParams.get('category') !== null) {
history.push({});
}
if (queryParams.get('category')) {

cc @jen-huang and @neptunian for thoughts on dealing with the params/state

setSelectedCategory(id);
}}
/>
) : null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui';
import React from 'react';
import React, { MouseEventHandler } from 'react';
import styled from 'styled-components';

const EmptyPrompt = styled(EuiEmptyPrompt)`
Expand All @@ -23,6 +23,12 @@ interface EmptyPageProps {
actionSecondaryLabel?: string;
actionSecondaryTarget?: string;
actionSecondaryUrl?: string;
actionTertiaryIcon?: IconType;
actionTertiaryLabel?: string;
actionTertiaryTarget?: string;
actionTertiaryUrl?: string;
actionTertiaryOnClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
actionTertiaryFill?: boolean;
'data-test-subj'?: string;
message?: string;
title: string;
Expand All @@ -38,6 +44,12 @@ export const EmptyPage = React.memo<EmptyPageProps>(
actionSecondaryLabel,
actionSecondaryTarget,
actionSecondaryUrl,
actionTertiaryIcon,
actionTertiaryLabel,
actionTertiaryTarget,
actionTertiaryUrl,
actionTertiaryOnClick,
actionTertiaryFill = false,
message,
title,
...rest
Expand All @@ -48,6 +60,21 @@ export const EmptyPage = React.memo<EmptyPageProps>(
body={message && <p>{message}</p>}
actions={
<EuiFlexGroup justifyContent="center">
{actionTertiaryLabel && actionTertiaryOnClick && (
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButton
data-test-subj="empty-page-tertiary-action"
fill={actionTertiaryFill}
onClick={actionTertiaryOnClick}
href={actionTertiaryUrl}
iconType={actionTertiaryIcon}
target={actionTertiaryTarget}
>
{actionTertiaryLabel}
</EuiButton>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
fill
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';

/**
* Returns an object which ingest permissions are allowed
*/
export const useIngestEnabledCheck = (): {
allEnabled: boolean;
show: boolean;
write: boolean;
read: boolean;
} => {
const { services } = useKibana();

// Check if Ingest Manager is present in the configuration
const show = services.application.capabilities.ingestManager?.show ?? false;
const write = services.application.capabilities.ingestManager?.write ?? false;
const read = services.application.capabilities.ingestManager?.read ?? false;

// Check if all Ingest Manager permissions are enabled
const allEnabled = show && read && write ? true : false;

return {
allEnabled,
show,
write,
read,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ export const EMPTY_ACTION_SECONDARY = i18n.translate(
defaultMessage: 'View getting started guide',
}
);

export const EMPTY_ACTION_ENDPOINT = i18n.translate(
'xpack.securitySolution.pages.common.emptyActionEndpoint',
{
defaultMessage: 'Add data with Elastic Agent (Beta)',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ export function useHostSelector<TSelected>(selector: (state: HostState) => TSele
/**
* Returns an object that contains Ingest app and URL information
*/
export const useHostIngestUrl = (): { url: string; appId: string; appPath: string } => {
export const useIngestUrl = (subpath: string): { url: string; appId: string; appPath: string } => {
const { services } = useKibana();
return useMemo(() => {
const appPath = `#/fleet`;
const appPath = `#/${subpath}`;
return {
url: `${services.application.getUrlForApp('ingestManager')}${appPath}`,
appId: 'ingestManager',
appPath,
};
}, [services.application]);
}, [services.application, subpath]);
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,38 @@ import * as i18nCommon from '../../../common/translations';
import { EmptyPage } from '../../../common/components/empty_page';
import { useKibana } from '../../../common/lib/kibana';
import { ADD_DATA_PATH } from '../../../../common/constants';
import { useIngestUrl } from '../../../management/pages/endpoint_hosts/view/hooks';
import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled';

const OverviewEmptyComponent: React.FC = () => {
const { http, docLinks } = useKibana().services;
const basePath = http.basePath.get();
const { appId: ingestAppId, appPath: ingestPath, url: ingestUrl } = useIngestUrl(
'integrations?category=security'
);
const handleOnClick = useNavigateToAppEventHandler(ingestAppId, { path: ingestPath });
const { allEnabled: isIngestEnabled } = useIngestEnabledCheck();

return (
return isIngestEnabled === true ? (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18nCommon.EMPTY_ACTION_PRIMARY}
actionPrimaryUrl={`${basePath}${ADD_DATA_PATH}`}
actionSecondaryIcon="popout"
actionSecondaryLabel={i18nCommon.EMPTY_ACTION_SECONDARY}
actionSecondaryTarget="_blank"
actionSecondaryUrl={docLinks.links.siem.gettingStarted}
actionTertiaryIcon="gear"
actionTertiaryLabel={i18nCommon.EMPTY_ACTION_ENDPOINT}
actionTertiaryUrl={ingestUrl}
actionTertiaryOnClick={handleOnClick}
actionTertiaryFill={true}
data-test-subj="empty-page"
message={i18nCommon.EMPTY_MESSAGE}
title={i18nCommon.EMPTY_TITLE}
/>
) : (
<EmptyPage
actionPrimaryIcon="gear"
actionPrimaryLabel={i18nCommon.EMPTY_ACTION_PRIMARY}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
UseMessagesStorage,
} from '../../common/containers/local_storage/use_messages_storage';
import { Overview } from './index';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';

jest.mock('../../common/lib/kibana');
jest.mock('../../common/containers/source');
Expand All @@ -28,6 +29,7 @@ jest.mock('../../common/components/search_bar', () => ({
jest.mock('../../common/components/query_bar', () => ({
QueryBar: () => null,
}));
jest.mock('../../common/hooks/endpoint/ingest_enabled');
jest.mock('../../common/containers/local_storage/use_messages_storage');

const endpointNoticeMessage = (hasMessageValue: boolean) => {
Expand All @@ -42,26 +44,54 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => {

describe('Overview', () => {
describe('rendering', () => {
test('it renders the Setup Instructions text when no index is available', async () => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: false,
describe('when no index is available', () => {
beforeEach(() => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: false,
});
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false });
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<
UseMessagesStorage
>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false));
});

const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false));
it('renders the Setup Instructions text', () => {
const wrapper = mount(
<TestProviders>
<MemoryRouter>
<Overview />
</MemoryRouter>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true);
});

const wrapper = mount(
<TestProviders>
<MemoryRouter>
<Overview />
</MemoryRouter>
</TestProviders>
);
it('does not show Endpoint get ready button when ingest is not enabled', () => {
const wrapper = mount(
<TestProviders>
<MemoryRouter>
<Overview />
</MemoryRouter>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="empty-page-tertiary-action"]').exists()).toBe(false);
});

expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true);
it('shows Endpoint get ready button when ingest is enabled', () => {
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
<MemoryRouter>
<Overview />
</MemoryRouter>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="empty-page-tertiary-action"]').exists()).toBe(true);
});
});

test('it DOES NOT render the Getting started text when an index is available', async () => {
it('it DOES NOT render the Getting started text when an index is available', () => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
Expand All @@ -80,7 +110,7 @@ describe('Overview', () => {
expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false);
});

test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', async () => {
test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', () => {
(useWithSource as jest.Mock).mockReturnValueOnce({
indicesExist: true,
indexPattern: {},
Expand All @@ -104,7 +134,7 @@ describe('Overview', () => {
expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true);
});

test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', async () => {
test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', () => {
(useWithSource as jest.Mock).mockReturnValueOnce({
indicesExist: true,
indexPattern: {},
Expand All @@ -128,7 +158,7 @@ describe('Overview', () => {
expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false);
});

test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', async () => {
test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', () => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
Expand All @@ -147,7 +177,7 @@ describe('Overview', () => {
expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false);
});

test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', async () => {
test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', () => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { SourceStatusAdapter } from './index';
import { buildQuery } from './query.dsl';
import { ApmServiceNameAgg } from './types';
import { ENDPOINT_METADATA_INDEX } from '../../../common/constants';
Copy link
Contributor

Choose a reason for hiding this comment

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

this should work, but we'll also wanna make sure that the endpoint events and alerts index are added to the defaults. Can be in another PR


const APM_INDEX_NAME = 'apm-*-transaction*';

Expand All @@ -18,6 +19,8 @@ export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter {
// Intended flow to determine app-empty state is to first check siem indices (as this is a quick shard count), and
// if no shards exist only then perform the heavier APM query. This optimizes for normal use when siem data exists
try {
// Add endpoint metadata index to indices to check
indexNames.push(ENDPOINT_METADATA_INDEX);
// Remove APM index if exists, and only query if length > 0 in case it's the only index provided
const nonApmIndexNames = indexNames.filter((name) => name !== APM_INDEX_NAME);
const indexCheckResponse = await (nonApmIndexNames.length > 0
Expand Down