diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index e8083836d3c1e..2beb07e6da5bc 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -216,28 +216,14 @@ describe('parseClientOptions', () => { ); }); - it('adds auth to the nodes if both `username` and `password` are set', () => { - let options = parseClientOptions( + it('does not add auth to the nodes', () => { + const options = parseClientOptions( createConfig({ username: 'user', - hosts: ['http://node-A:9200'], - }), - false - ); - expect(options.nodes).toMatchInlineSnapshot(` - Array [ - Object { - "url": "http://node-a:9200/", - }, - ] - `); - - options = parseClientOptions( - createConfig({ password: 'pass', hosts: ['http://node-A:9200'], }), - false + true ); expect(options.nodes).toMatchInlineSnapshot(` Array [ @@ -246,22 +232,6 @@ describe('parseClientOptions', () => { }, ] `); - - options = parseClientOptions( - createConfig({ - username: 'user', - password: 'pass', - hosts: ['http://node-A:9200'], - }), - false - ); - expect(options.nodes).toMatchInlineSnapshot(` - Array [ - Object { - "url": "http://user:pass@node-a:9200/", - }, - ] - `); }); }); describe('when `scoped` is true', () => { diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index f24c0d86550b8..dab20edfc61ff 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -93,7 +93,7 @@ export function parseClientOptions( }; } - clientOptions.nodes = config.hosts.map((host) => convertHost(host, !scoped, config)); + clientOptions.nodes = config.hosts.map((host) => convertHost(host)); if (config.ssl) { clientOptions.ssl = generateSslConfig( @@ -140,18 +140,10 @@ const generateSslConfig = ( return ssl; }; -const convertHost = ( - host: string, - needAuth: boolean, - { username, password }: ElasticsearchClientConfig -): NodeOptions => { +const convertHost = (host: string): NodeOptions => { const url = new URL(host); const isHTTPS = url.protocol === 'https:'; url.port = url.port || (isHTTPS ? '443' : '80'); - if (needAuth && username && password) { - url.username = username; - url.password = password; - } return { url, diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index d9af3d0ae6d5b..8e345d6ff66a8 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -621,9 +621,7 @@ describe('Task Runner', () => { expect(await taskRunner.run()).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:00:10.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( @@ -727,9 +725,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:00:10.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); @@ -781,9 +777,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); @@ -814,9 +808,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); @@ -846,13 +838,48 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": 1970-01-01T00:05:00.000Z, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); + test(`doesn't change previousStartedAt when it fails to run`, async () => { + const originalAlertSate = { + previousStartedAt: '1970-01-05T00:00:00.000Z', + }; + + alertType.executor.mockImplementation( + ({ services: executorServices }: AlertExecutorOptions) => { + throw new Error('OMG'); + } + ); + + const taskRunner = new TaskRunner( + alertType, + { + ...mockedTaskInstance, + state: originalAlertSate, + }, + taskRunnerFactoryInitializerParams + ); + + alertsClient.get.mockResolvedValueOnce(mockedAlertTypeSavedObject); + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [], + }); + + const runnerResult = await taskRunner.run(); + + expect(runnerResult.state.previousStartedAt).toEqual( + new Date(originalAlertSate.previousStartedAt) + ); + }); + test('avoids rescheduling a failed Alert Task Runner when it throws due to failing to fetch the alert', async () => { alertsClient.get.mockImplementation(() => { throw SavedObjectsErrorHelpers.createGenericNotFoundError('task', '1'); @@ -878,9 +905,7 @@ describe('Task Runner', () => { expect(runnerResult).toMatchInlineSnapshot(` Object { "runAt": undefined, - "state": Object { - "previousStartedAt": 1970-01-01T00:00:00.000Z, - }, + "state": Object {}, } `); }); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 1ccf14a3a5334..7da16717aef9f 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -322,7 +322,7 @@ export class TaskRunner { async run(): Promise { const { params: { alertId, spaceId }, - startedAt: previousStartedAt, + startedAt, state: originalState, } = this.taskInstance; @@ -360,7 +360,7 @@ export class TaskRunner { (stateUpdates: AlertTaskState) => { return { ...stateUpdates, - previousStartedAt, + previousStartedAt: startedAt, }; }, (err: Error) => { @@ -370,10 +370,7 @@ export class TaskRunner { } else { this.logger.error(message); } - return { - ...originalState, - previousStartedAt, - }; + return originalState; } ), runAt: resolveErr(runAt, (err) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 53aa3db00b66a..decf1e2158744 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -93,3 +93,5 @@ export const TOKEN_TYPE_INFO = [ ]; export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; + +export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx index d2e7ff5f32dd4..e9217da163636 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx @@ -4,15 +4,98 @@ * you may not use this file except in compliance with the Elastic License. */ +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + import React from 'react'; import { shallow } from 'enzyme'; -import { EuiFlyoutBody } from '@elastic/eui'; +import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; + +import { ApiTokenTypes } from '../constants'; +import { defaultApiToken } from '../credentials_logic'; +import { + FormKeyName, + FormKeyType, + FormKeyReadWriteAccess, + FormKeyEngineAccess, + FormKeyUpdateWarning, +} from './form_components'; import { CredentialsFlyoutBody } from './body'; describe('CredentialsFlyoutBody', () => { + const values = { + activeApiToken: defaultApiToken, + activeApiTokenExists: false, + }; + const actions = { + onApiTokenChange: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + it('renders', () => { const wrapper = shallow(); + expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1); + expect(wrapper.find(EuiForm)).toHaveLength(1); + }); + + it('shows the expected form components on default private key creation', () => { + const wrapper = shallow(); + + expect(wrapper.find(FormKeyName)).toHaveLength(1); + expect(wrapper.find(FormKeyType)).toHaveLength(1); + expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(1); + expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(1); + expect(wrapper.find(FormKeyUpdateWarning)).toHaveLength(0); + }); + + it('does not show read-write access options for search keys', () => { + setMockValues({ + ...values, + activeApiToken: { + ...defaultApiToken, + type: ApiTokenTypes.Search, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(0); + expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(1); + }); + + it('does not show read-write or engine access options for admin keys', () => { + setMockValues({ + ...values, + activeApiToken: { + ...defaultApiToken, + type: ApiTokenTypes.Admin, + }, + }); + const wrapper = shallow(); + + expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(0); + expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(0); + }); + + it('shows a warning if updating an existing key', () => { + setMockValues({ ...values, activeApiTokenExists: true }); + const wrapper = shallow(); + + expect(wrapper.find(FormKeyUpdateWarning)).toHaveLength(1); + }); + + it('calls onApiTokenChange on form submit', () => { + const wrapper = shallow(); + + const preventDefault = jest.fn(); + wrapper.find(EuiForm).simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(actions.onApiTokenChange).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx index 2afba633ca892..0395c77cf9d89 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx @@ -5,15 +5,41 @@ */ import React from 'react'; -import { EuiFlyoutBody } from '@elastic/eui'; +import { useValues, useActions } from 'kea'; +import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; +import { CredentialsLogic } from '../credentials_logic'; +import { ApiTokenTypes } from '../constants'; + +import { + FormKeyName, + FormKeyType, + FormKeyReadWriteAccess, + FormKeyEngineAccess, + FormKeyUpdateWarning, +} from './form_components'; export const CredentialsFlyoutBody: React.FC = () => { + const { onApiTokenChange } = useActions(CredentialsLogic); + const { activeApiToken, activeApiTokenExists } = useValues(CredentialsLogic); + return ( - Details go here + { + e.preventDefault(); + onApiTokenChange(); + }} + component="form" + > + + + {activeApiToken.type === ApiTokenTypes.Private && } + {activeApiToken.type !== ApiTokenTypes.Admin && } + + {activeApiTokenExists && } ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/index.ts new file mode 100644 index 0000000000000..ad39717ff8979 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { FormKeyName } from './key_name'; +export { FormKeyType } from './key_type'; +export { FormKeyReadWriteAccess } from './key_read_write_access'; +export { FormKeyEngineAccess } from './key_engine_access'; +export { FormKeyUpdateWarning } from './key_update_warning'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx new file mode 100644 index 0000000000000..b4b092f17a6aa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiRadio, EuiCheckbox } from '@elastic/eui'; + +import { FormKeyEngineAccess, EngineSelection } from './key_engine_access'; + +describe('FormKeyEngineAccess', () => { + const values = { + myRole: { canAccessAllEngines: true }, + fullEngineAccessChecked: true, + }; + const actions = { + setAccessAllEngines: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiRadio)).toHaveLength(2); + expect(wrapper.find(EngineSelection)).toHaveLength(0); + }); + + it('hides the full access radio option if the user does not have access to all engines', () => { + setMockValues({ + ...values, + myRole: { canAccessAllEngines: false }, + }); + const wrapper = shallow(); + + expect(wrapper.find('#all_engines').prop('hidden')).toEqual(true); + }); + + it('controls the checked values for access radios', () => { + setMockValues({ + ...values, + fullEngineAccessChecked: true, + }); + const wrapper = shallow(); + + expect(wrapper.find('#all_engines').prop('checked')).toEqual(true); + expect(wrapper.find('#all_engines').prop('value')).toEqual('true'); + expect(wrapper.find('#specific_engines').prop('checked')).toEqual(false); + expect(wrapper.find('#specific_engines').prop('value')).toEqual('false'); + + setMockValues({ + ...values, + fullEngineAccessChecked: false, + }); + wrapper.setProps({}); // Re-render + + expect(wrapper.find('#all_engines').prop('checked')).toEqual(false); + expect(wrapper.find('#all_engines').prop('value')).toEqual('false'); + expect(wrapper.find('#specific_engines').prop('checked')).toEqual(true); + expect(wrapper.find('#specific_engines').prop('value')).toEqual('true'); + }); + + it('calls setAccessAllEngines when the radios are changed', () => { + const wrapper = shallow(); + + wrapper.find('#all_engines').simulate('change'); + expect(actions.setAccessAllEngines).toHaveBeenCalledWith(true); + + wrapper.find('#specific_engines').simulate('change'); + expect(actions.setAccessAllEngines).toHaveBeenCalledWith(false); + }); + + it('displays the engine selection panel if the limited access radio is selected', () => { + setMockValues({ + ...values, + fullEngineAccessChecked: false, + }); + const wrapper = shallow(); + + expect(wrapper.find(EngineSelection)).toHaveLength(1); + }); +}); + +describe('EngineSelection', () => { + const values = { + activeApiToken: { engines: [] }, + engines: [{ name: 'engine1' }, { name: 'engine2' }, { name: 'engine3' }], + }; + const actions = { + onEngineSelect: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h4').text()).toEqual('Select Engines'); + expect(wrapper.find(EuiCheckbox)).toHaveLength(3); + expect(wrapper.find(EuiCheckbox).first().prop('label')).toEqual('engine1'); + }); + + it('controls the engines checked state', () => { + setMockValues({ + ...values, + activeApiToken: { engines: ['engine3'] }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCheckbox).first().prop('checked')).toEqual(false); + expect(wrapper.find(EuiCheckbox).last().prop('checked')).toEqual(true); + }); + + it('calls onEngineSelect when the checkboxes are changed', () => { + const wrapper = shallow(); + + wrapper.find(EuiCheckbox).first().simulate('change'); + expect(actions.onEngineSelect).toHaveBeenCalledWith('engine1'); + + wrapper.find(EuiCheckbox).last().simulate('change'); + expect(actions.onEngineSelect).toHaveBeenCalledWith('engine3'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx new file mode 100644 index 0000000000000..88e345d0f9966 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx @@ -0,0 +1,133 @@ +/* + * 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 { useValues, useActions } from 'kea'; +import { + EuiFormRow, + EuiRadio, + EuiCheckbox, + EuiText, + EuiTitle, + EuiSpacer, + EuiPanel, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AppLogic } from '../../../../app_logic'; +import { CredentialsLogic } from '../../credentials_logic'; + +export const FormKeyEngineAccess: React.FC = () => { + const { myRole } = useValues(AppLogic); + const { setAccessAllEngines } = useActions(CredentialsLogic); + const { fullEngineAccessChecked } = useValues(CredentialsLogic); + + return ( + <> + + + <> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.fullAccess.label', + { defaultMessage: 'Full Engine Access' } + )} +

+
+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.fullAccess.helpText', + { defaultMessage: 'Access to all current and future Engines.' } + )} + + + } + hidden={!myRole.canAccessAllEngines} + checked={fullEngineAccessChecked} + value={fullEngineAccessChecked.toString()} + onChange={() => setAccessAllEngines(true)} + /> + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.limitedAccess.label', + { defaultMessage: 'Limited Engine Access' } + )} +

+
+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.limitedAccess.helpText', + { defaultMessage: 'Limit key access to specific Engines.' } + )} + + + } + checked={!fullEngineAccessChecked} + value={(!fullEngineAccessChecked).toString()} + onChange={() => setAccessAllEngines(false)} + /> + +
+ {!fullEngineAccessChecked && } + + ); +}; + +export const EngineSelection: React.FC = () => { + const { onEngineSelect } = useActions(CredentialsLogic); + const { activeApiToken, engines } = useValues(CredentialsLogic); + + return ( + <> + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.engineAccess.label', + { defaultMessage: 'Select Engines' } + )} +

+
+ + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formEngineAccess.engineAccess.helpText', + { defaultMessage: 'Engines which the key can access:' } + )} + + + {engines.map((engine) => ( + onEngineSelect(engine.name)} + /> + ))} +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx new file mode 100644 index 0000000000000..87f0f843dfa67 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; + +import { FormKeyName } from './'; + +describe('FormKeyName', () => { + const values = { + activeApiToken: { name: '' }, + activeApiTokenRawName: '', + activeApiTokenExists: false, + }; + const actions = { + setNameInputBlurred: jest.fn(), + setTokenName: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText)).toHaveLength(1); + expect(wrapper.find(EuiFieldText).prop('placeholder')).toEqual('i.e., my-engine-key'); + expect(wrapper.find(EuiFieldText).prop('value')).toEqual(''); + expect(wrapper.find(EuiFieldText).prop('disabled')).toEqual(false); + expect(wrapper.find(EuiFormRow).prop('helpText')).toEqual(''); + }); + + it('shows help text if the raw name does not match the expected name', () => { + setMockValues({ + ...values, + activeApiToken: { name: 'my-engine-key' }, + activeApiTokenRawName: 'my engine key!!', + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiFormRow).prop('helpText')).toEqual( + 'Your key will be named: my-engine-key' + ); + }); + + it('controls the input value', () => { + setMockValues({ + ...values, + activeApiTokenRawName: 'test', + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText).prop('value')).toEqual('test'); + }); + + it('disables the input if editing an existing key', () => { + setMockValues({ + ...values, + activeApiTokenExists: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText).prop('disabled')).toEqual(true); + }); + + it('calls setTokenName when the input value is changed', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('change', { target: { value: 'changed' } }); + + expect(actions.setTokenName).toHaveBeenCalledWith('changed'); + }); + + it('calls setNameInputBlurred when the user stops focusing the input', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('blur'); + + expect(actions.setNameInputBlurred).toHaveBeenCalledWith(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx new file mode 100644 index 0000000000000..fb8de2b244ecc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx @@ -0,0 +1,57 @@ +/* + * 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 { useValues, useActions } from 'kea'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../../credentials_logic'; + +export const FormKeyName: React.FC = () => { + const { setNameInputBlurred, setTokenName } = useActions(CredentialsLogic); + const { + activeApiToken: { name }, + activeApiTokenRawName: rawName, + activeApiTokenExists, + } = useValues(CredentialsLogic); + + return ( + + setTokenName(e.target.value)} + onBlur={() => setNameInputBlurred(true)} + autoComplete="off" + maxLength={64} + disabled={activeApiTokenExists} + required + fullWidth + autoFocus + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx new file mode 100644 index 0000000000000..2f1be1b07cbe1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiCheckbox } from '@elastic/eui'; + +import { FormKeyReadWriteAccess } from './'; + +describe('FormKeyReadWriteAccess', () => { + const values = { + activeApiToken: { read: false, write: false }, + }; + const actions = { + setTokenReadWrite: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('h3').text()).toEqual('Read and Write Access Levels'); + expect(wrapper.find(EuiCheckbox)).toHaveLength(2); + }); + + it('controls the checked state for the read checkbox', () => { + setMockValues({ + ...values, + activeApiToken: { read: true, write: false }, + }); + const wrapper = shallow(); + + expect(wrapper.find('#read').prop('checked')).toEqual(true); + expect(wrapper.find('#write').prop('checked')).toEqual(false); + }); + + it('controls the checked state for the write checkbox', () => { + setMockValues({ + ...values, + activeApiToken: { read: false, write: true }, + }); + const wrapper = shallow(); + + expect(wrapper.find('#read').prop('checked')).toEqual(false); + expect(wrapper.find('#write').prop('checked')).toEqual(true); + }); + + it('calls setTokenReadWrite when the checkboxes are changed', () => { + const wrapper = shallow(); + + wrapper.find('#read').simulate('change', { target: { name: 'read', checked: true } }); + expect(actions.setTokenReadWrite).toHaveBeenCalledWith({ name: 'read', checked: true }); + + wrapper.find('#write').simulate('change', { target: { name: 'write', checked: false } }); + expect(actions.setTokenReadWrite).toHaveBeenCalledWith({ name: 'write', checked: false }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx new file mode 100644 index 0000000000000..a02b00b6ad377 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx @@ -0,0 +1,59 @@ +/* + * 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 { useValues, useActions } from 'kea'; +import { EuiCheckbox, EuiText, EuiTitle, EuiSpacer, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CredentialsLogic } from '../../credentials_logic'; +import { ITokenReadWrite } from '../../types'; + +export const FormKeyReadWriteAccess: React.FC = () => { + const { setTokenReadWrite } = useActions(CredentialsLogic); + const { activeApiToken } = useValues(CredentialsLogic); + + return ( + <> + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.formReadWrite.label', { + defaultMessage: 'Read and Write Access Levels', + })} +

+
+ + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.formReadWrite.helpText', { + defaultMessage: 'Only applies to Private API Keys.', + })} + + + setTokenReadWrite(e.target as ITokenReadWrite)} + label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formReadWrite.readLabel', + { defaultMessage: 'Read Access' } + )} + /> + setTokenReadWrite(e.target as ITokenReadWrite)} + label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formReadWrite.writeLabel', + { defaultMessage: 'Write Access' } + )} + /> +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx new file mode 100644 index 0000000000000..d07a705b2d90b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx @@ -0,0 +1,80 @@ +/* + * 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 { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiSelect } from '@elastic/eui'; + +import { ApiTokenTypes, TOKEN_TYPE_INFO } from '../../constants'; +import { FormKeyType } from './'; + +describe('FormKeyType', () => { + const values = { + myRole: { credentialTypes: ['search', 'private', 'admin'] }, + activeApiToken: { type: ApiTokenTypes.Private }, + activeApiTokenExists: false, + }; + const actions = { + setTokenType: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect)).toHaveLength(1); + expect(wrapper.find(EuiSelect).prop('placeholder')).toEqual('Select a key type'); + expect(wrapper.find(EuiSelect).prop('options')).toEqual(TOKEN_TYPE_INFO); + expect(wrapper.find(EuiSelect).prop('value')).toEqual(ApiTokenTypes.Private); + expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(false); + }); + + it('only shows the type options that the user has access to', () => { + setMockValues({ + ...values, + myRole: { credentialTypes: ['search'] }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('options')).toEqual([ + expect.objectContaining({ value: ApiTokenTypes.Search }), + ]); + }); + + it('controls the select value', () => { + setMockValues({ + ...values, + activeApiToken: { type: ApiTokenTypes.Search }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('value')).toEqual(ApiTokenTypes.Search); + }); + + it('disables the select if editing an existing key', () => { + setMockValues({ + ...values, + activeApiTokenExists: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiSelect).prop('disabled')).toEqual(true); + }); + + it('calls setTokenType when the select value is changed', () => { + const wrapper = shallow(); + wrapper.find(EuiSelect).simulate('change', { target: { value: ApiTokenTypes.Admin } }); + + expect(actions.setTokenType).toHaveBeenCalledWith(ApiTokenTypes.Admin); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx new file mode 100644 index 0000000000000..7268c12614e8b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx @@ -0,0 +1,63 @@ +/* + * 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 { useValues, useActions } from 'kea'; +import { EuiFormRow, EuiSelect, EuiText, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { AppLogic } from '../../../../app_logic'; +import { CredentialsLogic } from '../../credentials_logic'; +import { TOKEN_TYPE_DESCRIPTION, TOKEN_TYPE_INFO, DOCS_HREF } from '../../constants'; + +export const FormKeyType: React.FC = () => { + const { myRole } = useValues(AppLogic); + const { setTokenType } = useActions(CredentialsLogic); + const { activeApiToken, activeApiTokenExists } = useValues(CredentialsLogic); + + const tokenDescription = TOKEN_TYPE_DESCRIPTION[activeApiToken.type]; + const tokenOptions = TOKEN_TYPE_INFO.filter((typeInfo) => + myRole?.credentialTypes?.includes(typeInfo.value) + ); + + return ( + +

+ {tokenDescription}{' '} + + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.documentationLink1', { + defaultMessage: 'Visit the documentation', + })} + {' '} + {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.documentationLink2', { + defaultMessage: 'to learn more about keys.', + })} +

+ + } + > + setTokenType(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.credentials.formType.placeholder', + { defaultMessage: 'Select a key type' } + )} + disabled={activeApiTokenExists} + required + fullWidth + /> +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx new file mode 100644 index 0000000000000..c0ff892c220c7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx @@ -0,0 +1,18 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiCallOut } from '@elastic/eui'; + +import { FormKeyUpdateWarning } from './'; + +describe('FormKeyUpdateWarning', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx new file mode 100644 index 0000000000000..7e7aaa583325d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx @@ -0,0 +1,29 @@ +/* + * 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 { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const FormKeyUpdateWarning: React.FC = () => ( + <> + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.credentials.updateWarning', { + defaultMessage: + 'Existing API keys may be shared between users. Changing permissions for this key will affect all users who have access to this key.', + })} +

+
+ +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 30b5fabc4d0c4..7b8b864b3a317 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -21,7 +21,7 @@ import { IMeta } from '../../../../../common/types'; import { IEngine } from '../../types'; import { IApiToken, ICredentialsDetails, ITokenReadWrite } from './types'; -const defaultApiToken: IApiToken = { +export const defaultApiToken: IApiToken = { name: '', type: ApiTokenTypes.Private, read: true, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index b2a17515bbb96..67df745e619d4 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -18,8 +18,9 @@ import { import { NodeDetailStatus } from '../node_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; -export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { +export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props }) => { const metricsToShow = [ metrics.node_gc, metrics.node_gc_time, @@ -50,9 +51,25 @@ export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { - + + state.nodeId === nodeId || state.stackProductUuid === nodeId + } + /> + state.nodeId === nodeId || state.stackProductUuid === nodeId} + nextStepsFilter={(nextStep) => { + if (nextStep.text.includes('Elasticsearch nodes')) { + return false; + } + return true; + }} + /> {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index a84cae43c2dc4..7e78170d1117f 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -121,6 +121,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { { - let cpuUsage = 0; if (this.config.ui.container.elasticsearch.enabled) { - cpuUsage = + stat.cpuUsage = (stat.containerUsage / (stat.containerPeriods * stat.containerQuota * 1000)) * 100; - } else { - cpuUsage = stat.cpuUsage; } return { instanceKey: `${stat.clusterUuid}:${stat.nodeId}`, clusterUuid: stat.clusterUuid, - shouldFire: cpuUsage > params.threshold, + shouldFire: stat.cpuUsage > params.threshold, severity: AlertSeverity.Danger, meta: stat, ccs: stat.ccs, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts index 12926a30efa1b..88035c1121848 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.test.ts @@ -97,11 +97,18 @@ describe('fetchCpuUsageNodeStats', () => { }, ], }, - average_usage: { - value: 10, - }, - average_periods: { - value: 5, + histo: { + buckets: [ + null, + { + usage_deriv: { + normalized_value: 10, + }, + periods_deriv: { + normalized_value: 5, + }, + }, + ], }, average_quota: { value: 50, @@ -185,14 +192,14 @@ describe('fetchCpuUsageNodeStats', () => { }); await fetchCpuUsageNodeStats(callCluster, clusters, index, startMs, endMs, size); expect(params).toStrictEqual({ - index, + index: '.monitoring-es-*', filterPath: ['aggregations'], body: { size: 0, query: { bool: { filter: [ - { terms: { cluster_uuid: clusters.map((cluster) => cluster.clusterUuid) } }, + { terms: { cluster_uuid: ['abc123'] } }, { term: { type: 'node_stats' } }, { range: { timestamp: { format: 'epoch_millis', gte: 0, lte: 0 } } }, ], @@ -200,23 +207,38 @@ describe('fetchCpuUsageNodeStats', () => { }, aggs: { clusters: { - terms: { - field: 'cluster_uuid', - size, - include: clusters.map((cluster) => cluster.clusterUuid), - }, + terms: { field: 'cluster_uuid', size: 10, include: ['abc123'] }, aggs: { nodes: { - terms: { field: 'node_stats.node_id', size }, + terms: { field: 'node_stats.node_id', size: 10 }, aggs: { index: { terms: { field: '_index', size: 1 } }, average_cpu: { avg: { field: 'node_stats.process.cpu.percent' } }, - average_usage: { avg: { field: 'node_stats.os.cgroup.cpuacct.usage_nanos' } }, - average_periods: { - avg: { field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods' }, - }, average_quota: { avg: { field: 'node_stats.os.cgroup.cpu.cfs_quota_micros' } }, name: { terms: { field: 'source_node.name', size: 1 } }, + histo: { + date_histogram: { field: 'timestamp', fixed_interval: '0m' }, + aggs: { + average_periods: { + max: { field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods' }, + }, + average_usage: { max: { field: 'node_stats.os.cgroup.cpuacct.usage_nanos' } }, + usage_deriv: { + derivative: { + buckets_path: 'average_usage', + gap_policy: 'skip', + unit: '1s', + }, + }, + periods_deriv: { + derivative: { + buckets_path: 'average_periods', + gap_policy: 'skip', + unit: '1s', + }, + }, + }, + }, }, }, }, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts index 4fdb03b61950e..ecd324c083a8c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cpu_usage_node_stats.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { get } from 'lodash'; +import moment from 'moment'; +import { NORMALIZED_DERIVATIVE_UNIT } from '../../../common/constants'; import { AlertCluster, AlertCpuUsageNodeStats } from '../../alerts/types'; interface NodeBucketESResponse { @@ -26,6 +28,9 @@ export async function fetchCpuUsageNodeStats( endMs: number, size: number ): Promise { + // Using pure MS didn't seem to work well with the date_histogram interval + // but minutes does + const intervalInMinutes = moment.duration(endMs - startMs).asMinutes(); const filterPath = ['aggregations']; const params = { index, @@ -82,16 +87,6 @@ export async function fetchCpuUsageNodeStats( field: 'node_stats.process.cpu.percent', }, }, - average_usage: { - avg: { - field: 'node_stats.os.cgroup.cpuacct.usage_nanos', - }, - }, - average_periods: { - avg: { - field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods', - }, - }, average_quota: { avg: { field: 'node_stats.os.cgroup.cpu.cfs_quota_micros', @@ -103,6 +98,38 @@ export async function fetchCpuUsageNodeStats( size: 1, }, }, + histo: { + date_histogram: { + field: 'timestamp', + fixed_interval: `${intervalInMinutes}m`, + }, + aggs: { + average_periods: { + max: { + field: 'node_stats.os.cgroup.cpu.stat.number_of_elapsed_periods', + }, + }, + average_usage: { + max: { + field: 'node_stats.os.cgroup.cpuacct.usage_nanos', + }, + }, + usage_deriv: { + derivative: { + buckets_path: 'average_usage', + gap_policy: 'skip', + unit: NORMALIZED_DERIVATIVE_UNIT, + }, + }, + periods_deriv: { + derivative: { + buckets_path: 'average_periods', + gap_policy: 'skip', + unit: NORMALIZED_DERIVATIVE_UNIT, + }, + }, + }, + }, }, }, }, @@ -120,17 +147,19 @@ export async function fetchCpuUsageNodeStats( ) as ClusterBucketESResponse[]; for (const clusterBucket of clusterBuckets) { for (const node of clusterBucket.nodes.buckets) { + const lastBucket = get(node, 'histo.buckets[1]', {}); const indexName = get(node, 'index.buckets[0].key', ''); - stats.push({ + const stat = { clusterUuid: clusterBucket.key, nodeId: node.key, nodeName: get(node, 'name.buckets[0].key'), cpuUsage: get(node, 'average_cpu.value'), - containerUsage: get(node, 'average_usage.value'), - containerPeriods: get(node, 'average_periods.value'), + containerUsage: get(lastBucket, 'usage_deriv.normalized_value'), + containerPeriods: get(lastBucket, 'periods_deriv.normalized_value'), containerQuota: get(node, 'average_quota.value'), ccs: indexName.includes(':') ? indexName.split(':')[0] : null, - }); + }; + stats.push(stat); } } return stats; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 005eebbfbf3bb..eb6d25c9ef5ab 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -66,6 +66,11 @@ describe('', () => { kibana: [{ base: [], spaces: [], feature: {} }], transient_metadata: { enabled: false }, }, + { + name: 'special%chars%role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, ]); }); @@ -121,7 +126,7 @@ describe('', () => { expect(wrapper.find(PermissionDenied)).toMatchSnapshot(); }); - it('renders role actions as appropriate', async () => { + it('renders role actions as appropriate, escaping when necessary', async () => { const wrapper = mountWithIntl( ', () => { expect(wrapper.find(PermissionDenied)).toHaveLength(0); - const editButton = wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]'); + let editButton = wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-test-role-1"]'); expect(editButton).toHaveLength(1); expect(editButton.prop('href')).toBe('/edit/test-role-1'); - const cloneButton = wrapper.find( - 'EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]' + editButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="edit-role-action-special%chars%role"]' ); + expect(editButton).toHaveLength(1); + expect(editButton.prop('href')).toBe('/edit/special%25chars%25role'); + + let cloneButton = wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-test-role-1"]'); expect(cloneButton).toHaveLength(1); expect(cloneButton.prop('href')).toBe('/clone/test-role-1'); + cloneButton = wrapper.find( + 'EuiButtonIcon[data-test-subj="clone-role-action-special%chars%role"]' + ); + expect(cloneButton).toHaveLength(1); + expect(cloneButton.prop('href')).toBe('/clone/special%25chars%25role'); + expect( wrapper.find('EuiButtonIcon[data-test-subj="edit-role-action-disabled-role"]') ).toHaveLength(1); @@ -182,6 +197,11 @@ describe('', () => { kibana: [{ base: [], spaces: [], feature: {} }], metadata: { _reserved: true }, }, + { + name: 'special%chars%role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, { name: 'test-role-1', elasticsearch: { cluster: [], indices: [], run_as: [] }, @@ -198,6 +218,11 @@ describe('', () => { kibana: [{ base: [], spaces: [], feature: {} }], transient_metadata: { enabled: false }, }, + { + name: 'special%chars%role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, { name: 'test-role-1', elasticsearch: { cluster: [], indices: [], run_as: [] }, diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 4c8f45d7eea43..1e733dfa0767e 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -58,7 +58,7 @@ interface State { } const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { - return `/${action}${roleName ? `/${roleName}` : ''}`; + return `/${action}${roleName ? `/${encodeURIComponent(roleName)}` : ''}`; }; export class RolesGridPage extends Component { diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index df8fe8cee7699..0cc3146172f70 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -71,6 +71,38 @@ describe('UsersGridPage', () => { expect(findTestSubject(wrapper, 'userDisabled')).toHaveLength(0); }); + it('generates valid links when usernames contain special characters', async () => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { + return Promise.resolve([ + { + username: 'username with some fun characters!@#$%^&*()', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: true, + }, + ]); + }); + + const wrapper = mountWithIntl( + + ); + + await waitForRender(wrapper); + + const link = findTestSubject(wrapper, 'userRowUserName'); + expect(link.props().href).toMatchInlineSnapshot( + `"/edit/username%20with%20some%20fun%20characters!%40%23%24%25%5E%26*()"` + ); + }); + it('renders a forbidden message if user is not authorized', async () => { const apiClient = userAPIClientMock.create(); apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index fbbd8dc0e2c57..debe1c5e82d3b 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -112,7 +112,7 @@ export class UsersGridPage extends Component { render: (username: string) => ( {username} diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json new file mode 100644 index 0000000000000..476f5926f757a --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "server/**/*", + "../../../typings/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry/tsconfig.json" } + ] +} + diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5cabdd62d7c87..e9e0f3aed6457 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20272,7 +20272,6 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} はベータ段階で、変更される可能性があります。デザインとコードはオフィシャル GA 機能よりも完成度が低く、現状のまま保証なしで提供されています。ベータ機能にはオフィシャル GA 機能の SLA が適用されません。", - "xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", @@ -20280,7 +20279,6 @@ "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", - "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 229938a3c1d08..48797d2b1e941 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20292,7 +20292,6 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", "xpack.triggersActionsUI.sections.alertAdd.betaBadgeTooltipContent": "{pluginName} 为公测版,可能会进行更改。设计和代码相对于正式发行版功能还不够成熟,将按原样提供,且不提供任何保证。公测版功能不受正式发行版功能支持 SLA 的约束。", - "xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", @@ -20300,7 +20299,6 @@ "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "完成表达式以生成预览。", - "xpack.triggersActionsUI.sections.alertAdd.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx index 1d908920db8b0..a7de73c9aab29 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.test.tsx @@ -10,6 +10,7 @@ import { HealthCheck } from './health_check'; import { act } from 'react-dom/test-utils'; import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { HealthContextProvider } from '../context/health_context'; const docLinks = { ELASTIC_WEBSITE_URL: 'elastic.co/', DOC_LINK_VERSION: 'current' }; @@ -20,9 +21,11 @@ describe('health check', () => { http.get.mockImplementationOnce(() => new Promise(() => {})); const { queryByText, container } = render( - -

{'shouldnt render'}

-
+ + +

{'shouldnt render'}

+
+
); await act(async () => { // wait for useEffect to run @@ -32,13 +35,33 @@ describe('health check', () => { expect(queryByText('shouldnt render')).not.toBeInTheDocument(); }); + it('renders children immediately if waitForCheck is false', async () => { + http.get.mockImplementationOnce(() => new Promise(() => {})); + + const { queryByText, container } = render( + + +

{'should render'}

+
+
+ ); + await act(async () => { + // wait for useEffect to run + }); + + expect(container.getElementsByClassName('euiLoadingSpinner').length).toBe(0); + expect(queryByText('should render')).toBeInTheDocument(); + }); + it('renders children if keys are enabled', async () => { http.get.mockResolvedValue({ isSufficientlySecure: true, hasPermanentEncryptionKey: true }); const { queryByText } = render( - -

{'should render'}

-
+ + +

{'should render'}

+
+
); await act(async () => { // wait for useEffect to run @@ -53,9 +76,11 @@ describe('health check', () => { })); const { queryAllByText } = render( - -

{'should render'}

-
+ + +

{'should render'}

+
+
); await act(async () => { // wait for useEffect to run @@ -81,9 +106,11 @@ describe('health check', () => { })); const { queryByText, queryByRole } = render( - -

{'should render'}

-
+ + +

{'should render'}

+
+
); await act(async () => { // wait for useEffect to run @@ -108,9 +135,11 @@ describe('health check', () => { })); const { queryByText } = render( - -

{'should render'}

-
+ + +

{'should render'}

+
+
); await act(async () => { // wait for useEffect to run diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 009f582424765..c4d0b4976266e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -18,33 +18,39 @@ import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { AlertingFrameworkHealth } from '../../types'; import { health } from '../lib/alert_api'; import './health_check.scss'; +import { useHealthContext } from '../context/health_context'; interface Props { docLinks: Pick; http: HttpSetup; inFlyout?: boolean; + waitForCheck: boolean; } export const HealthCheck: React.FunctionComponent = ({ docLinks, http, children, + waitForCheck, inFlyout = false, }) => { + const { setLoadingHealthCheck } = useHealthContext(); const [alertingHealth, setAlertingHealth] = React.useState>(none); React.useEffect(() => { (async function () { + setLoadingHealthCheck(true); setAlertingHealth(some(await health({ http }))); + setLoadingHealthCheck(false); })(); - }, [http]); + }, [http, setLoadingHealthCheck]); const className = inFlyout ? 'alertingFlyoutHealthCheck' : 'alertingHealthCheck'; return pipe( alertingHealth, fold( - () => , + () => (waitForCheck ? : {children}), (healthCheck) => { return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( {children} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/health_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/health_context.tsx new file mode 100644 index 0000000000000..de27f6db761e8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/health_context.tsx @@ -0,0 +1,45 @@ +/* + * 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, { createContext, useCallback, useContext, useMemo, useState } from 'react'; + +export interface HealthContextValue { + loadingHealthCheck: boolean; + setLoadingHealthCheck: (loading: boolean) => void; +} + +const defaultHealthContext: HealthContextValue = { + loadingHealthCheck: false, + setLoadingHealthCheck: (loading: boolean) => { + throw new Error( + 'setLoadingHealthCheck was not initialized, set it when you invoke the context' + ); + }, +}; + +const HealthContext = createContext(defaultHealthContext); + +export const HealthContextProvider = ({ children }: { children: React.ReactNode }) => { + const [loading, setLoading] = useState(false); + + const setLoadingHealthCheck = useCallback((isLoading: boolean) => { + setLoading(isLoading); + }, []); + + const value = useMemo(() => { + return { loadingHealthCheck: loading, setLoadingHealthCheck }; + }, [loading, setLoadingHealthCheck]); + + return {children}; +}; + +export const useHealthContext = () => { + const ctx = useContext(HealthContext); + if (!ctx) { + throw new Error('HealthContext has not been set.'); + } + return ctx; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index eb6b1ada3ba93..f009a04d40978 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -31,6 +31,7 @@ import { ActionsConnectorsList } from './sections/actions_connectors_list/compon import { AlertsList } from './sections/alerts_list/components/alerts_list'; import { PLUGIN } from './constants/plugin'; import { HealthCheck } from './components/health_check'; +import { HealthContextProvider } from './context/health_context'; interface MatchParams { section: Section; @@ -139,9 +140,11 @@ export const TriggersActionsUIHome: React.FunctionComponent ( - - - + + + + + )} /> )} @@ -149,9 +152,11 @@ export const TriggersActionsUIHome: React.FunctionComponent ( - - - + + + + + )} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 7be7e60c2e19c..763462ba6ebf4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -10,11 +10,6 @@ import { EuiTitle, EuiFlyoutHeader, EuiFlyout, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, EuiFlyoutBody, EuiPortal, EuiBetaBadge, @@ -29,6 +24,8 @@ import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; import { ConfirmAlertSave } from './confirm_alert_save'; import { hasShowActionsCapability } from '../../lib/capabilities'; +import AlertAddFooter from './alert_add_footer'; +import { HealthContextProvider } from '../../context/health_context'; interface AlertAddProps { consumer: string; @@ -183,54 +180,37 @@ export const AlertAdd = ({ - - - + + + + + { + setIsSaving(true); + if (shouldConfirmSave) { + setIsConfirmAlertSaveModalOpen(true); + } else { + await saveAlertAndCloseFlyout(); + } + }} + onCancel={closeFlyout} /> - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertAdd.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - if (shouldConfirmSave) { - setIsConfirmAlertSaveModalOpen(true); - } else { - await saveAlertAndCloseFlyout(); - } - }} - > - - - - - - + + {isConfirmAlertSaveModalOpen && ( { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx new file mode 100644 index 0000000000000..92e1198de8440 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add_footer.tsx @@ -0,0 +1,61 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useHealthContext } from '../../context/health_context'; + +interface AlertAddFooterProps { + isSaving: boolean; + hasErrors: boolean; + onSave: () => void; + onCancel: () => void; +} + +export const AlertAddFooter = ({ isSaving, hasErrors, onSave, onCancel }: AlertAddFooterProps) => { + const { loadingHealthCheck } = useHealthContext(); + + return ( + + + + + {i18n.translate('xpack.triggersActionsUI.sections.alertAddFooter.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { AlertAddFooter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index b60aa04ee9f27..0435a4cc33cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -28,6 +28,7 @@ import { alertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; +import { HealthContextProvider } from '../../context/health_context'; interface AlertEditProps { initialAlert: Alert; @@ -135,74 +136,82 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { - - - {hasActionsDisabled && ( - - - - - )} - - - - - - - {i18n.translate('xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', { - defaultMessage: 'Cancel', - })} - - - - { - setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); - } - } - }} - > - + + + {hasActionsDisabled && ( + + - - - - - + + + )} + + + + + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.alertEdit.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + + + { + setIsSaving(true); + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } + } + }} + > + + + + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 8800f149c033b..d2ca0abe566ad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -105,9 +105,7 @@ export const AlertForm = ({ } = alertsContext; const canShowActions = hasShowActionsCapability(capabilities); - const [alertTypeModel, setAlertTypeModel] = useState( - alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null - ); + const [alertTypeModel, setAlertTypeModel] = useState(null); const [alertTypesIndex, setAlertTypesIndex] = useState(undefined); const [alertInterval, setAlertInterval] = useState( @@ -149,6 +147,10 @@ export const AlertForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + setAlertTypeModel(alert.alertTypeId ? alertTypeRegistry.get(alert.alertTypeId) : null); + }, [alert, alertTypeRegistry]); + const setAlertProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); }; diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js index c304b177cb04b..a58f66214e772 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }) { const nodesList = getService('monitoringElasticsearchNodes'); const nodeDetail = getService('monitoringElasticsearchNodeDetail'); - // Failing: See https://github.com/elastic/kibana/issues/81166 - describe.skip('Elasticsearch node detail', () => { + describe('Elasticsearch node detail', () => { describe('Active Nodes', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 297eb2e9b4540..7bd38ea4afab7 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../../src/plugins/telemetry/tsconfig.json" }, { "path": "../../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 79309369386cf..382d4c073d41e 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -13,7 +13,8 @@ "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", "plugins/licensing/**/*", - "plugins/global_search/**/*" + "plugins/global_search/**/*", + "plugins/telemetry_collection_xpack/**/*" ], "compilerOptions": { "paths": { @@ -35,6 +36,7 @@ { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, { "path": "../src/plugins/kibana_usage_collection/tsconfig.json" }, + { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index a389bbcf0272b..a4dfa15a3d8ab 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -3,5 +3,6 @@ "references": [ { "path": "./plugins/licensing/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, + { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, ] }