From 8fa9962641dd0035a33a8cdf5489a062a6b2d475 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 6 Mar 2020 12:16:06 -0500 Subject: [PATCH 01/20] Ensure logged out starting state for tests that need it (#59322) --- x-pack/test/functional/apps/spaces/enter_space.ts | 5 ++++- x-pack/test/functional/apps/spaces/spaces_selection.ts | 5 ++++- x-pack/test/functional/page_objects/monitoring_page.js | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index b931a5cb0ca6..e0b1ec544d46 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -15,7 +15,10 @@ export default function enterSpaceFunctonalTests({ describe('Enter Space', function() { this.tags('smoke'); - before(async () => await esArchiver.load('spaces/enter_space')); + before(async () => { + await esArchiver.load('spaces/enter_space'); + await PageObjects.security.forceLogout(); + }); after(async () => await esArchiver.unload('spaces/enter_space')); afterEach(async () => { diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 5af9bc135ae2..7b4a1e6e2b8a 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -23,7 +23,10 @@ export default function spaceSelectorFunctonalTests({ describe('Spaces', function() { this.tags('smoke'); describe('Space Selector', () => { - before(async () => await esArchiver.load('spaces/selector')); + before(async () => { + await esArchiver.load('spaces/selector'); + await PageObjects.security.forceLogout(); + }); after(async () => await esArchiver.unload('spaces/selector')); afterEach(async () => { diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 8de5b5e69d34..323c01e23488 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -5,7 +5,7 @@ */ export function MonitoringPageProvider({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'header', 'shield', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'header', 'security', 'shield', 'spaceSelector']); const testSubjects = getService('testSubjects'); const security = getService('security'); @@ -19,7 +19,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { }); if (!useSuperUser) { - await PageObjects.common.navigateToApp('login'); + await PageObjects.security.forceLogout(); await PageObjects.shield.login('basic_monitoring_user', 'monitoring_user_password'); } await PageObjects.common.navigateToApp('monitoring'); From fbbb3f809108ac2016dfe54bd2d4a18c74dde021 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 6 Mar 2020 11:43:15 -0700 Subject: [PATCH 02/20] [Reporting] Improve the page exit error messages (#59351) Co-authored-by: Elastic Machine --- .../browsers/chromium/driver_factory/index.ts | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 1a57408f41dd..11b70c82f6fa 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -3,27 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { i18n } from '@kbn/i18n'; +import del from 'del'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { Browser, - Page, - LaunchOptions, ConsoleMessage, + LaunchOptions, + Page, Request as PuppeteerRequest, } from 'puppeteer'; -import del from 'del'; import * as Rx from 'rxjs'; -import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; - +import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { BrowserConfig, CaptureConfig } from '../../../../types'; import { LevelLogger as Logger } from '../../../lib/level_logger'; -import { HeadlessChromiumDriver } from '../driver'; import { safeChildProcess } from '../../safe_child_process'; -import { puppeteerLaunch } from '../puppeteer'; +import { HeadlessChromiumDriver } from '../driver'; import { getChromeLogLocation } from '../paths'; +import { puppeteerLaunch } from '../puppeteer'; import { args } from './args'; type binaryPath = string; @@ -216,17 +217,35 @@ export class HeadlessChromiumDriverFactory { } getPageExit(browser: Browser, page: Page) { - const pageError$ = Rx.fromEvent(page, 'error').pipe(mergeMap(err => Rx.throwError(err))); + const pageError$ = Rx.fromEvent(page, 'error').pipe( + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { + defaultMessage: 'Reporting detected an error: {err}', + values: { err: err.toString() }, + }) + ); + }) + ); const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( - mergeMap(err => Rx.throwError(err)) + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { + defaultMessage: `Reporting detected an error on the page: {err}`, + values: { err: err.toString() }, + }) + ); + }) ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( mergeMap(() => Rx.throwError( new Error( - `Puppeteer was disconnected from the Chromium instance! Chromium has closed or crashed.` + i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { + defaultMessage: `Reporting detected that Chromium has closed.`, + }) ) ) ) From 3c4cf56008a381f9824d8cf5eae1725d39a6c8da Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 6 Mar 2020 19:46:47 +0100 Subject: [PATCH 03/20] [Drilldowns] Component (#59032) --- .../storybook_config/webpack.config.js | 35 +++- src/dev/storybook/aliases.ts | 1 + .../action_wizard/action_wizard.scss | 10 + .../action_wizard/action_wizard.story.tsx | 33 +++ .../action_wizard/action_wizard.test.tsx | 64 ++++++ .../action_wizard/action_wizard.tsx | 196 ++++++++++++++++++ .../public/components/action_wizard/i18n.ts | 14 ++ .../public/components/action_wizard/index.ts | 7 + .../components/action_wizard/test_data.tsx | 171 +++++++++++++++ .../advanced_ui_actions/scripts/storybook.js | 13 ++ 10 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx create mode 100644 x-pack/plugins/advanced_ui_actions/scripts/storybook.js diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 72ff9162ffe6..1531c1d22b01 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { resolve } = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); // eslint-disable-next-line import/no-unresolved @@ -72,6 +73,38 @@ module.exports = async ({ config }) => { ], }); + // Enable SASS + config.module.rules.push({ + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: resolve(REPO_ROOT, 'src/optimize/'), + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], + }); + // Reference the built DLL file of static(ish) dependencies, which are removed // during kbn:bootstrap and rebuilt if missing. config.plugins.push( @@ -96,7 +129,7 @@ module.exports = async ({ config }) => { ); // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx'); + config.resolve.extensions.push('.ts', '.tsx', '.scss'); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 35ac4e27f9c8..8ed64f004c9b 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -25,4 +25,5 @@ export const storybookAliases = { embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js', + ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', }; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss new file mode 100644 index 000000000000..2ba6f9baca90 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss @@ -0,0 +1,10 @@ +.auaActionWizard__selectedActionFactoryContainer { + background-color: $euiColorLightestShade; + padding: $euiSize; +} + +.auaActionWizard__actionFactoryItem { + .euiKeyPadMenuItem__label { + height: #{$euiSizeXL}; + } +} diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx new file mode 100644 index 000000000000..62f16890cade --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx @@ -0,0 +1,33 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { dashboardDrilldownActionFactory, Demo, urlDrilldownActionFactory } from './test_data'; + +storiesOf('components/ActionWizard', module) + .add('default', () => ( + + )) + .add('Only one factory is available', () => ( + // to make sure layout doesn't break + + )) + .add('Long list of action factories', () => ( + // to make sure layout doesn't break + + )); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx new file mode 100644 index 000000000000..aea47be693b8 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { cleanup, fireEvent, render } from '@testing-library/react/pure'; +import '@testing-library/jest-dom/extend-expect'; // TODO: this should be global +import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard'; +import { + dashboardDrilldownActionFactory, + dashboards, + Demo, + urlDrilldownActionFactory, +} from './test_data'; + +// TODO: afterEach is not available for it globally during setup +// https://github.com/elastic/kibana/issues/59469 +afterEach(cleanup); + +test('Pick and configure action', () => { + const screen = render( + + ); + + // check that all factories are displayed to pick + expect(screen.getAllByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).toHaveLength(2); + + // select URL one + fireEvent.click(screen.getByText(/Go to URL/i)); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + // change to dashboard + fireEvent.click(screen.getByText(/change/i)); + fireEvent.click(screen.getByText(/Go to Dashboard/i)); + + // Select dashboard + fireEvent.change(screen.getByLabelText(/Choose destination dashboard/i), { + target: { value: dashboards[1].id }, + }); +}); + +test('If only one actions factory is available then actionFactory selection is emitted without user input', () => { + const screen = render(); + + // check that no factories are displayed to pick from + expect(screen.queryByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).not.toBeInTheDocument(); + expect(screen.queryByTestId(TEST_SUBJ_SELECTED_ACTION_FACTORY)).toBeInTheDocument(); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + // check that can't change to action factory type + expect(screen.queryByTestId(/change/i)).not.toBeInTheDocument(); +}); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx new file mode 100644 index 000000000000..41ef863c00e4 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -0,0 +1,196 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiText, + EuiKeyPadMenuItemButton, +} from '@elastic/eui'; +import { txtChangeButton } from './i18n'; +import './action_wizard.scss'; + +// TODO: this interface is temporary for just moving forward with the component +// and it will be imported from the ../ui_actions when implemented properly +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ActionBaseConfig = {}; +export interface ActionFactory { + type: string; // TODO: type should be tied to Action and ActionByType + displayName: string; + iconType?: string; + wizard: React.FC>; + createConfig: () => Config; + isValid: (config: Config) => boolean; +} + +export interface ActionFactoryWizardProps { + config?: Config; + + /** + * Callback called when user updates the config in UI. + */ + onConfig: (config: Config) => void; +} + +export interface ActionWizardProps { + /** + * List of available action factories + */ + actionFactories: Array>; // any here to be able to pass array of ActionFactory with different configs + + /** + * Currently selected action factory + * undefined - is allowed and means that non is selected + */ + currentActionFactory?: ActionFactory; + /** + * Action factory selected changed + * null - means user click "change" and removed action factory selection + */ + onActionFactoryChange: (actionFactory: ActionFactory | null) => void; + + /** + * current config for currently selected action factory + */ + config?: ActionBaseConfig; + + /** + * config changed + */ + onConfigChange: (config: ActionBaseConfig) => void; +} + +export const ActionWizard: React.FC = ({ + currentActionFactory, + actionFactories, + onActionFactoryChange, + onConfigChange, + config, +}) => { + // auto pick action factory if there is only 1 available + if (!currentActionFactory && actionFactories.length === 1) { + onActionFactoryChange(actionFactories[0]); + } + + if (currentActionFactory && config) { + return ( + 1} + onDeselect={() => { + onActionFactoryChange(null); + }} + config={config} + onConfigChange={newConfig => { + onConfigChange(newConfig); + }} + /> + ); + } + + return ( + { + onActionFactoryChange(actionFactory); + }} + /> + ); +}; + +interface SelectedActionFactoryProps { + actionFactory: ActionFactory; + config: Config; + onConfigChange: (config: Config) => void; + showDeselect: boolean; + onDeselect: () => void; +} + +export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selected-action-factory'; + +const SelectedActionFactory: React.FC = ({ + actionFactory, + onDeselect, + showDeselect, + onConfigChange, + config, +}) => { + return ( +
+
+ + {actionFactory.iconType && ( + + + + )} + + +

{actionFactory.displayName}

+
+
+ {showDeselect && ( + + onDeselect()}> + {txtChangeButton} + + + )} +
+
+ +
+ {actionFactory.wizard({ + config, + onConfig: onConfigChange, + })} +
+
+ ); +}; + +interface ActionFactorySelectorProps { + actionFactories: ActionFactory[]; + onActionFactorySelected: (actionFactory: ActionFactory) => void; +} + +export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'action-factory-item'; + +const ActionFactorySelector: React.FC = ({ + actionFactories, + onActionFactorySelected, +}) => { + if (actionFactories.length === 0) { + // this is not user facing, as it would be impossible to get into this state + // just leaving for dev purposes for troubleshooting + return
No action factories to pick from
; + } + + return ( + + {actionFactories.map(actionFactory => ( + onActionFactorySelected(actionFactory)} + > + {actionFactory.iconType && } + + ))} + + ); +}; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts new file mode 100644 index 000000000000..641f25176264 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts @@ -0,0 +1,14 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const txtChangeButton = i18n.translate( + 'xpack.advancedUiActions.components.actionWizard.changeButton', + { + defaultMessage: 'change', + } +); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts new file mode 100644 index 000000000000..ed224248ec4c --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ActionFactory, ActionWizard } from './action_wizard'; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx new file mode 100644 index 000000000000..8ecdde681069 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx @@ -0,0 +1,171 @@ +/* + * 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, { useState } from 'react'; +import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; +import { ActionFactory, ActionBaseConfig, ActionWizard } from './action_wizard'; + +export const dashboards = [ + { id: 'dashboard1', title: 'Dashboard 1' }, + { id: 'dashboard2', title: 'Dashboard 2' }, +]; + +export const dashboardDrilldownActionFactory: ActionFactory<{ + dashboardId?: string; + useCurrentDashboardFilters: boolean; + useCurrentDashboardDataRange: boolean; +}> = { + type: 'Dashboard', + displayName: 'Go to Dashboard', + iconType: 'dashboardApp', + createConfig: () => { + return { + dashboardId: undefined, + useCurrentDashboardDataRange: true, + useCurrentDashboardFilters: true, + }; + }, + isValid: config => { + if (!config.dashboardId) return false; + return true; + }, + wizard: props => { + const config = props.config ?? { + dashboardId: undefined, + useCurrentDashboardDataRange: true, + useCurrentDashboardFilters: true, + }; + return ( + <> + + ({ value: id, text: title }))} + value={config.dashboardId} + onChange={e => { + props.onConfig({ ...config, dashboardId: e.target.value }); + }} + /> + + + + props.onConfig({ + ...config, + useCurrentDashboardFilters: !config.useCurrentDashboardFilters, + }) + } + /> + + + + props.onConfig({ + ...config, + useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, + }) + } + /> + + + ); + }, +}; + +export const urlDrilldownActionFactory: ActionFactory<{ url: string; openInNewTab: boolean }> = { + type: 'Url', + displayName: 'Go to URL', + iconType: 'link', + createConfig: () => { + return { + url: '', + openInNewTab: false, + }; + }, + isValid: config => { + if (!config.url) return false; + return true; + }, + wizard: props => { + const config = props.config ?? { + url: '', + openInNewTab: false, + }; + return ( + <> + + props.onConfig({ ...config, url: event.target.value })} + /> + + + props.onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + ); + }, +}; + +export function Demo({ actionFactories }: { actionFactories: Array> }) { + const [state, setState] = useState<{ + currentActionFactory?: ActionFactory; + config?: ActionBaseConfig; + }>({}); + + function changeActionFactory(newActionFactory: ActionFactory | null) { + if (!newActionFactory) { + // removing action factory + return setState({}); + } + + setState({ + currentActionFactory: newActionFactory, + config: newActionFactory.createConfig(), + }); + } + + return ( + <> + { + setState({ + ...state, + config: newConfig, + }); + }} + onActionFactoryChange={newActionFactory => { + changeActionFactory(newActionFactory); + }} + currentActionFactory={state.currentActionFactory} + /> +
+
+
Action Factory Type: {state.currentActionFactory?.type}
+
Action Factory Config: {JSON.stringify(state.config)}
+
+ Is config valid:{' '} + {JSON.stringify(state.currentActionFactory?.isValid(state.config!) ?? false)} +
+ + ); +} diff --git a/x-pack/plugins/advanced_ui_actions/scripts/storybook.js b/x-pack/plugins/advanced_ui_actions/scripts/storybook.js new file mode 100644 index 000000000000..3da0a3b37bfa --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/scripts/storybook.js @@ -0,0 +1,13 @@ +/* + * 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 { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'advanced_ui_actions', + storyGlobs: [join(__dirname, '..', 'public', 'components', '**', '*.story.tsx')], +}); From 0d3dd97691ea541d2c453afefe4aab1a922a7c9c Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Fri, 6 Mar 2020 12:54:28 -0600 Subject: [PATCH 04/20] Empty message for APM service map (#59518) When only one node is displayed, show an empty message. Also: * Start adding a basic Jest test for the ServiceMap component * Fix bug where EuiDocsLink was rendering "children" instead of the actual children Closes #59326. Closes #59128. --- .../components/app/ServiceMap/EmptyBanner.tsx | 43 ++++++++++++++++++ .../components/app/ServiceMap/index.test.tsx | 45 +++++++++++++++++++ .../components/app/ServiceMap/index.tsx | 6 ++- .../shared/Links/ElasticDocsLink.tsx | 2 +- 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx new file mode 100644 index 000000000000..418430e37b21 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; + +const EmptyBannerCallOut = styled(EuiCallOut)` + margin: ${lightTheme.gutterTypes.gutterSmall}; + /* Add some extra margin so it displays to the right of the controls. */ + margin-left: calc( + ${lightTheme.gutterTypes.gutterLarge} + + ${lightTheme.gutterTypes.gutterExtraLarge} + ); + position: absolute; + z-index: 1; +`; + +export function EmptyBanner() { + return ( + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { + defaultMessage: + "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." + })}{' '} + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { + defaultMessage: 'Learn more in the docs' + })} + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx new file mode 100644 index 000000000000..926f53954e7c --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.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 { render } from '@testing-library/react'; +import React, { FunctionComponent } from 'react'; +import { License } from '../../../../../../../plugins/licensing/common/license'; +import { LicenseContext } from '../../../context/LicenseContext'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; +import { ServiceMap } from './'; + +const expiredLicense = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'expired', + type: 'platinum', + uid: '1' + } +}); + +const Wrapper: FunctionComponent = ({ children }) => { + return ( + + {children} + + ); +}; + +describe('ServiceMap', () => { + describe('with an inactive license', () => { + it('renders the license banner', async () => { + expect( + ( + await render(, { + wrapper: Wrapper + }).findAllByText(/Platinum/) + ).length + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 9a93c67f0818..2942ce64729e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -26,13 +26,14 @@ import { useLicense } from '../../../hooks/useLicense'; import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; import { getCytoscapeElements } from './get_cytoscape_elements'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; interface ServiceMapProps { serviceName?: string; @@ -214,6 +215,9 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > + {serviceName && renderedElements.current.length === 1 && ( + + )}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 7645162ab265..0e0c318ad329 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -23,7 +23,7 @@ export function ElasticDocsLink({ section, path, children, ...rest }: Props) { children(href) ) : ( - children + {children} ); } From 76c205cd1ef3b9d343a7c5416983b984d2d7bea7 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 6 Mar 2020 11:54:49 -0700 Subject: [PATCH 05/20] ensure fs deletes are not cwd dependent (#59570) Co-authored-by: spalger --- src/cli_plugin/install/cleanup.js | 2 +- src/cli_plugin/install/install.js | 2 +- src/cli_plugin/remove/remove.js | 2 +- .../reporting/server/browsers/chromium/driver_factory/index.ts | 2 +- .../legacy/plugins/reporting/server/browsers/download/clean.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli_plugin/install/cleanup.js b/src/cli_plugin/install/cleanup.js index fa4bdcf4f696..eaa25962ef0e 100644 --- a/src/cli_plugin/install/cleanup.js +++ b/src/cli_plugin/install/cleanup.js @@ -27,7 +27,7 @@ export function cleanPrevious(settings, logger) { logger.log('Found previous install attempt. Deleting...'); try { - del.sync(settings.workingPath); + del.sync(settings.workingPath, { force: true }); } catch (e) { reject(e); } diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 5a341e67dc12..92be2ac25032 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -46,7 +46,7 @@ export default async function install(settings, logger) { await extract(settings, logger); - del.sync(settings.tempArchiveFile); + del.sync(settings.tempArchiveFile, { force: true }); existingInstall(settings, logger); diff --git a/src/cli_plugin/remove/remove.js b/src/cli_plugin/remove/remove.js index 8432d0f44836..353e592390ff 100644 --- a/src/cli_plugin/remove/remove.js +++ b/src/cli_plugin/remove/remove.js @@ -37,7 +37,7 @@ export default function remove(settings, logger) { } logger.log(`Removing ${settings.plugin}...`); - del.sync(settings.pluginPath); + del.sync(settings.pluginPath, { force: true }); logger.log('Plugin removal complete'); } catch (err) { logger.error(`Unable to remove plugin because of error: "${err.message}"`); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 11b70c82f6fa..f90f2c7aee39 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -168,7 +168,7 @@ export class HeadlessChromiumDriverFactory { logger.debug(`deleting chromium user data directory at [${userDataDir}]`); // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. - del(userDataDir).catch(error => { + del(userDataDir, { force: true }).catch(error => { logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts index 4355a6a0a177..a2d1fc7f91a2 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts @@ -31,7 +31,7 @@ export async function clean(dir: string, expectedPaths: string[]) { const path = resolvePath(dir, filename); if (!expectedPaths.includes(path)) { log(`Deleting unexpected file ${path}`); - await del(path); + await del(path, { force: true }); } }); } From dd9999bf5d3c76167e7cb981984b1c93dee6cc5d Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 6 Mar 2020 16:03:41 -0500 Subject: [PATCH 06/20] Check for alert dialog when doing a force logout (#59329) --- .../functional/page_objects/security_page.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 4803596b973b..4b097b916573 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -110,12 +110,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } await userMenu.clickLogoutButton(); - - await retry.waitForWithTimeout( - 'login form', - config.get('timeouts.waitFor') * 5, - async () => await find.existsByDisplayedByCssSelector('.login-form') - ); + await this.waitForLoginForm(); } async forceLogout() { @@ -129,11 +124,17 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const url = PageObjects.common.getHostPort() + '/logout'; await browser.get(url); log.debug('Waiting on the login form to appear'); - await retry.waitForWithTimeout( - 'login form', - config.get('timeouts.waitFor') * 5, - async () => await find.existsByDisplayedByCssSelector('.login-form') - ); + await this.waitForLoginForm(); + } + + async waitForLoginForm() { + await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => { + const alert = await browser.getAlert(); + if (alert && alert.accept) { + await alert.accept(); + } + return await find.existsByDisplayedByCssSelector('.login-form'); + }); } async clickRolesSection() { From ac9c19223408625dd6945f0479dd7966ed6d4443 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 6 Mar 2020 16:04:11 -0500 Subject: [PATCH 07/20] Navigate back to discover app during test, because the saved search from the preceding test has major performance problems when used with this test (#59571) --- test/functional/apps/discover/_discover.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 432e83891aa9..2011b9bc274f 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -23,7 +23,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); - const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); @@ -188,7 +187,7 @@ export default function({ getService, getPageObjects }) { describe('time zone switch', () => { it('should show bars in the correct time zone after switching', async function() { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); - await browser.refresh(); + await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); await PageObjects.timePicker.setDefaultAbsoluteRange(); From 5db1b0a18d83a9e4dee9f1a4b260701be978c4d7 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Fri, 6 Mar 2020 21:32:42 +0000 Subject: [PATCH 08/20] Show error if field is not found during filter rendering (#59298) * Show error if field is not found * Errored filter state * Design adjustments * Fixing class names and making look similar to disabled * code review fixes Co-authored-by: Elastic Machine Co-authored-by: cchaos --- .../es_query/filters/get_display_value.ts | 12 ++++++- .../ui/filter_bar/_global_filter_item.scss | 9 +++++ .../filter_editor/lib/filter_label.tsx | 18 ++++++---- .../data/public/ui/filter_bar/filter_item.tsx | 34 ++++++++++++++----- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 4bf7e1c9c6ba..03167f308041 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -18,6 +18,7 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { IIndexPattern, IFieldType } from '../..'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; import { Filter } from '../filters'; @@ -27,7 +28,16 @@ function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { let format = get(indexPattern, ['fields', 'byName', key, 'format']); if (!format && (indexPattern.fields as any).getByName) { // TODO: Why is indexPatterns sometimes a map and sometimes an array? - format = ((indexPattern.fields as any).getByName(key) as IFieldType).format; + const field: IFieldType = (indexPattern.fields as any).getByName(key); + if (!field) { + throw new Error( + i18n.translate('data.filter.filterBar.fieldNotFound', { + defaultMessage: 'Field {key} not found in index pattern {indexPattern}', + values: { key, indexPattern: indexPattern.title }, + }) + ); + } + format = field.format; } return format; } diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 51204e2a6116..24adf0093af9 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -32,6 +32,15 @@ font-style: italic; } +.globalFilterItem-isInvalid { + text-decoration: none; + + .globalFilterLabel__value { + color: $euiColorDanger; + font-weight: $euiFontWeightBold; + } +} + .globalFilterItem-isPinned { position: relative; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index ee6d178b25c2..070631354d8b 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -41,6 +41,10 @@ export function FilterLabel({ filter, valueLabel }: Props) { prefixText ); + const getValue = (text?: string) => { + return {text}; + }; + if (filter.meta.alias !== null) { return ( @@ -55,35 +59,35 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key} {existsOperator.message} + {filter.meta.key}: {getValue(`${existsOperator.message}`)} ); case FILTERS.GEO_BOUNDING_BOX: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.GEO_POLYGON: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.PHRASES: return ( {prefix} - {filter.meta.key} {isOneOfOperator.message} {valueLabel} + {filter.meta.key}: {getValue(`${isOneOfOperator.message} ${valueLabel}`)} ); case FILTERS.QUERY_STRING: return ( {prefix} - {valueLabel} + {getValue(`${valueLabel}`)} ); case FILTERS.PHRASE: @@ -91,14 +95,14 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); default: return ( {prefix} - {JSON.stringify(filter.query) || filter.meta.value} + {getValue(`${JSON.stringify(filter.query) || filter.meta.value}`)} ); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 0febfe807a94..6b5fd41dc06e 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -33,6 +33,7 @@ import { toggleFilterPinned, toggleFilterDisabled, } from '../../../common'; +import { getNotifications } from '../../services'; interface Props { id: string; @@ -64,24 +65,41 @@ class FilterItemUI extends Component { public render() { const { filter, id } = this.props; const { negate, disabled } = filter.meta; + let hasError: boolean = false; + + let valueLabel; + try { + valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); + } catch (e) { + getNotifications().toasts.addError(e, { + title: this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorMessage', + defaultMessage: 'Failed to display filter', + }), + }); + valueLabel = this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorText', + defaultMessage: 'Error', + }); + hasError = true; + } + const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; + const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; + const dataTestSubjDisabled = `filter-${ + this.props.filter.meta.disabled ? 'disabled' : 'enabled' + }`; const classes = classNames( 'globalFilterItem', { - 'globalFilterItem-isDisabled': disabled, + 'globalFilterItem-isDisabled': disabled || hasError, + 'globalFilterItem-isInvalid': hasError, 'globalFilterItem-isPinned': isFilterPinned(filter), 'globalFilterItem-isExcluded': negate, }, this.props.className ); - const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); - const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; - const dataTestSubjDisabled = `filter-${ - this.props.filter.meta.disabled ? 'disabled' : 'enabled' - }`; - const badge = ( Date: Fri, 6 Mar 2020 22:41:05 +0100 Subject: [PATCH 09/20] [SIEM] Adds 'Load prebuilt rules' Cypress test (#59529) * adds 'load prebuilt rules' * fixes typecheck issue * updates jest snapshot --- .../signal_detection_rules.spec.ts | 50 +++++++++++++++++++ .../siem/cypress/screens/detections.ts | 9 ++++ .../cypress/screens/signal_detection_rules.ts | 22 ++++++++ .../plugins/siem/cypress/tasks/detections.ts | 28 +++++++++++ .../cypress/tasks/signal_detection_rules.ts | 40 +++++++++++++++ .../plugins/siem/cypress/urls/navigation.ts | 1 + .../loader/__snapshots__/index.test.tsx.snap | 1 + .../siem/public/components/loader/index.tsx | 2 +- .../components/signals/index.tsx | 2 +- .../detection_engine/detection_engine.tsx | 7 ++- .../detection_engine/rules/all/index.tsx | 1 + .../pre_packaged_rules/load_empty_prompt.tsx | 1 + x-pack/test/siem_cypress/config.ts | 5 ++ 13 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/detections.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/detections.ts create mode 100644 x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts new file mode 100644 index 000000000000..f2ed9d48daaf --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -0,0 +1,50 @@ +/* + * 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 { ELASTIC_RULES_BTN, RULES_TABLE, RULES_ROW } from '../screens/signal_detection_rules'; + +import { + changeToThreeHundredRowsPerPage, + loadPrebuiltDetectionRules, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForPrebuiltDetectionRulesToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/signal_detection_rules'; +import { + goToManageSignalDetectionRules, + waitForSignalsIndexToBeCreated, + waitForSignalsPanelToBeLoaded, +} from '../tasks/detections'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Signal detection rules', () => { + before(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS); + }); + it('Loads prebuilt rules', () => { + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); + goToManageSignalDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + loadPrebuiltDetectionRules(); + waitForPrebuiltDetectionRulesToBeLoaded(); + + const expectedElasticRulesBtnText = 'Elastic rules (92)'; + cy.get(ELASTIC_RULES_BTN) + .invoke('text') + .should('eql', expectedElasticRulesBtnText); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 92; + cy.get(RULES_TABLE).then($table => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts new file mode 100644 index 000000000000..8089b028a10d --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -0,0 +1,9 @@ +/* + * 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 const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; + +export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts new file mode 100644 index 000000000000..bfaa86e83f30 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts @@ -0,0 +1,22 @@ +/* + * 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 const ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]'; + +export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; + +export const LOADING_INITIAL_PREBUILT_RULES_TABLE = + '[data-test-subj="initialLoadingPanelAllRulesTable"]'; + +export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]'; + +export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; + +export const RULES_TABLE = '[data-test-subj="rules-table"]'; + +export const RULES_ROW = '.euiTableRow'; + +export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts new file mode 100644 index 000000000000..4a0a565a74e2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -0,0 +1,28 @@ +/* + * 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 { LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN } from '../screens/detections'; + +export const goToManageSignalDetectionRules = () => { + cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForSignalsIndexToBeCreated = () => { + cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( + response => { + if (response.status !== 200) { + cy.wait(7500); + } + } + ); +}; + +export const waitForSignalsPanelToBeLoaded = () => { + cy.get(LOADING_SIGNALS_PANEL).should('exist'); + cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts new file mode 100644 index 000000000000..cc0e4bce1035 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts @@ -0,0 +1,40 @@ +/* + * 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 { + LOAD_PREBUILT_RULES_BTN, + LOADING_INITIAL_PREBUILT_RULES_TABLE, + LOADING_SPINNER, + PAGINATION_POPOVER_BTN, + RULES_TABLE, + THREE_HUNDRED_ROWS, +} from '../screens/signal_detection_rules'; + +export const changeToThreeHundredRowsPerPage = () => { + cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); + cy.get(THREE_HUNDRED_ROWS).click(); +}; + +export const loadPrebuiltDetectionRules = () => { + cy.get(LOAD_PREBUILT_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded = () => { + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('exist'); + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('not.exist'); +}; + +export const waitForPrebuiltDetectionRulesToBeLoaded = () => { + cy.get(LOAD_PREBUILT_RULES_BTN).should('not.exist'); + cy.get(RULES_TABLE).should('exist'); +}; + +export const waitForRulesToBeLoaded = () => { + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 8fdc939e7ee5..5e65e5aa34c1 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 0885f15b1efb..ad2d57b948ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -16,6 +16,7 @@ exports[`rendering renders correctly 1`] = ` grow={false} > diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index be2ce3dde951..e78f14841858 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -62,7 +62,7 @@ export const Loader = React.memo(({ children, overlay, overlayBackg