[Endpoint] new AppRootProvider + Policy details tests (#62319)
* Refactor of mocks into own dir ++ added `createAppContextTestRender` * new AppRootProvider component * Refactor application `index.tsx` to use `AppRootProvider` * Add `generatePolicyDatasource()` to EndpointDocGenerator * Test for policy details
This commit is contained in:
parent
5e1708f884
commit
eacdbcd4f5
|
@ -7,6 +7,11 @@
|
|||
import uuid from 'uuid';
|
||||
import seedrandom from 'seedrandom';
|
||||
import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields } from './types';
|
||||
// FIXME: move types/model to top-level
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { PolicyData } from '../public/applications/endpoint/types';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { generatePolicy } from '../public/applications/endpoint/models/policy';
|
||||
|
||||
export type Event = AlertEvent | EndpointEvent;
|
||||
|
||||
|
@ -452,6 +457,39 @@ export class EndpointDocGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an Ingest `datasource` that includes the Endpoint Policy data
|
||||
*/
|
||||
public generatePolicyDatasource(): PolicyData {
|
||||
return {
|
||||
id: this.seededUUIDv4(),
|
||||
name: 'Endpoint Policy',
|
||||
description: 'Policy to protect the worlds data',
|
||||
config_id: this.seededUUIDv4(),
|
||||
enabled: true,
|
||||
output_id: '',
|
||||
inputs: [
|
||||
{
|
||||
type: 'endpoint',
|
||||
enabled: true,
|
||||
streams: [],
|
||||
config: {
|
||||
policy: {
|
||||
value: generatePolicy(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
namespace: 'default',
|
||||
package: {
|
||||
name: 'endpoint',
|
||||
title: 'Elastic Endpoint',
|
||||
version: '1.0.0',
|
||||
},
|
||||
revision: 1,
|
||||
};
|
||||
}
|
||||
|
||||
private randomN(n: number): number {
|
||||
return Math.floor(this.random() * n);
|
||||
}
|
||||
|
|
|
@ -7,13 +7,9 @@
|
|||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { CoreStart, AppMountParameters, ScopedHistory } from 'kibana/public';
|
||||
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Route, Switch, Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Store } from 'redux';
|
||||
import { useObservable } from 'react-use';
|
||||
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { RouteCapture } from './view/route_capture';
|
||||
import { EndpointPluginStartDependencies } from '../../plugin';
|
||||
import { appStoreFactory } from './store';
|
||||
import { AlertIndex } from './view/alerts';
|
||||
|
@ -21,7 +17,7 @@ import { HostList } from './view/hosts';
|
|||
import { PolicyList } from './view/policy';
|
||||
import { PolicyDetails } from './view/policy';
|
||||
import { HeaderNavigation } from './components/header_nav';
|
||||
import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components';
|
||||
import { AppRootProvider } from './view/app_root_provider';
|
||||
|
||||
/**
|
||||
* This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle.
|
||||
|
@ -49,54 +45,31 @@ interface RouterProps {
|
|||
}
|
||||
|
||||
const AppRoot: React.FunctionComponent<RouterProps> = React.memo(
|
||||
({
|
||||
history,
|
||||
store,
|
||||
coreStart: { http, notifications, uiSettings, application },
|
||||
depsStart: { data },
|
||||
}) => {
|
||||
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
|
||||
|
||||
({ history, store, coreStart, depsStart }) => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={{ http, notifications, application, data }}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<Router history={history}>
|
||||
<RouteCapture>
|
||||
<HeaderNavigation />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => (
|
||||
<h1 data-test-subj="welcomeTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.endpoint.welcomeTitle"
|
||||
defaultMessage="Hello World"
|
||||
/>
|
||||
</h1>
|
||||
)}
|
||||
/>
|
||||
<Route path="/hosts" component={HostList} />
|
||||
<Route path="/alerts" component={AlertIndex} />
|
||||
<Route path="/policy" exact component={PolicyList} />
|
||||
<Route path="/policy/:id" exact component={PolicyDetails} />
|
||||
<Route
|
||||
render={() => (
|
||||
<FormattedMessage
|
||||
id="xpack.endpoint.notFound"
|
||||
defaultMessage="Page Not Found"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</RouteCapture>
|
||||
</Router>
|
||||
</EuiThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
|
||||
<HeaderNavigation />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => (
|
||||
<h1 data-test-subj="welcomeTitle">
|
||||
<FormattedMessage id="xpack.endpoint.welcomeTitle" defaultMessage="Hello World" />
|
||||
</h1>
|
||||
)}
|
||||
/>
|
||||
<Route path="/hosts" component={HostList} />
|
||||
<Route path="/alerts" component={AlertIndex} />
|
||||
<Route path="/policy" exact component={PolicyList} />
|
||||
<Route path="/policy/:id" exact component={PolicyDetails} />
|
||||
<Route
|
||||
render={() => (
|
||||
<FormattedMessage id="xpack.endpoint.notFound" defaultMessage="Page Not Found" />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</AppRootProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
|
||||
import { appStoreFactory } from '../store';
|
||||
import { coreMock } from '../../../../../../../src/core/public/mocks';
|
||||
import { EndpointPluginStartDependencies } from '../../../plugin';
|
||||
import { depsStartMock } from './dependencies_start_mock';
|
||||
import { AppRootProvider } from '../view/app_root_provider';
|
||||
|
||||
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
|
||||
|
||||
/**
|
||||
* Mocked app root context renderer
|
||||
*/
|
||||
interface AppContextTestRender {
|
||||
store: ReturnType<typeof appStoreFactory>;
|
||||
history: ReturnType<typeof createMemoryHistory>;
|
||||
coreStart: ReturnType<typeof coreMock.createStart>;
|
||||
depsStart: EndpointPluginStartDependencies;
|
||||
/**
|
||||
* A wrapper around `AppRootContext` component. Uses the mocked modules as input to the
|
||||
* `AppRootContext`
|
||||
*/
|
||||
AppWrapper: React.FC<any>;
|
||||
/**
|
||||
* Renders the given UI within the created `AppWrapper` providing the given UI a mocked
|
||||
* endpoint runtime context environment
|
||||
*/
|
||||
render: UiRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mocked endpoint app context custom renderer that can be used to render
|
||||
* component that depend upon the application's surrounding context providers.
|
||||
* Factory also returns the content that was used to create the custom renderer, allowing
|
||||
* for further customization.
|
||||
*/
|
||||
export const createAppRootMockRenderer = (): AppContextTestRender => {
|
||||
const history = createMemoryHistory<never>();
|
||||
const coreStart = coreMock.createStart({ basePath: '/mock' });
|
||||
const depsStart = depsStartMock();
|
||||
const store = appStoreFactory({ coreStart, depsStart });
|
||||
const AppWrapper: React.FunctionComponent<{ children: React.ReactElement }> = ({ children }) => (
|
||||
<AppRootProvider store={store} history={history} coreStart={coreStart} depsStart={depsStart}>
|
||||
{children}
|
||||
</AppRootProvider>
|
||||
);
|
||||
const render: UiRender = (ui, options) => {
|
||||
// @ts-ignore
|
||||
return reactRender(ui, {
|
||||
wrapper: AppWrapper,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
history,
|
||||
coreStart,
|
||||
depsStart,
|
||||
AppWrapper,
|
||||
render,
|
||||
};
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
import {
|
||||
dataPluginMock,
|
||||
Start as DataPublicStartMock,
|
||||
} from '../../../../../../src/plugins/data/public/mocks';
|
||||
} from '../../../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
type DataMock = Omit<DataPublicStartMock, 'indexPatterns' | 'query'> & {
|
||||
indexPatterns: Omit<DataPublicStartMock['indexPatterns'], 'getFieldsForWildcard'> & {
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './dependencies_start_mock';
|
||||
export * from './app_context_render';
|
|
@ -74,8 +74,12 @@ export const policyDetailsReducer: Reducer<PolicyDetailsState, AppAction> = (
|
|||
...state,
|
||||
location: action.payload,
|
||||
};
|
||||
const isCurrentlyOnDetailsPage = isOnPolicyDetailsPage(newState);
|
||||
const wasPreviouslyOnDetailsPage = isOnPolicyDetailsPage(state);
|
||||
|
||||
if (isOnPolicyDetailsPage(newState)) {
|
||||
// Did user just enter the Detail page? if so, then set the loading indicator and return new state
|
||||
if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) {
|
||||
newState.isLoading = true;
|
||||
return newState;
|
||||
}
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 React, { memo, ReactNode, useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { History } from 'history';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { useObservable } from 'react-use';
|
||||
import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components';
|
||||
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { appStoreFactory } from '../store';
|
||||
import { RouteCapture } from './route_capture';
|
||||
import { EndpointPluginStartDependencies } from '../../../plugin';
|
||||
|
||||
/**
|
||||
* Provides the context for rendering the endpoint app
|
||||
*/
|
||||
export const AppRootProvider = memo<{
|
||||
store: ReturnType<typeof appStoreFactory>;
|
||||
history: History;
|
||||
coreStart: CoreStart;
|
||||
depsStart: EndpointPluginStartDependencies;
|
||||
children: ReactNode | ReactNode[];
|
||||
}>(
|
||||
({
|
||||
store,
|
||||
history,
|
||||
coreStart: { http, notifications, uiSettings, application },
|
||||
depsStart: { data },
|
||||
children,
|
||||
}) => {
|
||||
const isDarkMode = useObservable<boolean>(uiSettings.get$('theme:darkMode'));
|
||||
const services = useMemo(() => ({ http, notifications, application, data }), [
|
||||
application,
|
||||
data,
|
||||
http,
|
||||
notifications,
|
||||
]);
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EuiThemeProvider darkMode={isDarkMode}>
|
||||
<Router history={history}>
|
||||
<RouteCapture>{children}</RouteCapture>
|
||||
</Router>
|
||||
</EuiThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -61,7 +61,7 @@ export const AgentsSummary = memo<AgentsSummaryProps>(props => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xl">
|
||||
<EuiFlexGroup gutterSize="xl" data-test-subj="policyAgentsSummary">
|
||||
{stats.map(({ key, title, health }) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={key}>
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { createAppRootMockRenderer } from '../../mocks';
|
||||
import { PolicyDetails } from './policy_details';
|
||||
import { EndpointDocGenerator } from '../../../../../common/generate_data';
|
||||
|
||||
describe('Policy Details', () => {
|
||||
type FindReactWrapperResponse = ReturnType<ReturnType<typeof render>['find']>;
|
||||
|
||||
const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms));
|
||||
const generator = new EndpointDocGenerator();
|
||||
const { history, AppWrapper, coreStart } = createAppRootMockRenderer();
|
||||
const http = coreStart.http;
|
||||
const render = (ui: Parameters<typeof mount>[0]) => mount(ui, { wrappingComponent: AppWrapper });
|
||||
let policyDatasource: ReturnType<typeof generator.generatePolicyDatasource>;
|
||||
let policyView: ReturnType<typeof render>;
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
afterEach(() => {
|
||||
if (policyView) {
|
||||
policyView.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
describe('when displayed with invalid id', () => {
|
||||
beforeEach(() => {
|
||||
http.get.mockReturnValue(Promise.reject(new Error('policy not found')));
|
||||
history.push('/policy/1');
|
||||
policyView = render(<PolicyDetails />);
|
||||
});
|
||||
|
||||
it('should show loader followed by error message', () => {
|
||||
expect(policyView.find('EuiLoadingSpinner').length).toBe(1);
|
||||
policyView.update();
|
||||
const callout = policyView.find('EuiCallOut');
|
||||
expect(callout).toHaveLength(1);
|
||||
expect(callout.prop('color')).toEqual('danger');
|
||||
expect(callout.text()).toEqual('policy not found');
|
||||
});
|
||||
});
|
||||
describe('when displayed with valid id', () => {
|
||||
let asyncActions: Promise<unknown> = Promise.resolve();
|
||||
|
||||
beforeEach(() => {
|
||||
policyDatasource = generator.generatePolicyDatasource();
|
||||
policyDatasource.id = '1';
|
||||
|
||||
http.get.mockImplementation((...args) => {
|
||||
const [path] = args;
|
||||
if (typeof path === 'string') {
|
||||
// GET datasouce
|
||||
if (path === '/api/ingest_manager/datasources/1') {
|
||||
asyncActions = asyncActions.then<unknown>(async (): Promise<unknown> => await sleep());
|
||||
return Promise.resolve({
|
||||
item: policyDatasource,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
// GET Agent status for agent config
|
||||
if (path === '/api/ingest_manager/fleet/agent-status') {
|
||||
asyncActions = asyncActions.then(async () => await sleep());
|
||||
return Promise.resolve({
|
||||
results: { events: 0, total: 5, online: 3, error: 1, offline: 1 },
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('unknown API call!'));
|
||||
});
|
||||
history.push('/policy/1');
|
||||
policyView = render(<PolicyDetails />);
|
||||
});
|
||||
|
||||
it('should display back to list button and policy title', () => {
|
||||
policyView.update();
|
||||
const pageHeaderLeft = policyView.find(
|
||||
'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"]'
|
||||
);
|
||||
|
||||
const backToListButton = pageHeaderLeft.find('EuiButtonEmpty');
|
||||
expect(backToListButton.prop('iconType')).toBe('arrowLeft');
|
||||
expect(backToListButton.prop('href')).toBe('/mock/app/endpoint/policy');
|
||||
expect(backToListButton.text()).toBe('Back to policy list');
|
||||
|
||||
const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]');
|
||||
expect(pageTitle).toHaveLength(1);
|
||||
expect(pageTitle.text()).toEqual(policyDatasource.name);
|
||||
});
|
||||
it('should navigate to list if back to link is clicked', async () => {
|
||||
policyView.update();
|
||||
const backToListButton = policyView.find(
|
||||
'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty'
|
||||
);
|
||||
expect(history.location.pathname).toEqual('/policy/1');
|
||||
backToListButton.simulate('click');
|
||||
expect(history.location.pathname).toEqual('/policy');
|
||||
});
|
||||
it('should display agent stats', async () => {
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
const headerRight = policyView.find(
|
||||
'EuiPageHeaderSection[data-test-subj="pageViewHeaderRight"]'
|
||||
);
|
||||
const agentsSummary = headerRight.find('EuiFlexGroup[data-test-subj="policyAgentsSummary"]');
|
||||
expect(agentsSummary).toHaveLength(1);
|
||||
expect(agentsSummary.text()).toBe('Hosts5Online3Offline1Error1');
|
||||
});
|
||||
it('should display cancel button', async () => {
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
const cancelbutton = policyView.find(
|
||||
'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]'
|
||||
);
|
||||
expect(cancelbutton).toHaveLength(1);
|
||||
expect(cancelbutton.text()).toEqual('Cancel');
|
||||
});
|
||||
it('should redirect to policy list when cancel button is clicked', async () => {
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
const cancelbutton = policyView.find(
|
||||
'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]'
|
||||
);
|
||||
expect(history.location.pathname).toEqual('/policy/1');
|
||||
cancelbutton.simulate('click');
|
||||
expect(history.location.pathname).toEqual('/policy');
|
||||
});
|
||||
it('should display save button', async () => {
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
const saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]');
|
||||
expect(saveButton).toHaveLength(1);
|
||||
expect(saveButton.text()).toEqual('Save');
|
||||
});
|
||||
describe('when the save button is clicked', () => {
|
||||
let saveButton: FindReactWrapperResponse;
|
||||
let confirmModal: FindReactWrapperResponse;
|
||||
let modalCancelButton: FindReactWrapperResponse;
|
||||
let modalConfirmButton: FindReactWrapperResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
saveButton = policyView.find('EuiButton[data-test-subj="policyDetailsSaveButton"]');
|
||||
saveButton.simulate('click');
|
||||
policyView.update();
|
||||
confirmModal = policyView.find(
|
||||
'EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]'
|
||||
);
|
||||
modalCancelButton = confirmModal.find('button[data-test-subj="confirmModalCancelButton"]');
|
||||
modalConfirmButton = confirmModal.find(
|
||||
'button[data-test-subj="confirmModalConfirmButton"]'
|
||||
);
|
||||
http.put.mockImplementation((...args) => {
|
||||
asyncActions = asyncActions.then(async () => await sleep());
|
||||
const [path] = args;
|
||||
if (typeof path === 'string') {
|
||||
if (path === '/api/ingest_manager/datasources/1') {
|
||||
return Promise.resolve({
|
||||
item: policyDatasource,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('unknown PUT path!'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a modal confirmation', () => {
|
||||
expect(confirmModal).toHaveLength(1);
|
||||
expect(confirmModal.find('div[data-test-subj="confirmModalTitleText"]').text()).toEqual(
|
||||
'Save and deploy changes'
|
||||
);
|
||||
expect(modalCancelButton.text()).toEqual('Cancel');
|
||||
expect(modalConfirmButton.text()).toEqual('Save and deploy changes');
|
||||
});
|
||||
it('should show info callout if policy is in use', () => {
|
||||
const warningCallout = confirmModal.find(
|
||||
'EuiCallOut[data-test-subj="policyDetailsWarningCallout"]'
|
||||
);
|
||||
expect(warningCallout).toHaveLength(1);
|
||||
expect(warningCallout.text()).toEqual(
|
||||
'This action will update 5 hostsSaving these changes will apply the updates to all active endpoints assigned to this policy'
|
||||
);
|
||||
});
|
||||
it('should close dialog if cancel button is clicked', () => {
|
||||
modalCancelButton.simulate('click');
|
||||
expect(
|
||||
policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]')
|
||||
).toHaveLength(0);
|
||||
});
|
||||
it('should update policy and show success notification when confirm button is clicked', async () => {
|
||||
modalConfirmButton.simulate('click');
|
||||
policyView.update();
|
||||
// Modal should be closed
|
||||
expect(
|
||||
policyView.find('EuiConfirmModal[data-test-subj="policyDetailsConfirmModal"]')
|
||||
).toHaveLength(0);
|
||||
|
||||
// API should be called
|
||||
await asyncActions;
|
||||
expect(http.put.mock.calls[0][0]).toEqual(`/api/ingest_manager/datasources/1`);
|
||||
policyView.update();
|
||||
|
||||
// Toast notification should be shown
|
||||
const toastAddMock = coreStart.notifications.toasts.add.mock;
|
||||
expect(toastAddMock.calls).toHaveLength(1);
|
||||
expect(toastAddMock.calls[0][0]).toMatchObject({
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
});
|
||||
});
|
||||
it('should show an error notification toast if update fails', async () => {
|
||||
policyDatasource.id = 'invalid';
|
||||
modalConfirmButton.simulate('click');
|
||||
|
||||
await asyncActions;
|
||||
policyView.update();
|
||||
|
||||
// Toast notification should be shown
|
||||
const toastAddMock = coreStart.notifications.toasts.add.mock;
|
||||
expect(toastAddMock.calls).toHaveLength(1);
|
||||
expect(toastAddMock.calls[0][0]).toMatchObject({
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -149,7 +149,10 @@ export const PolicyDetails = React.memo(() => {
|
|||
<VerticalDivider spacing="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={handleBackToListOnClick}>
|
||||
<EuiButtonEmpty
|
||||
onClick={handleBackToListOnClick}
|
||||
data-test-subj="policyDetailsCancelButton"
|
||||
>
|
||||
<FormattedMessage id="xpack.endpoint.policy.details.cancel" defaultMessage="Cancel" />
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
@ -157,6 +160,7 @@ export const PolicyDetails = React.memo(() => {
|
|||
<EuiButton
|
||||
fill={true}
|
||||
iconType="save"
|
||||
data-test-subj="policyDetailsSaveButton"
|
||||
// FIXME: need to disable if User has no write permissions to ingest - see: https://github.com/elastic/endpoint-app-team/issues/296
|
||||
onClick={handleSaveOnClick}
|
||||
isLoading={isPolicyLoading}
|
||||
|
@ -216,6 +220,7 @@ const ConfirmUpdate = React.memo<{
|
|||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
data-test-subj="policyDetailsConfirmModal"
|
||||
title={i18n.translate('xpack.endpoint.policy.details.updateConfirm.title', {
|
||||
defaultMessage: 'Save and deploy changes',
|
||||
})}
|
||||
|
@ -237,6 +242,7 @@ const ConfirmUpdate = React.memo<{
|
|||
{hostCount > 0 && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
data-test-subj="policyDetailsWarningCallout"
|
||||
title={i18n.translate('xpack.endpoint.policy.details.updateConfirm.warningTitle', {
|
||||
defaultMessage:
|
||||
'This action will update {hostCount, plural, one {# host} other {# hosts}}',
|
||||
|
|
Loading…
Reference in a new issue