diff --git a/test/plugin_functional/plugins/kbn_top_nav/kibana.json b/test/plugin_functional/plugins/kbn_top_nav/kibana.json
new file mode 100644
index 0000000000000..b274e80b9ef65
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_top_nav/kibana.json
@@ -0,0 +1,9 @@
+{
+ "id": "kbn_top_nav",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["kbn_top_nav"],
+ "server": false,
+ "ui": true,
+ "requiredPlugins": ["navigation"]
+}
\ No newline at end of file
diff --git a/test/plugin_functional/plugins/kbn_top_nav/package.json b/test/plugin_functional/plugins/kbn_top_nav/package.json
new file mode 100644
index 0000000000000..510d681a4a75c
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_top_nav/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "kbn_top_nav",
+ "version": "1.0.0",
+ "main": "target/test/plugin_functional/plugins/kbn_top_nav",
+ "kibana": {
+ "version": "kibana",
+ "templateVersion": "1.0.0"
+ },
+ "license": "Apache-2.0",
+ "scripts": {
+ "kbn": "node ../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && tsc"
+ },
+ "devDependencies": {
+ "typescript": "3.7.2"
+ }
+}
+
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
similarity index 71%
rename from test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx
rename to test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
index f77db4fe1654e..0f65e6159796b 100644
--- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx
+++ b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
@@ -18,11 +18,15 @@
*/
import React from 'react';
-import './initialize';
-import { npStart } from 'ui/new_platform';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { AppMountParameters } from 'kibana/public';
+import { AppPluginDependencies } from './types';
-export const AppWithTopNav = () => {
- const { TopNavMenu } = npStart.plugins.navigation.ui;
+export const renderApp = (
+ depsStart: AppPluginDependencies,
+ { appBasePath, element }: AppMountParameters
+) => {
+ const { TopNavMenu } = depsStart.navigation.ui;
const config = [
{
id: 'new',
@@ -32,10 +36,12 @@ export const AppWithTopNav = () => {
testId: 'demoNewButton',
},
];
-
- return (
+ render(
Hey
-
+ ,
+ element
);
+
+ return () => unmountComponentAtNode(element);
};
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts
similarity index 71%
rename from test/plugin_functional/plugins/kbn_tp_top_nav/index.js
rename to test/plugin_functional/plugins/kbn_top_nav/public/index.ts
index b4c3e05c28b66..bd478f1dd3bdb 100644
--- a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js
+++ b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts
@@ -17,15 +17,8 @@
* under the License.
*/
-export default function(kibana) {
- return new kibana.Plugin({
- uiExports: {
- app: {
- title: 'Top Nav Menu test',
- description: 'This is a sample plugin for the functional tests.',
- main: 'plugins/kbn_tp_top_nav/app',
- },
- hacks: ['plugins/kbn_tp_top_nav/initialize'],
- },
- });
-}
+import { PluginInitializer } from 'kibana/public';
+import { TopNavTestPlugin, TopNavTestPluginSetup, TopNavTestPluginStart } from './plugin';
+
+export const plugin: PluginInitializer = () =>
+ new TopNavTestPlugin();
diff --git a/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx
new file mode 100644
index 0000000000000..a433de98357fb
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { CoreSetup, Plugin, AppMountParameters } from 'kibana/public';
+import { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public';
+import { AppPluginDependencies } from './types';
+
+export class TopNavTestPlugin implements Plugin {
+ public setup(core: CoreSetup, { navigation }: { navigation: NavigationPublicPluginSetup }) {
+ const customExtension = {
+ id: 'registered-prop',
+ label: 'Registered Button',
+ description: 'Registered Demo',
+ run() {},
+ testId: 'demoRegisteredNewButton',
+ };
+
+ navigation.registerMenuItem(customExtension);
+
+ const customDiscoverExtension = {
+ id: 'registered-discover-prop',
+ label: 'Registered Discover Button',
+ description: 'Registered Discover Demo',
+ run() {},
+ testId: 'demoDiscoverRegisteredNewButton',
+ appName: 'discover',
+ };
+
+ navigation.registerMenuItem(customDiscoverExtension);
+
+ core.application.register({
+ id: 'topNavMenu',
+ title: 'Top nav menu example',
+ async mount(params: AppMountParameters) {
+ const { renderApp } = await import('./application');
+ const services = await core.getStartServices();
+ return renderApp(services[1] as AppPluginDependencies, params);
+ },
+ });
+
+ return {};
+ }
+
+ public start() {}
+ public stop() {}
+}
+
+export type TopNavTestPluginSetup = ReturnType;
+export type TopNavTestPluginStart = ReturnType;
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts
similarity index 81%
rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js
rename to test/plugin_functional/plugins/kbn_top_nav/public/types.ts
index b2497a824ba2b..c70a78bedb54f 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js
+++ b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts
@@ -17,10 +17,8 @@
* under the License.
*/
-export default function(kibana) {
- return new kibana.Plugin({
- uiExports: {
- hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'],
- },
- });
+import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public';
+
+export interface AppPluginDependencies {
+ navigation: NavigationPublicPluginStart;
}
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json
similarity index 100%
rename from test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json
rename to test/plugin_functional/plugins/kbn_top_nav/tsconfig.json
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json
new file mode 100644
index 0000000000000..622cbd80090ba
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "kbn_tp_custom_visualizations",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "requiredPlugins": [
+ "visualizations"
+ ],
+ "server": false,
+ "ui": true
+}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
index 344aae30b5bbc..9ee7845816faa 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -1,6 +1,7 @@
{
"name": "kbn_tp_custom_visualizations",
"version": "1.0.0",
+ "main": "target/test/plugin_functional/plugins/kbn_tp_custom_visualizations",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
@@ -9,5 +10,13 @@
"dependencies": {
"@elastic/eui": "21.0.1",
"react": "^16.12.0"
+ },
+ "scripts": {
+ "kbn": "node ../../../../scripts/kbn.js",
+ "build": "rm -rf './target' && tsc"
+ },
+ "devDependencies": {
+ "@kbn/plugin-helpers": "9.0.2",
+ "typescript": "3.7.2"
}
}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts
new file mode 100644
index 0000000000000..cb821a2698479
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { PluginInitializer } from 'kibana/public';
+import {
+ CustomVisualizationsPublicPlugin,
+ CustomVisualizationsSetup,
+ CustomVisualizationsStart,
+} from './plugin';
+
+export { CustomVisualizationsPublicPlugin as Plugin };
+
+export const plugin: PluginInitializer = () =>
+ new CustomVisualizationsPublicPlugin();
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts
new file mode 100644
index 0000000000000..1be4aa9ee42ae
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { CoreSetup, Plugin } from 'kibana/public';
+import { VisualizationsSetup } from '../../../../../src/plugins/visualizations/public';
+import { SelfChangingEditor } from './self_changing_vis/self_changing_editor';
+import { SelfChangingComponent } from './self_changing_vis/self_changing_components';
+
+export interface SetupDependencies {
+ visualizations: VisualizationsSetup;
+}
+
+export class CustomVisualizationsPublicPlugin
+ implements Plugin {
+ public setup(core: CoreSetup, setupDeps: SetupDependencies) {
+ setupDeps.visualizations.createReactVisualization({
+ name: 'self_changing_vis',
+ title: 'Self Changing Vis',
+ icon: 'controlsHorizontal',
+ description:
+ 'This visualization is able to change its own settings, that you could also set in the editor.',
+ visConfig: {
+ component: SelfChangingComponent,
+ defaults: {
+ counter: 0,
+ },
+ },
+ editorConfig: {
+ optionTabs: [
+ {
+ name: 'options',
+ title: 'Options',
+ editor: SelfChangingEditor,
+ },
+ ],
+ },
+ requestHandler: 'none',
+ });
+ }
+
+ public start() {}
+ public stop() {}
+}
+
+export type CustomVisualizationsSetup = ReturnType;
+export type CustomVisualizationsStart = ReturnType;
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js
deleted file mode 100644
index c5b074db43a1b..0000000000000
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React from 'react';
-
-import { EuiBadge } from '@elastic/eui';
-
-export class SelfChangingComponent extends React.Component {
- onClick = () => {
- this.props.vis.params.counter++;
- this.props.vis.updateState();
- };
-
- render() {
- return (
-
-
- {this.props.vis.params.counter}
-
-
- );
- }
-
- componentDidMount() {
- this.props.renderComplete();
- }
-
- componentDidUpdate() {
- this.props.renderComplete();
- }
-}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx
new file mode 100644
index 0000000000000..2f01908122457
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect } from 'react';
+
+import { EuiBadge } from '@elastic/eui';
+
+interface SelfChangingComponentProps {
+ renderComplete: () => {};
+ visParams: {
+ counter: number;
+ };
+}
+
+export function SelfChangingComponent(props: SelfChangingComponentProps) {
+ useEffect(() => {
+ props.renderComplete();
+ });
+
+ return (
+
+ {}}
+ data-test-subj="counter"
+ onClickAriaLabel="Increase counter"
+ color="primary"
+ >
+ {props.visParams.counter}
+
+
+ );
+}
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx
similarity index 76%
rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js
rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx
index fa3a0c8b9f6fe..d3f66d708603c 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx
@@ -20,10 +20,15 @@
import React from 'react';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
+import { VisOptionsProps } from '../../../../../../src/legacy/core_plugins/vis_default_editor/public/vis_options_props';
-export class SelfChangingEditor extends React.Component {
- onCounterChange = ev => {
- this.props.setValue('counter', parseInt(ev.target.value));
+interface CounterParams {
+ counter: number;
+}
+
+export class SelfChangingEditor extends React.Component> {
+ onCounterChange = (ev: any) => {
+ this.props.setValue('counter', parseInt(ev.target.value, 10));
};
render() {
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json
new file mode 100644
index 0000000000000..d8096d9aab27a
--- /dev/null
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true,
+ "types": [
+ "node",
+ "jest",
+ "react"
+ ]
+ },
+ "include": [
+ "index.ts",
+ "public/**/*.ts",
+ "public/**/*.tsx",
+ "../../../../typings/**/*",
+ ],
+ "exclude": []
+}
\ No newline at end of file
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json b/test/plugin_functional/plugins/kbn_tp_top_nav/package.json
deleted file mode 100644
index 7102d24d3292d..0000000000000
--- a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "kbn_tp_top_nav",
- "version": "1.0.0",
- "kibana": {
- "version": "kibana",
- "templateVersion": "1.0.0"
- },
- "license": "Apache-2.0"
-}
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js
deleted file mode 100644
index e7f97e68c086d..0000000000000
--- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import React from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-
-import { uiModules } from 'ui/modules';
-import chrome from 'ui/chrome';
-
-// This is required so some default styles and required scripts/Angular modules are loaded,
-// or the timezone setting is correctly applied.
-import 'ui/autoload/all';
-
-import { AppWithTopNav } from './top_nav';
-
-const app = uiModules.get('apps/topnavDemoPlugin', ['kibana']);
-
-app.config($locationProvider => {
- $locationProvider.html5Mode({
- enabled: false,
- requireBase: false,
- rewriteLinks: false,
- });
-});
-
-function RootController($scope, $element) {
- const domNode = $element[0];
-
- // render react to DOM
- render( , domNode);
-
- // unmount react on controller destroy
- $scope.$on('$destroy', () => {
- unmountComponentAtNode(domNode);
- });
-}
-
-chrome.setRootController('topnavDemoPlugin', RootController);
diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js
deleted file mode 100644
index d46e47f6d248a..0000000000000
--- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { npSetup } from 'ui/new_platform';
-
-const customExtension = {
- id: 'registered-prop',
- label: 'Registered Button',
- description: 'Registered Demo',
- run() {},
- testId: 'demoRegisteredNewButton',
-};
-
-npSetup.plugins.navigation.registerMenuItem(customExtension);
-
-const customDiscoverExtension = {
- id: 'registered-discover-prop',
- label: 'Registered Discover Button',
- description: 'Registered Discover Demo',
- run() {},
- testId: 'demoDiscoverRegisteredNewButton',
- appName: 'discover',
-};
-
-npSetup.plugins.navigation.registerMenuItem(customDiscoverExtension);
diff --git a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js
index ef6f0a626bd15..83258a1ca3bdc 100644
--- a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js
+++ b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js
@@ -28,11 +28,7 @@ export default function({ getService, getPageObjects }) {
return await testSubjects.getVisibleText('counter');
}
- async function getEditorValue() {
- return await testSubjects.getAttribute('counterEditor', 'value');
- }
-
- describe.skip('self changing vis', function describeIndexTests() {
+ describe('self changing vis', function describeIndexTests() {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('self_changing_vis');
@@ -45,16 +41,17 @@ export default function({ getService, getPageObjects }) {
const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled();
expect(isApplyEnabled).to.be(true);
await PageObjects.visEditor.clickGo();
+ await renderable.waitForRender();
const counter = await getCounterValue();
expect(counter).to.be('10');
});
- it('should allow changing params from within the vis', async () => {
+ it.skip('should allow changing params from within the vis', async () => {
await testSubjects.click('counter');
await renderable.waitForRender();
const visValue = await getCounterValue();
expect(visValue).to.be('11');
- const editorValue = await getEditorValue();
+ const editorValue = await testSubjects.getAttribute('counterEditor', 'value');
expect(editorValue).to.be('11');
// If changing a param from within the vis it should immediately apply and not bring editor in an unchanged state
const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled();
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx
new file mode 100644
index 0000000000000..938962cc9dd18
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { TraceAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/traces/get_trace';
+import { WaterfallContainer } from './index';
+import {
+ location,
+ urlParams,
+ simpleTrace,
+ traceWithErrors,
+ traceChildStartBeforeParent
+} from './waterfallContainer.stories.data';
+import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
+
+storiesOf('app/TransactionDetails/Waterfall', module).add(
+ 'simple',
+ () => {
+ const waterfall = getWaterfall(
+ simpleTrace as TraceAPIResponse,
+ '975c8d5bfd1dd20b'
+ );
+ return (
+
+ );
+ },
+ { info: { source: false } }
+);
+
+storiesOf('app/TransactionDetails/Waterfall', module).add(
+ 'with errors',
+ () => {
+ const waterfall = getWaterfall(
+ (traceWithErrors as unknown) as TraceAPIResponse,
+ '975c8d5bfd1dd20b'
+ );
+ return (
+
+ );
+ },
+ { info: { source: false } }
+);
+
+storiesOf('app/TransactionDetails/Waterfall', module).add(
+ 'child starts before parent',
+ () => {
+ const waterfall = getWaterfall(
+ traceChildStartBeforeParent as TraceAPIResponse,
+ '975c8d5bfd1dd20b'
+ );
+ return (
+
+ );
+ },
+ { info: { source: false } }
+);
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
new file mode 100644
index 0000000000000..835183e73b298
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
@@ -0,0 +1,1647 @@
+/*
+ * 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 { Location } from 'history';
+import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
+
+export const location = {
+ pathname: '/services/opbeans-go/transactions/view',
+ search:
+ '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=service.name%253A%2520%2522opbeans-java%2522%2520or%2520service.name%2520%253A%2520%2522opbeans-go%2522&traceId=513d33fafe99bbe6134749310c9b5322&transactionId=975c8d5bfd1dd20b&transactionName=GET%20%2Fapi%2Forders&transactionType=request',
+ hash: ''
+} as Location;
+
+export const urlParams = {
+ start: '2020-03-22T15:16:38.742Z',
+ end: '2020-03-23T15:16:38.742Z',
+ rangeFrom: 'now-24h',
+ rangeTo: 'now',
+ refreshPaused: true,
+ refreshInterval: 0,
+ page: 0,
+ transactionId: '975c8d5bfd1dd20b',
+ traceId: '513d33fafe99bbe6134749310c9b5322',
+ kuery: 'service.name: "opbeans-java" or service.name : "opbeans-go"',
+ transactionName: 'GET /api/orders',
+ transactionType: 'request',
+ processorEvent: 'transaction',
+ serviceName: 'opbeans-go'
+} as IUrlParams;
+
+export const simpleTrace = {
+ trace: {
+ items: [
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 46
+ }
+ },
+ source: {
+ ip: '172.19.0.13'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: '172.19.0.9',
+ full: 'http://172.19.0.9:3000/api/orders'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ type: 'apm-server',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ http: {
+ request: {
+ headers: {
+ Accept: ['*/*'],
+ 'User-Agent': ['Python/3.7 aiohttp/3.3.2'],
+ Host: ['172.19.0.9:3000'],
+ 'Accept-Encoding': ['gzip, deflate']
+ },
+ method: 'get',
+ socket: {
+ encrypted: false,
+ remote_address: '172.19.0.13'
+ },
+ body: {
+ original: '[REDACTED]'
+ }
+ },
+ response: {
+ headers: {
+ 'Transfer-Encoding': ['chunked'],
+ Date: ['Mon, 23 Mar 2020 15:04:28 GMT'],
+ 'Content-Type': ['application/json;charset=ISO-8859-1']
+ },
+ status_code: 200,
+ finished: true,
+ headers_sent: true
+ },
+ version: '1.1'
+ },
+ client: {
+ ip: '172.19.0.13'
+ },
+ transaction: {
+ duration: {
+ us: 18842
+ },
+ result: 'HTTP 2xx',
+ name: 'DispatcherServlet#doGet',
+ id: '49809ad3c26adf74',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ user_agent: {
+ original: 'Python/3.7 aiohttp/3.3.2',
+ name: 'Other',
+ device: {
+ name: 'Other'
+ }
+ },
+ timestamp: {
+ us: 1584975868785000
+ }
+ },
+ {
+ parent: {
+ id: 'fc107f7b556eb49b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ framework: {
+ name: 'gin',
+ version: 'v1.4.0'
+ },
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ duration: {
+ us: 16597
+ },
+ result: 'HTTP 2xx',
+ name: 'GET /api/orders',
+ id: '975c8d5bfd1dd20b',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ timestamp: {
+ us: 1584975868787052
+ }
+ },
+ {
+ parent: {
+ id: 'daae24d83c269918'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ timestamp: {
+ us: 1584975868788603
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ '@timestamp': '2020-03-23T15:04:28.788Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ result: 'HTTP 2xx',
+ duration: {
+ us: 14648
+ },
+ name: 'GET opbeans.views.orders',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ id: '6fb0ff7365b87298',
+ type: 'request',
+ sampled: true
+ }
+ },
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ parent: {
+ id: '49809ad3c26adf74'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 44
+ }
+ },
+ destination: {
+ address: 'opbeans-go',
+ port: 3000
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ type: 'apm-server',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ connection: {
+ hash:
+ "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}"
+ },
+ transaction: {
+ id: '49809ad3c26adf74'
+ },
+ timestamp: {
+ us: 1584975868785273
+ },
+ span: {
+ duration: {
+ us: 17530
+ },
+ subtype: 'http',
+ name: 'GET opbeans-go',
+ destination: {
+ service: {
+ resource: 'opbeans-go:3000',
+ name: 'http://opbeans-go:3000',
+ type: 'external'
+ }
+ },
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-go:3000/api/orders'
+ }
+ },
+ id: 'fc107f7b556eb49b',
+ type: 'external'
+ }
+ },
+ {
+ parent: {
+ id: '975c8d5bfd1dd20b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '975c8d5bfd1dd20b'
+ },
+ timestamp: {
+ us: 1584975868787174
+ },
+ span: {
+ duration: {
+ us: 16250
+ },
+ subtype: 'http',
+ destination: {
+ service: {
+ resource: 'opbeans-python:3000',
+ name: 'http://opbeans-python:3000',
+ type: 'external'
+ }
+ },
+ name: 'GET opbeans-python:3000',
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-python:3000/api/orders'
+ }
+ },
+ id: 'daae24d83c269918',
+ type: 'external'
+ }
+ },
+ {
+ container: {
+ id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ parent: {
+ id: '6fb0ff7365b87298'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.790Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '6fb0ff7365b87298'
+ },
+ timestamp: {
+ us: 1584975868790080
+ },
+ span: {
+ duration: {
+ us: 2519
+ },
+ subtype: 'postgresql',
+ name: 'SELECT FROM opbeans_order',
+ destination: {
+ service: {
+ resource: 'postgresql',
+ name: 'postgresql',
+ type: 'db'
+ }
+ },
+ action: 'query',
+ id: 'c9407abb4d08ead1',
+ type: 'db',
+ sync: true,
+ db: {
+ statement:
+ 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000',
+ type: 'sql'
+ }
+ }
+ }
+ ],
+ exceedsMax: false,
+ errorDocs: []
+ },
+ errorsPerTransaction: {}
+};
+
+export const traceWithErrors = {
+ trace: {
+ items: [
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 46
+ }
+ },
+ source: {
+ ip: '172.19.0.13'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: '172.19.0.9',
+ full: 'http://172.19.0.9:3000/api/orders'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ type: 'apm-server',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ http: {
+ request: {
+ headers: {
+ Accept: ['*/*'],
+ 'User-Agent': ['Python/3.7 aiohttp/3.3.2'],
+ Host: ['172.19.0.9:3000'],
+ 'Accept-Encoding': ['gzip, deflate']
+ },
+ method: 'get',
+ socket: {
+ encrypted: false,
+ remote_address: '172.19.0.13'
+ },
+ body: {
+ original: '[REDACTED]'
+ }
+ },
+ response: {
+ headers: {
+ 'Transfer-Encoding': ['chunked'],
+ Date: ['Mon, 23 Mar 2020 15:04:28 GMT'],
+ 'Content-Type': ['application/json;charset=ISO-8859-1']
+ },
+ status_code: 200,
+ finished: true,
+ headers_sent: true
+ },
+ version: '1.1'
+ },
+ client: {
+ ip: '172.19.0.13'
+ },
+ transaction: {
+ duration: {
+ us: 18842
+ },
+ result: 'HTTP 2xx',
+ name: 'DispatcherServlet#doGet',
+ id: '49809ad3c26adf74',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ user_agent: {
+ original: 'Python/3.7 aiohttp/3.3.2',
+ name: 'Other',
+ device: {
+ name: 'Other'
+ }
+ },
+ timestamp: {
+ us: 1584975868785000
+ }
+ },
+ {
+ parent: {
+ id: 'fc107f7b556eb49b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ framework: {
+ name: 'gin',
+ version: 'v1.4.0'
+ },
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ duration: {
+ us: 16597
+ },
+ result: 'HTTP 2xx',
+ name: 'GET /api/orders',
+ id: '975c8d5bfd1dd20b',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ timestamp: {
+ us: 1584975868787052
+ }
+ },
+ {
+ parent: {
+ id: 'daae24d83c269918'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ timestamp: {
+ us: 1584975868788603
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ '@timestamp': '2020-03-23T15:04:28.788Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ result: 'HTTP 2xx',
+ duration: {
+ us: 14648
+ },
+ name: 'GET opbeans.views.orders',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ id: '6fb0ff7365b87298',
+ type: 'request',
+ sampled: true
+ }
+ },
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ parent: {
+ id: '49809ad3c26adf74'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 44
+ }
+ },
+ destination: {
+ address: 'opbeans-go',
+ port: 3000
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ type: 'apm-server',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ connection: {
+ hash:
+ "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}"
+ },
+ transaction: {
+ id: '49809ad3c26adf74'
+ },
+ timestamp: {
+ us: 1584975868785273
+ },
+ span: {
+ duration: {
+ us: 17530
+ },
+ subtype: 'http',
+ name: 'GET opbeans-go',
+ destination: {
+ service: {
+ resource: 'opbeans-go:3000',
+ name: 'http://opbeans-go:3000',
+ type: 'external'
+ }
+ },
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-go:3000/api/orders'
+ }
+ },
+ id: 'fc107f7b556eb49b',
+ type: 'external'
+ }
+ },
+ {
+ parent: {
+ id: '975c8d5bfd1dd20b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '975c8d5bfd1dd20b'
+ },
+ timestamp: {
+ us: 1584975868787174
+ },
+ span: {
+ duration: {
+ us: 16250
+ },
+ subtype: 'http',
+ destination: {
+ service: {
+ resource: 'opbeans-python:3000',
+ name: 'http://opbeans-python:3000',
+ type: 'external'
+ }
+ },
+ name: 'GET opbeans-python:3000',
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-python:3000/api/orders'
+ }
+ },
+ id: 'daae24d83c269918',
+ type: 'external'
+ }
+ },
+ {
+ container: {
+ id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ parent: {
+ id: '6fb0ff7365b87298'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.790Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '6fb0ff7365b87298'
+ },
+ timestamp: {
+ us: 1584975868790080
+ },
+ span: {
+ duration: {
+ us: 2519
+ },
+ subtype: 'postgresql',
+ name: 'SELECT FROM opbeans_order',
+ destination: {
+ service: {
+ resource: 'postgresql',
+ name: 'postgresql',
+ type: 'db'
+ }
+ },
+ action: 'query',
+ id: 'c9407abb4d08ead1',
+ type: 'db',
+ sync: true,
+ db: {
+ statement:
+ 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000',
+ type: 'sql'
+ }
+ }
+ }
+ ],
+ exceedsMax: false,
+ errorDocs: [
+ {
+ parent: {
+ id: '975c8d5bfd1dd20b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ error: {
+ culprit: 'logrusMiddleware',
+ log: {
+ level: 'error',
+ message: 'GET //api/products (502)'
+ },
+ id: '1f3cb98206b5c54225cb7c8908a658da',
+ grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a'
+ },
+ processor: {
+ name: 'error',
+ event: 'error'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T16:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '975c8d5bfd1dd20b',
+ sampled: false
+ },
+ timestamp: {
+ us: 1584975868787052
+ }
+ },
+ {
+ parent: {
+ id: '6fb0ff7365b87298'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ error: {
+ culprit: 'logrusMiddleware',
+ log: {
+ level: 'error',
+ message: 'GET //api/products (502)'
+ },
+ id: '1f3cb98206b5c54225cb7c8908a658d2',
+ grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a'
+ },
+ processor: {
+ name: 'error',
+ event: 'error'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T16:04:28.790Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ name: 'opbeans-python',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '6fb0ff7365b87298',
+ sampled: false
+ },
+ timestamp: {
+ us: 1584975868790000
+ }
+ }
+ ]
+ },
+ errorsPerTransaction: {
+ '975c8d5bfd1dd20b': 1,
+ '6fb0ff7365b87298': 1
+ }
+};
+
+export const traceChildStartBeforeParent = {
+ trace: {
+ items: [
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 46
+ }
+ },
+ source: {
+ ip: '172.19.0.13'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: '172.19.0.9',
+ full: 'http://172.19.0.9:3000/api/orders'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ type: 'apm-server',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ http: {
+ request: {
+ headers: {
+ Accept: ['*/*'],
+ 'User-Agent': ['Python/3.7 aiohttp/3.3.2'],
+ Host: ['172.19.0.9:3000'],
+ 'Accept-Encoding': ['gzip, deflate']
+ },
+ method: 'get',
+ socket: {
+ encrypted: false,
+ remote_address: '172.19.0.13'
+ },
+ body: {
+ original: '[REDACTED]'
+ }
+ },
+ response: {
+ headers: {
+ 'Transfer-Encoding': ['chunked'],
+ Date: ['Mon, 23 Mar 2020 15:04:28 GMT'],
+ 'Content-Type': ['application/json;charset=ISO-8859-1']
+ },
+ status_code: 200,
+ finished: true,
+ headers_sent: true
+ },
+ version: '1.1'
+ },
+ client: {
+ ip: '172.19.0.13'
+ },
+ transaction: {
+ duration: {
+ us: 18842
+ },
+ result: 'HTTP 2xx',
+ name: 'DispatcherServlet#doGet',
+ id: '49809ad3c26adf74',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ user_agent: {
+ original: 'Python/3.7 aiohttp/3.3.2',
+ name: 'Other',
+ device: {
+ name: 'Other'
+ }
+ },
+ timestamp: {
+ us: 1584975868785000
+ }
+ },
+ {
+ parent: {
+ id: 'fc107f7b556eb49b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ framework: {
+ name: 'gin',
+ version: 'v1.4.0'
+ },
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ duration: {
+ us: 16597
+ },
+ result: 'HTTP 2xx',
+ name: 'GET /api/orders',
+ id: '975c8d5bfd1dd20b',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ type: 'request',
+ sampled: true
+ },
+ timestamp: {
+ us: 1584975868787052
+ }
+ },
+ {
+ parent: {
+ id: 'daae24d83c269918'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ timestamp: {
+ us: 1584975868780000
+ },
+ processor: {
+ name: 'transaction',
+ event: 'transaction'
+ },
+ url: {
+ path: '/api/orders',
+ scheme: 'http',
+ port: 3000,
+ domain: 'opbeans-go',
+ full: 'http://opbeans-go:3000/api/orders'
+ },
+ '@timestamp': '2020-03-23T15:04:28.788Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ result: 'HTTP 2xx',
+ duration: {
+ us: 1464
+ },
+ name: 'I started before my parent 😰',
+ span_count: {
+ dropped: 0,
+ started: 1
+ },
+ id: '6fb0ff7365b87298',
+ type: 'request',
+ sampled: true
+ }
+ },
+ {
+ container: {
+ id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ parent: {
+ id: '49809ad3c26adf74'
+ },
+ process: {
+ pid: 6,
+ title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java',
+ ppid: 1
+ },
+ agent: {
+ name: 'java',
+ ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb',
+ version: '1.14.1-SNAPSHOT'
+ },
+ internal: {
+ sampler: {
+ value: 44
+ }
+ },
+ destination: {
+ address: 'opbeans-go',
+ port: 3000
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ observer: {
+ hostname: 'f37f48d8b60b',
+ id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e',
+ type: 'apm-server',
+ ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9',
+ version: '8.0.0',
+ version_major: 8
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.785Z',
+ ecs: {
+ version: '1.4.0'
+ },
+ service: {
+ node: {
+ name:
+ '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e'
+ },
+ environment: 'production',
+ name: 'opbeans-java',
+ runtime: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ language: {
+ name: 'Java',
+ version: '10.0.2'
+ },
+ version: 'None'
+ },
+ host: {
+ hostname: '4cf84d094553',
+ os: {
+ platform: 'Linux'
+ },
+ ip: '172.19.0.9',
+ name: '4cf84d094553',
+ architecture: 'amd64'
+ },
+ connection: {
+ hash:
+ "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}"
+ },
+ transaction: {
+ id: '49809ad3c26adf74'
+ },
+ timestamp: {
+ us: 1584975868785273
+ },
+ span: {
+ duration: {
+ us: 17530
+ },
+ subtype: 'http',
+ name: 'GET opbeans-go',
+ destination: {
+ service: {
+ resource: 'opbeans-go:3000',
+ name: 'http://opbeans-go:3000',
+ type: 'external'
+ }
+ },
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-go:3000/api/orders'
+ }
+ },
+ id: 'fc107f7b556eb49b',
+ type: 'external'
+ }
+ },
+ {
+ parent: {
+ id: '975c8d5bfd1dd20b'
+ },
+ agent: {
+ name: 'go',
+ version: '1.7.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.787Z',
+ service: {
+ node: {
+ name:
+ 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29'
+ },
+ environment: 'production',
+ name: 'opbeans-go',
+ runtime: {
+ name: 'gc',
+ version: 'go1.14.1'
+ },
+ language: {
+ name: 'go',
+ version: 'go1.14.1'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '975c8d5bfd1dd20b'
+ },
+ timestamp: {
+ us: 1584975868787174
+ },
+ span: {
+ duration: {
+ us: 16250
+ },
+ subtype: 'http',
+ destination: {
+ service: {
+ resource: 'opbeans-python:3000',
+ name: 'http://opbeans-python:3000',
+ type: 'external'
+ }
+ },
+ name: 'I am his 👇🏻 parent 😡',
+ http: {
+ response: {
+ status_code: 200
+ },
+ url: {
+ original: 'http://opbeans-python:3000/api/orders'
+ }
+ },
+ id: 'daae24d83c269918',
+ type: 'external'
+ }
+ },
+ {
+ container: {
+ id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ parent: {
+ id: '6fb0ff7365b87298'
+ },
+ agent: {
+ name: 'python',
+ version: '5.5.2'
+ },
+ processor: {
+ name: 'transaction',
+ event: 'span'
+ },
+ trace: {
+ id: '513d33fafe99bbe6134749310c9b5322'
+ },
+ '@timestamp': '2020-03-23T15:04:28.790Z',
+ service: {
+ node: {
+ name:
+ 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51'
+ },
+ environment: 'production',
+ framework: {
+ name: 'django',
+ version: '2.1.13'
+ },
+ name: 'opbeans-python',
+ runtime: {
+ name: 'CPython',
+ version: '3.6.10'
+ },
+ language: {
+ name: 'python',
+ version: '3.6.10'
+ },
+ version: 'None'
+ },
+ transaction: {
+ id: '6fb0ff7365b87298'
+ },
+ timestamp: {
+ us: 1584975868781000
+ },
+ span: {
+ duration: {
+ us: 2519
+ },
+ subtype: 'postgresql',
+ name: 'I am using my parents skew 😇',
+ destination: {
+ service: {
+ resource: 'postgresql',
+ name: 'postgresql',
+ type: 'db'
+ }
+ },
+ action: 'query',
+ id: 'c9407abb4d08ead1',
+ type: 'db',
+ sync: true,
+ db: {
+ statement:
+ 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000',
+ type: 'sql'
+ }
+ }
+ }
+ ],
+ exceedsMax: false,
+ errorDocs: []
+ },
+ errorsPerTransaction: {}
+};
diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js
index 519ba0b1e3d96..bc97643689e12 100644
--- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js
+++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js
@@ -310,9 +310,15 @@ app.controller(
const layerListConfigOnly = copyPersistentState(layerList);
const savedLayerList = savedMap.getLayerList();
- const oldConfig = savedLayerList ? savedLayerList : initialLayerListConfig;
- return !_.isEqual(layerListConfigOnly, oldConfig);
+ return !savedLayerList
+ ? !_.isEqual(layerListConfigOnly, initialLayerListConfig)
+ : // savedMap stores layerList as a JSON string using JSON.stringify.
+ // JSON.stringify removes undefined properties from objects.
+ // savedMap.getLayerList converts the JSON string back into Javascript array of objects.
+ // Need to perform the same process for layerListConfigOnly to compare apples to apples
+ // and avoid undefined properties in layerListConfigOnly triggering unsaved changes.
+ !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList);
}
function isOnMapNow() {
diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
index a1c15e27c9eb3..5e8f720fcc5e3 100644
--- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
+++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js
@@ -28,12 +28,20 @@ export function DynamicColorForm({
};
if (type === COLOR_MAP_TYPE.ORDINAL) {
newColorOptions.useCustomColorRamp = useCustomColorMap;
- newColorOptions.customColorRamp = customColorMap;
- newColorOptions.color = color;
+ if (customColorMap) {
+ newColorOptions.customColorRamp = customColorMap;
+ }
+ if (color) {
+ newColorOptions.color = color;
+ }
} else {
newColorOptions.useCustomColorPalette = useCustomColorMap;
- newColorOptions.customColorPalette = customColorMap;
- newColorOptions.colorCategory = color;
+ if (customColorMap) {
+ newColorOptions.customColorPalette = customColorMap;
+ }
+ if (color) {
+ newColorOptions.colorCategory = color;
+ }
}
onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions);
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx
index fa474c4d601ad..cf1a4ebec9bb6 100644
--- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButtonIcon, EuiPopover, EuiSelectableOption } from '@elastic/eui';
+import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
@@ -62,13 +62,15 @@ export const InsertTimelinePopoverComponent: React.FC = ({
const insertTimelineButton = useMemo(
() => (
-
+ {i18n.INSERT_TIMELINE}
}>
+
+
),
[handleOpenPopover, isDisabled]
);
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts
index de3e3c8e792fe..101837168350f 100644
--- a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts
@@ -25,5 +25,5 @@ export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate(
);
export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', {
- defaultMessage: 'Insert Timeline…',
+ defaultMessage: 'Insert timeline link',
});
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts
new file mode 100644
index 0000000000000..dbd618f40155d
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export * from '../translations';
+
+export const SUCCESS_CONFIGURE = i18n.translate('xpack.siem.case.configure.successSaveToast', {
+ defaultMessage: 'Saved external connection settings',
+});
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
index b25667f070fdf..6524c40a8e6e4 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx
@@ -7,8 +7,8 @@
import { useState, useEffect, useCallback } from 'react';
import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api';
-import { useStateToaster, errorToToaster } from '../../../components/toasters';
-import * as i18n from '../translations';
+import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters';
+import * as i18n from './translations';
import { ClosureType } from './types';
import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer';
@@ -124,6 +124,8 @@ export const useCaseConfigure = ({
closureType: res.closureType,
});
}
+
+ displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster);
}
} catch (error) {
if (!didCancel) {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
index 601db373f041e..a453be32480e2 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts
@@ -10,6 +10,46 @@ export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle
defaultMessage: 'Error fetching data',
});
+export const ERROR_DELETING = i18n.translate('xpack.siem.containers.case.errorDeletingTitle', {
+ defaultMessage: 'Error deleting data',
+});
+
+export const UPDATED_CASE = (caseTitle: string) =>
+ i18n.translate('xpack.siem.containers.case.updatedCase', {
+ values: { caseTitle },
+ defaultMessage: 'Updated "{caseTitle}"',
+ });
+
+export const DELETED_CASES = (totalCases: number, caseTitle?: string) =>
+ i18n.translate('xpack.siem.containers.case.deletedCases', {
+ values: { caseTitle, totalCases },
+ defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
+ });
+
+export const CLOSED_CASES = ({
+ totalCases,
+ caseTitle,
+}: {
+ totalCases: number;
+ caseTitle?: string;
+}) =>
+ i18n.translate('xpack.siem.containers.case.closedCases', {
+ values: { caseTitle, totalCases },
+ defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
+ });
+
+export const REOPENED_CASES = ({
+ totalCases,
+ caseTitle,
+}: {
+ totalCases: number;
+ caseTitle?: string;
+}) =>
+ i18n.translate('xpack.siem.containers.case.reopenedCases', {
+ values: { caseTitle, totalCases },
+ defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}',
+ });
+
export const TAG_FETCH_FAILURE = i18n.translate(
'xpack.siem.containers.case.tagFetchFailDescription',
{
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
index bb215d6ac271c..cb3df78257dc1 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts
@@ -114,3 +114,8 @@ export interface ActionLicense {
enabledInConfig: boolean;
enabledInLicense: boolean;
}
+
+export interface DeleteCase {
+ id: string;
+ title?: string;
+}
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx
index f1129bae9f537..7d040c49f1971 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx
@@ -5,7 +5,7 @@
*/
import { useCallback, useReducer } from 'react';
-import { errorToToaster, useStateToaster } from '../../components/toasters';
+import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters';
import * as i18n from './translations';
import { patchCasesStatus } from './api';
import { BulkUpdateStatus, Case } from './types';
@@ -71,9 +71,22 @@ export const useUpdateCases = (): UseUpdateCase => {
const patchData = async () => {
try {
dispatch({ type: 'FETCH_INIT' });
- await patchCasesStatus(cases, abortCtrl.signal);
+ const patchResponse = await patchCasesStatus(cases, abortCtrl.signal);
if (!cancel) {
+ const resultCount = Object.keys(patchResponse).length;
+ const firstTitle = patchResponse[0].title;
+
dispatch({ type: 'FETCH_SUCCESS', payload: true });
+ const messageArgs = {
+ totalCases: resultCount,
+ caseTitle: resultCount === 1 ? firstTitle : '',
+ };
+ const message =
+ resultCount && patchResponse[0].status === 'open'
+ ? i18n.REOPENED_CASES(messageArgs)
+ : i18n.CLOSED_CASES(messageArgs);
+
+ displaySuccessToast(message, dispatchToaster);
}
} catch (error) {
if (!cancel) {
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx
index b44e01d06acaf..07e3786758aeb 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx
@@ -5,9 +5,10 @@
*/
import { useCallback, useReducer } from 'react';
-import { errorToToaster, useStateToaster } from '../../components/toasters';
+import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters';
import * as i18n from './translations';
import { deleteCases } from './api';
+import { DeleteCase } from './types';
interface DeleteState {
isDisplayConfirmDeleteModal: boolean;
@@ -57,9 +58,10 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => {
return state;
}
};
+
interface UseDeleteCase extends DeleteState {
dispatchResetIsDeleted: () => void;
- handleOnDeleteConfirm: (caseIds: string[]) => void;
+ handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void;
handleToggleModal: () => void;
}
@@ -72,21 +74,26 @@ export const useDeleteCases = (): UseDeleteCase => {
});
const [, dispatchToaster] = useStateToaster();
- const dispatchDeleteCases = useCallback((caseIds: string[]) => {
+ const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => {
let cancel = false;
const abortCtrl = new AbortController();
const deleteData = async () => {
try {
dispatch({ type: 'FETCH_INIT' });
+ const caseIds = cases.map(theCase => theCase.id);
await deleteCases(caseIds, abortCtrl.signal);
if (!cancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: true });
+ displaySuccessToast(
+ i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''),
+ dispatchToaster
+ );
}
} catch (error) {
if (!cancel) {
errorToToaster({
- title: i18n.ERROR_TITLE,
+ title: i18n.ERROR_DELETING,
error: error.body && error.body.message ? new Error(error.body.message) : error,
dispatchToaster,
});
diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
index 85ad4fd3fc47a..4973deef4d91a 100644
--- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx
@@ -5,8 +5,8 @@
*/
import { useReducer, useCallback } from 'react';
+import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters';
import { CasePatchRequest } from '../../../../../../plugins/case/common/api';
-import { errorToToaster, useStateToaster } from '../../components/toasters';
import { patchCase } from './api';
import * as i18n from './translations';
@@ -94,6 +94,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase =>
updateCase(response[0]);
}
dispatch({ type: 'FETCH_SUCCESS' });
+ displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster);
}
} catch (error) {
if (!cancel) {
diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx
index d67007399abea..536798ffad41b 100644
--- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx
+++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx
@@ -181,6 +181,7 @@ const ServiceNowConnectorFields: React.FunctionComponent
+
+
+
-
{
.last()
.simulate('click');
expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual(
- useGetCasesMockState.data.cases.map(theCase => theCase.id)
+ useGetCasesMockState.data.cases.map(({ id }) => ({ id }))
);
});
it('Bulk close status update', () => {
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
index 27316ab8427cb..dcfa1712c6ef9 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx
@@ -21,7 +21,7 @@ import styled, { css } from 'styled-components';
import * as i18n from './translations';
import { getCasesColumns } from './columns';
-import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types';
+import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types';
import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases';
import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status';
import { useDeleteCases } from '../../../../containers/case/use_delete_cases';
@@ -107,11 +107,24 @@ export const AllCases = React.memo(() => {
isDisplayConfirmDeleteModal,
} = useDeleteCases();
- const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases();
+ // Update case
+ const {
+ dispatchResetIsUpdated,
+ isLoading: isUpdating,
+ isUpdated,
+ updateBulkStatus,
+ } = useUpdateCases();
+ const [deleteThisCase, setDeleteThisCase] = useState({
+ title: '',
+ id: '',
+ });
+ const [deleteBulk, setDeleteBulk] = useState([]);
const refreshCases = useCallback(() => {
refetchCases(filterOptions, queryParams);
fetchCasesStatus();
+ setSelectedCases([]);
+ setDeleteBulk([]);
}, [filterOptions, queryParams]);
useEffect(() => {
@@ -124,11 +137,6 @@ export const AllCases = React.memo(() => {
dispatchResetIsUpdated();
}
}, [isDeleted, isUpdated]);
- const [deleteThisCase, setDeleteThisCase] = useState({
- title: '',
- id: '',
- });
- const [deleteBulk, setDeleteBulk] = useState([]);
const confirmDeleteModal = useMemo(
() => (
{
onCancel={handleToggleModal}
onConfirm={handleOnDeleteConfirm.bind(
null,
- deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id]
+ deleteBulk.length > 0 ? deleteBulk : [deleteThisCase]
)}
/>
),
@@ -150,10 +158,20 @@ export const AllCases = React.memo(() => {
setDeleteThisCase(deleteCase);
}, []);
- const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => {
- handleToggleModal();
- setDeleteBulk(deleteCases);
- }, []);
+ const toggleBulkDeleteModal = useCallback(
+ (caseIds: string[]) => {
+ handleToggleModal();
+ if (caseIds.length === 1) {
+ const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]);
+ if (singleCase) {
+ return setDeleteThisCase({ id: singleCase.id, title: singleCase.title });
+ }
+ }
+ const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id }));
+ setDeleteBulk(convertToDeleteCases);
+ },
+ [selectedCases]
+ );
const handleUpdateCaseStatus = useCallback(
(status: string) => {
@@ -289,7 +307,7 @@ export const AllCases = React.memo(() => {
- {(isCasesLoading || isDeleting) && !isDataEmpty && (
+ {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && (
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
index 1be0d6a3b5fcc..49f5f44cba271 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx
@@ -60,6 +60,6 @@ describe('CaseView actions', () => {
expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy();
wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click');
- expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([data.id]);
+ expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]);
});
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
index 1d90470eab0e1..04b79967aa36e 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx
@@ -34,7 +34,7 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => {
isModalVisible={isDisplayConfirmDeleteModal}
isPlural={false}
onCancel={handleToggleModal}
- onConfirm={handleOnDeleteConfirm.bind(null, [caseData.id])}
+ onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])}
/>
),
[isDisplayConfirmDeleteModal, caseData]
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
index a1f24275df6cd..18d5191fe6d33 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx
@@ -57,8 +57,12 @@ const FormWrapper = styled.div`
margin-top 40px;
}
- padding-top: ${theme.eui.paddingSizes.l};
- padding-bottom: ${theme.eui.paddingSizes.l};
+ & > :first-child {
+ margin-top: 0;
+ }
+
+ padding-top: ${theme.eui.paddingSizes.xl};
+ padding-bottom: ${theme.eui.paddingSizes.xl};
`}
`;
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
index d1f04a34b7bad..49caeae1c3a34 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts
@@ -155,7 +155,7 @@ export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate(
'xpack.siem.case.configureCases.warningMessage',
{
defaultMessage:
- 'Configuration seems to be invalid. The selected connector is missing. Did you delete the connector?',
+ 'The selected connector has been deleted. Either select a different connector or create a new one.',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
index 0ca6bcff513fc..066145f7762c9 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts
@@ -22,13 +22,13 @@ export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate(
}
);
-export const COPY_LINK_COMMENT = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', {
- defaultMessage: 'click to copy comment link',
+export const COPY_REFERENCE_LINK = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', {
+ defaultMessage: 'Copy reference link',
});
export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate(
'xpack.siem.case.caseView.moveToCommentAria',
{
- defaultMessage: 'click to highlight the reference comment',
+ defaultMessage: 'Highlight the referenced comment',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
index cc36e791e35b4..340e24e8fa55b 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx
@@ -154,6 +154,7 @@ export const UserActionItem = ({
labelQuoteAction={labelQuoteAction}
labelTitle={labelTitle ?? <>>}
linkId={linkId}
+ fullName={fullName}
username={username}
updatedAt={updatedAt}
onEdit={onEdit}
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
index 94185cb4d130c..af1a1fdff26ce 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx
@@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonIcon } from '@elastic/eui';
+import {
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiText,
+ EuiButtonIcon,
+ EuiToolTip,
+} from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import copy from 'copy-to-clipboard';
import { isEmpty } from 'lodash/fp';
@@ -33,6 +40,7 @@ interface UserActionTitleProps {
labelQuoteAction?: string;
labelTitle: JSX.Element;
linkId?: string | null;
+ fullName?: string | null;
updatedAt?: string | null;
username: string;
onEdit?: (id: string) => void;
@@ -48,6 +56,7 @@ export const UserActionTitle = ({
labelQuoteAction,
labelTitle,
linkId,
+ fullName,
username,
updatedAt,
onEdit,
@@ -105,7 +114,9 @@ export const UserActionTitle = ({
- {username}
+ {fullName ?? username}}>
+ {username}
+
{labelTitle}
@@ -137,20 +148,24 @@ export const UserActionTitle = ({
{!isEmpty(linkId) && (
-
+ {i18n.MOVE_TO_ORIGINAL_COMMENT}}>
+
+
)}
-
+ {i18n.COPY_REFERENCE_LINK}}>
+
+
{propertyActions.length > 0 && (
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx
index 3109f2382c362..87a446c45d891 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx
@@ -13,6 +13,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
+ EuiToolTip,
} from '@elastic/eui';
import styled, { css } from 'styled-components';
import { ElasticUser } from '../../../../containers/case/types';
@@ -40,8 +41,8 @@ const MyFlexGroup = styled(EuiFlexGroup)`
const renderUsers = (
users: ElasticUser[],
handleSendEmail: (emailAddress: string | undefined | null) => void
-) => {
- return users.map(({ fullName, username, email }, key) => (
+) =>
+ users.map(({ fullName, username, email }, key) => (
@@ -49,11 +50,13 @@ const renderUsers = (
-
-
- {username}
-
-
+ {fullName ?? username}}>
+
+
+ {username}
+
+
+
@@ -63,11 +66,11 @@ const renderUsers = (
onClick={handleSendEmail.bind(null, email)}
iconType="email"
aria-label="email"
+ isDisabled={email == null}
/>
));
-};
export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => {
const handleSendEmail = useCallback(
diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx
new file mode 100644
index 0000000000000..62399891c9606
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { TimelinesPageComponent } from './timelines_page';
+import { useKibana } from '../../lib/kibana';
+import { shallow, ShallowWrapper } from 'enzyme';
+import React from 'react';
+import ApolloClient from 'apollo-client';
+
+jest.mock('../../lib/kibana', () => {
+ return {
+ useKibana: jest.fn(),
+ };
+});
+describe('TimelinesPageComponent', () => {
+ const mockAppollloClient = {} as ApolloClient;
+ let wrapper: ShallowWrapper;
+
+ describe('If the user is authorised', () => {
+ beforeAll(() => {
+ ((useKibana as unknown) as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ capabilities: {
+ siem: {
+ crud: true,
+ },
+ },
+ },
+ },
+ });
+ wrapper = shallow( );
+ });
+
+ afterAll(() => {
+ ((useKibana as unknown) as jest.Mock).mockReset();
+ });
+
+ test('should not show the import timeline modal by default', () => {
+ expect(
+ wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle')
+ ).toEqual(false);
+ });
+
+ test('should show the import timeline button', () => {
+ expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(true);
+ });
+
+ test('should show the import timeline modal after user clicking on the button', () => {
+ wrapper.find('[data-test-subj="open-import-data-modal-btn"]').simulate('click');
+ expect(
+ wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle')
+ ).toEqual(true);
+ });
+ });
+
+ describe('If the user is not authorised', () => {
+ beforeAll(() => {
+ ((useKibana as unknown) as jest.Mock).mockReturnValue({
+ services: {
+ application: {
+ capabilities: {
+ siem: {
+ crud: false,
+ },
+ },
+ },
+ },
+ });
+ wrapper = shallow( );
+ });
+
+ afterAll(() => {
+ ((useKibana as unknown) as jest.Mock).mockReset();
+ });
+ test('should not show the import timeline modal by default', () => {
+ expect(
+ wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle')
+ ).toEqual(false);
+ });
+
+ test('should not show the import timeline button', () => {
+ expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(false);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx
index 75bef7a04a4c9..73070d2b94aac 100644
--- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx
@@ -28,7 +28,7 @@ type OwnProps = TimelinesProps;
export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10;
-const TimelinesPageComponent: React.FC = ({ apolloClient }) => {
+export const TimelinesPageComponent: React.FC = ({ apolloClient }) => {
const [importDataModalToggle, setImportDataModalToggle] = useState(false);
const onImportTimelineBtnClick = useCallback(() => {
setImportDataModalToggle(true);
@@ -43,7 +43,11 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => {
{capabilitiesCanUserCRUD && (
-
+
{i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE}
)}
@@ -57,6 +61,7 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => {
importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD}
setImportDataModalToggle={setImportDataModalToggle}
title={i18n.ALL_TIMELINES_PANEL_TITLE}
+ data-test-subj="stateful-open-timeline"
/>
diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json
index bec6988bdebd9..c4705c8b8c16a 100644
--- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json
+++ b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json
@@ -4,7 +4,8 @@
"plugins/siem/**/*",
"legacy/plugins/siem/**/*",
"plugins/apm/typings/numeral.d.ts",
- "legacy/plugins/canvas/types/webpack.d.ts"
+ "legacy/plugins/canvas/types/webpack.d.ts",
+ "plugins/triggers_actions_ui/**/*"
],
"exclude": [
"test/**/*",
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
index 36764439462c3..3195483013c19 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
@@ -30,9 +30,13 @@ export const createIndexRoute = (router: IRouter) => {
try {
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
const callCluster = clusterClient.callAsCurrentUser;
+ if (!siemClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
+
const index = siemClient.signalsIndex;
const indexExists = await getIndexExists(callCluster, index);
if (indexExists) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
index aa418c11d9d16..c667e7ae9c463 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
@@ -38,7 +38,11 @@ export const deleteIndexRoute = (router: IRouter) => {
try {
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
+
+ if (!siemClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
const callCluster = clusterClient.callAsCurrentUser;
const index = siemClient.signalsIndex;
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
index 4fc5a4e1f347f..047176f155611 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
@@ -23,7 +23,11 @@ export const readIndexRoute = (router: IRouter) => {
try {
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
+
+ if (!siemClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
const index = siemClient.signalsIndex;
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts
index aa4f6150889f9..3209f5ce9f519 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts
@@ -62,6 +62,13 @@ describe('read_privileges route', () => {
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: 'Test error', status_code: 500 });
});
+
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getPrivilegeRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
});
describe('when security plugin is disabled', () => {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
index 2f5ea4d1ec767..d86880de65386 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
@@ -27,9 +27,14 @@ export const readPrivilegesRoute = (
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
+
try {
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
+
+ if (!siemClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
const index = siemClient.signalsIndex;
const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
index f53efc8a3234d..f0b975379388f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts
@@ -63,7 +63,7 @@ describe('add_prepackaged_rules_route', () => {
addPrepackedRulesRoute(server.router);
});
- describe('status codes with actionClient and alertClient', () => {
+ describe('status codes', () => {
test('returns 200 when creating with a valid actionClient and alertClient', async () => {
const request = addPrepackagedRulesRequest();
const response = await server.inject(request, context);
@@ -96,6 +96,13 @@ describe('add_prepackaged_rules_route', () => {
),
});
});
+
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(addPrepackagedRulesRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
});
describe('responses', () => {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
index 4e08188af0d12..3eba04debb21f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
@@ -33,16 +33,13 @@ export const addPrepackedRulesRoute = (router: IRouter) => {
const siemResponse = buildSiemResponse(response);
try {
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const clusterClient = context.core.elasticsearch.dataClient;
const savedObjectsClient = context.core.savedObjects.client;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
+ if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts
index 32b8eca298229..e6facf6f3b7a8 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts
@@ -42,7 +42,7 @@ describe('create_rules_bulk', () => {
createRulesBulkRoute(server.router);
});
- describe('status codes with actionClient and alertClient', () => {
+ describe('status codes', () => {
test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => {
const response = await server.inject(getReadBulkRequest(), context);
expect(response.status).toEqual(200);
@@ -54,6 +54,13 @@ describe('create_rules_bulk', () => {
expect(response.status).toEqual(404);
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
+
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getReadBulkRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
});
describe('unhappy paths', () => {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
index 1ca9f7ef9075e..daeb11e88508b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
@@ -37,15 +37,12 @@ export const createRulesBulkRoute = (router: IRouter) => {
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
+ if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
index 4da879d12f809..a77911bbb35e8 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts
@@ -60,6 +60,13 @@ describe('create_rules', () => {
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getCreateRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
+
it('returns 200 if license is not platinum', async () => {
(context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
index edf37bcb8dbe7..f68f204c12730 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -72,16 +72,13 @@ export const createRulesRoute = (router: IRouter): void => {
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const clusterClient = context.core.elasticsearch.dataClient;
const savedObjectsClient = context.core.savedObjects.client;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
+ if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
index 85cfeefdceead..33ffc245e7668 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
@@ -35,11 +35,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => {
const handler: Handler = async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!actionsClient || !alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
index 6fd50abd9364a..a4e659da76bb2 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
@@ -34,12 +34,9 @@ export const deleteRulesRoute = (router: IRouter) => {
try {
const { id, rule_id: ruleId } = request.query;
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!actionsClient || !alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
index c434f42780e47..50eafe163c265 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
@@ -28,10 +28,7 @@ export const exportRulesRoute = (router: IRouter, config: LegacyServices['config
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
if (!alertsClient) {
return siemResponse.error({ statusCode: 404 });
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
index 961859417ef1b..77351d2e0751b 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
@@ -32,10 +32,7 @@ export const findRulesRoute = (router: IRouter) => {
try {
const { query } = request;
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
index 4f4ae7c2c1fa6..6fee4d71a904e 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
@@ -35,10 +35,7 @@ export const findRulesStatusesRoute = (router: IRouter) => {
async (context, request, response) => {
const { query } = request;
const siemResponse = buildSiemResponse(response);
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
index 7e16b4495593e..7f0bf4bf81179 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts
@@ -29,10 +29,7 @@ export const getPrepackagedRulesStatusRoute = (router: IRouter) => {
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
if (!alertsClient) {
return siemResponse.error({ statusCode: 404 });
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
index aacf83b9ec58a..61f5e6faf1bdb 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
@@ -101,6 +101,13 @@ describe('import_rules_route', () => {
expect(response.status).toEqual(404);
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
+
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(request, contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
});
describe('unhappy paths', () => {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 2e6c72a87ec7f..d9fc89740c9ef 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -57,30 +57,27 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
- const clusterClient = context.core.elasticsearch.dataClient;
- const savedObjectsClient = context.core.savedObjects.client;
- const siemClient = context.siem.getSiemClient();
+ try {
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
+ const clusterClient = context.core.elasticsearch.dataClient;
+ const savedObjectsClient = context.core.savedObjects.client;
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
- return siemResponse.error({ statusCode: 404 });
- }
+ if (!siemClient || !actionsClient || !alertsClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
- const { filename } = request.body.file.hapi;
- const fileExtension = extname(filename).toLowerCase();
- if (fileExtension !== '.ndjson') {
- return siemResponse.error({
- statusCode: 400,
- body: `Invalid file extension ${fileExtension}`,
- });
- }
+ const { filename } = request.body.file.hapi;
+ const fileExtension = extname(filename).toLowerCase();
+ if (fileExtension !== '.ndjson') {
+ return siemResponse.error({
+ statusCode: 400,
+ body: `Invalid file extension ${fileExtension}`,
+ });
+ }
- const objectLimit = config().get('savedObjects.maxImportExportSize');
- try {
+ const objectLimit = config().get('savedObjects.maxImportExportSize');
const readStream = createRulesStreamFromNdJson(objectLimit);
const parsedObjects = await createPromiseFromStreams([
request.body.file,
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
index 645dbdadf8cab..b19039321a6d8 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
@@ -37,11 +37,8 @@ export const patchRulesBulkRoute = (router: IRouter) => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!actionsClient || !alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
index 620bcd8fc17b0..fab53079361ad 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
@@ -74,12 +74,8 @@ export const patchRulesRoute = (router: IRouter) => {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
}
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
-
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
if (!actionsClient || !alertsClient) {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
index e4117166ed4fa..bc52445feee76 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
@@ -32,10 +32,7 @@ export const readRulesRoute = (router: IRouter) => {
const { id, rule_id: ruleId } = request.query;
const siemResponse = buildSiemResponse(response);
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
const savedObjectsClient = context.core.savedObjects.client;
try {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts
index 611b38ccbae8b..332a47d0c0fc2 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts
@@ -69,6 +69,13 @@ describe('update_rules_bulk', () => {
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getUpdateBulkRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
+
test('returns an error if update throws', async () => {
clients.alertsClient.update.mockImplementation(() => {
throw new Error('Test error');
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
index 4abeb840c8c0a..789f7d1ca0744 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
@@ -37,15 +37,12 @@ export const updateRulesBulkRoute = (router: IRouter) => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
+ if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts
index 717f2cc4a52fe..454fe1f0706cb 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts
@@ -67,6 +67,13 @@ describe('update_rules', () => {
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getUpdateRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
+
test('returns error when updating non-rule', async () => {
clients.alertsClient.find.mockResolvedValue(nonRuleFindResult());
const response = await server.inject(getUpdateRequest(), context);
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
index f0d5f08c5f636..5856575eb9799 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
@@ -74,15 +74,12 @@ export const updateRulesRoute = (router: IRouter) => {
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
- if (!context.alerting || !context.actions) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
- const actionsClient = context.actions.getActionsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
+ const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
- if (!actionsClient || !alertsClient) {
+ if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts
index 612d08c09785a..72f3c89f660c7 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts
@@ -49,6 +49,13 @@ describe('set signal status', () => {
expect(response.status).toEqual(200);
});
+ it('returns 404 if siem client is unavailable', async () => {
+ const { siem, ...contextWithoutSiem } = context;
+ const response = await server.inject(getSetSignalStatusByQueryRequest(), contextWithoutSiem);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
+ });
+
test('catches error if callAsCurrentUser throws error', async () => {
clients.clusterClient.callAsCurrentUser.mockImplementation(async () => {
throw new Error('Test error');
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
index c1cba641de3ef..2daf63c468593 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
@@ -24,9 +24,13 @@ export const setSignalsStatusRoute = (router: IRouter) => {
async (context, request, response) => {
const { signal_ids: signalIds, query, status } = request.body;
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem?.getSiemClient();
const siemResponse = buildSiemResponse(response);
+ if (!siemClient) {
+ return siemResponse.error({ statusCode: 404 });
+ }
+
let queryObject;
if (signalIds) {
queryObject = { ids: { values: signalIds } };
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
index 77b62b058fa54..f05f494619b9c 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
@@ -24,7 +24,7 @@ export const querySignalsRoute = (router: IRouter) => {
async (context, request, response) => {
const { query, aggs, _source, track_total_hits, size } = request.body;
const clusterClient = context.core.elasticsearch.dataClient;
- const siemClient = context.siem.getSiemClient();
+ const siemClient = context.siem!.getSiemClient();
const siemResponse = buildSiemResponse(response);
try {
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts
index e12bf50169c17..adabc62a9456f 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts
@@ -20,11 +20,7 @@ export const readTagsRoute = (router: IRouter) => {
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
-
- if (!context.alerting) {
- return siemResponse.error({ statusCode: 404 });
- }
- const alertsClient = context.alerting.getAlertsClient();
+ const alertsClient = context.alerting?.getAlertsClient();
if (!alertsClient) {
return siemResponse.error({ statusCode: 404 });
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
index ada11174c5340..68716bb4e3795 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts
@@ -16,7 +16,6 @@ import {
import { AlertsClient, PartialAlert } from '../../../../../../../plugins/alerting/server';
import { Alert } from '../../../../../../../plugins/alerting/common';
import { SIGNALS_ID } from '../../../../common/constants';
-import { LegacyRequest } from '../../../types';
import { ActionsClient } from '../../../../../../../plugins/actions/server';
import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types';
@@ -39,14 +38,6 @@ export interface FindParamsRest {
filter: string;
}
-export interface PatchRulesRequest extends LegacyRequest {
- payload: PatchRuleAlertParamsRest;
-}
-
-export interface UpdateRulesRequest extends LegacyRequest {
- payload: UpdateRuleAlertParamsRest;
-}
-
export interface RuleAlertType extends Alert {
params: RuleTypeParams;
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts
index 63aee97729141..6552f973a66fa 100644
--- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts
@@ -6,14 +6,14 @@
import Joi from 'joi';
const allowEmptyString = Joi.string().allow([null, '']);
-const columnHeaderType = Joi.string();
+const columnHeaderType = allowEmptyString;
export const created = Joi.number().allow(null);
-export const createdBy = Joi.string();
+export const createdBy = allowEmptyString;
export const description = allowEmptyString;
export const end = Joi.number();
export const eventId = allowEmptyString;
-export const eventType = Joi.string();
+export const eventType = allowEmptyString;
export const filters = Joi.array()
.items(
@@ -24,19 +24,11 @@ export const filters = Joi.array()
disabled: Joi.boolean().allow(null),
field: allowEmptyString,
formattedValue: allowEmptyString,
- index: {
- type: 'keyword',
- },
- key: {
- type: 'keyword',
- },
- negate: {
- type: 'boolean',
- },
+ index: allowEmptyString,
+ key: allowEmptyString,
+ negate: Joi.boolean().allow(null),
params: allowEmptyString,
- type: {
- type: 'keyword',
- },
+ type: allowEmptyString,
value: allowEmptyString,
}),
exists: allowEmptyString,
@@ -68,22 +60,22 @@ export const version = allowEmptyString;
export const columns = Joi.array().items(
Joi.object({
aggregatable: Joi.boolean().allow(null),
- category: Joi.string(),
+ category: allowEmptyString,
columnHeaderType,
description,
example: allowEmptyString,
indexes: allowEmptyString,
- id: Joi.string(),
+ id: allowEmptyString,
name,
placeholder: allowEmptyString,
searchable: Joi.boolean().allow(null),
- type: Joi.string(),
+ type: allowEmptyString,
}).required()
);
export const dataProviders = Joi.array()
.items(
Joi.object({
- id: Joi.string(),
+ id: allowEmptyString,
name: allowEmptyString,
enabled: Joi.boolean().allow(null),
excluded: Joi.boolean().allow(null),
@@ -98,7 +90,7 @@ export const dataProviders = Joi.array()
and: Joi.array()
.items(
Joi.object({
- id: Joi.string(),
+ id: allowEmptyString,
name,
enabled: Joi.boolean().allow(null),
excluded: Joi.boolean().allow(null),
@@ -122,9 +114,9 @@ export const dateRange = Joi.object({
});
export const favorite = Joi.array().items(
Joi.object({
- keySearch: Joi.string(),
- fullName: Joi.string(),
- userName: Joi.string(),
+ keySearch: allowEmptyString,
+ fullName: allowEmptyString,
+ userName: allowEmptyString,
favoriteDate: Joi.number(),
}).allow(null)
);
@@ -141,26 +133,26 @@ const noteItem = Joi.object({
});
export const eventNotes = Joi.array().items(noteItem);
export const globalNotes = Joi.array().items(noteItem);
-export const kqlMode = Joi.string();
+export const kqlMode = allowEmptyString;
export const kqlQuery = Joi.object({
filterQuery: Joi.object({
kuery: Joi.object({
- kind: Joi.string(),
+ kind: allowEmptyString,
expression: allowEmptyString,
}),
serializedQuery: allowEmptyString,
}),
});
export const pinnedEventIds = Joi.array()
- .items(Joi.string())
+ .items(allowEmptyString)
.allow(null);
export const sort = Joi.object({
- columnId: Joi.string(),
- sortDirection: Joi.string(),
+ columnId: allowEmptyString,
+ sortDirection: allowEmptyString,
});
/* eslint-disable @typescript-eslint/camelcase */
-export const ids = Joi.array().items(Joi.string());
+export const ids = Joi.array().items(allowEmptyString);
export const exclude_export_details = Joi.boolean();
export const file_name = allowEmptyString;
diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts
index 4119645a5af47..a52322f5f830c 100644
--- a/x-pack/legacy/plugins/siem/server/types.ts
+++ b/x-pack/legacy/plugins/siem/server/types.ts
@@ -7,12 +7,8 @@
import { Legacy } from 'kibana';
import { SiemClient } from './client';
-export { LegacyRequest } from '../../../../../src/core/server';
-
export interface LegacyServices {
- alerting?: Legacy.Server['plugins']['alerting'];
config: Legacy.Server['config'];
- route: Legacy.Server['route'];
}
export { SiemClient };
@@ -23,6 +19,6 @@ export interface SiemRequestContext {
declare module 'src/core/server' {
interface RequestHandlerContext {
- siem: SiemRequestContext;
+ siem?: SiemRequestContext;
}
}
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap
index 354521e7c55b9..ead27425c26f3 100644
--- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap
+++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap
@@ -53,9 +53,18 @@ exports[`ML Flyout component renders without errors 1`] = `
+
+
+ Cancel
+
+
@@ -206,8 +215,26 @@ exports[`ML Flyout component shows license info if no ml available 1`] = `
class="euiFlyoutFooter"
>
+
+
+
+
+ Cancel
+
+
+
+
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx
index 917367f3e8dad..fdecfbf20810c 100644
--- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx
@@ -7,6 +7,7 @@
import React, { useContext } from 'react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
@@ -64,11 +65,15 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM
{labels.TAKE_SOME_TIME_TEXT}
-
-
+
+
+ onClose()} disabled={isCreatingJob || isLoadingMLJob}>
+ {labels.CANCEL_LABEL}
+
+
onClickCreate()}
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx
index 570dd9d1bfa26..32374674771e8 100644
--- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx
+++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx
@@ -124,6 +124,13 @@ export const CREATE_NEW_JOB = i18n.translate(
}
);
+export const CANCEL_LABEL = i18n.translate(
+ 'xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+);
+
export const CREAT_ML_JOB_DESC = i18n.translate(
'xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription',
{
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
index 8623d02e72862..727d26b5868de 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts
@@ -76,7 +76,8 @@ export const getFileHandler: RequestHandler {
try {
const { pkgkey, filePath } = request.params;
- const registryResponse = await getFile(`/package/${pkgkey}/${filePath}`);
+ const [pkgName, pkgVersion] = pkgkey.split('-');
+ const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`);
const contentType = registryResponse.headers.get('Content-Type');
const customResponseObj: CustomHttpResponseOptions = {
body: registryResponse.body,
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
index 5153f9205dde7..6d5ca036aeb13 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts
@@ -11,19 +11,18 @@ const tests = [
{
package: {
assets: [
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
- name: 'coredns',
- version: '1.0.1',
+ path: '/package/coredns/1.0.1',
},
dataset: 'log',
filter: (path: string) => {
return true;
},
expected: [
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
- '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
+ '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
+ '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
},
{
@@ -32,8 +31,7 @@ const tests = [
'/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json',
'/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json',
],
- name: 'coredns',
- version: '1.0.1',
+ path: '/package/coredns/1.0.1',
},
// Non existant dataset
dataset: 'foo',
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
index e36c2de1b4e80..d7a5c5569986e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts
@@ -9,14 +9,16 @@ import * as Registry from '../registry';
import { cacheHas } from '../registry/cache';
// paths from RegistryPackage are routes to the assets on EPR
-// e.g. `/package/nginx-1.2.0/dataset/access/fields/fields.yml`
+// e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml`
// paths for ArchiveEntry are routes to the assets in the archive
// e.g. `nginx-1.2.0/dataset/access/fields/fields.yml`
// RegistryPackage paths have a `/package/` prefix compared to ArchiveEntry paths
+// and different package and version structure
const EPR_PATH_PREFIX = '/package';
function registryPathToArchivePath(registryPath: RegistryPackage['path']): string {
- const archivePath = registryPath.replace(`${EPR_PATH_PREFIX}/`, '');
- return archivePath;
+ const path = registryPath.replace(`${EPR_PATH_PREFIX}/`, '');
+ const [pkgName, pkgVersion] = path.split('/');
+ return path.replace(`${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`);
}
export function getAssets(
@@ -35,7 +37,7 @@ export function getAssets(
// if dataset, filter for them
if (datasetName) {
- const comparePath = `${EPR_PATH_PREFIX}/${packageInfo.name}-${packageInfo.version}/dataset/${datasetName}/`;
+ const comparePath = `${packageInfo.path}/dataset/${datasetName}/`;
if (!path.includes(comparePath)) {
continue;
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
index 7c315f7616e1f..36a04b88bba29 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
@@ -6,7 +6,6 @@
import { Response } from 'node-fetch';
import { URL } from 'url';
-import { sortBy } from 'lodash';
import {
AssetParts,
AssetsGroupedByServiceByType,
@@ -51,11 +50,7 @@ export async function fetchFindLatestPackage(
const res = await fetchUrl(url.toString());
const searchResults = JSON.parse(res);
if (searchResults.length) {
- // sort by version, then get the last (most recent)
- const latestPackage = sortBy(searchResults, ['version'])[
- searchResults.length - 1
- ];
- return latestPackage;
+ return searchResults[0];
} else {
throw new Error('package not found');
}
@@ -63,7 +58,8 @@ export async function fetchFindLatestPackage(
export async function fetchInfo(pkgkey: string): Promise {
const registryUrl = appContextService.getConfig()?.epm.registryUrl;
- return fetchUrl(`${registryUrl}/package/${pkgkey}`).then(JSON.parse);
+ // change pkg-version to pkg/version
+ return fetchUrl(`${registryUrl}/package/${pkgkey.replace('-', '/')}`).then(JSON.parse);
}
export async function fetchFile(filePath: string): Promise {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index 95a8dfbb308f8..9791cd9210fe2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -62,6 +62,9 @@ export interface LoadExploreDataArg {
export const SEARCH_SIZE = 1000;
+export const TRAINING_PERCENT_MIN = 1;
+export const TRAINING_PERCENT_MAX = 100;
+
export const defaultSearchQuery = {
match_all: {},
};
@@ -172,6 +175,19 @@ export const getDependentVar = (analysis: AnalysisConfig) => {
return depVar;
};
+export const getTrainingPercent = (analysis: AnalysisConfig) => {
+ let trainingPercent;
+
+ if (isRegressionAnalysis(analysis)) {
+ trainingPercent = analysis.regression.training_percent;
+ }
+
+ if (isClassificationAnalysis(analysis)) {
+ trainingPercent = analysis.classification.training_percent;
+ }
+ return trainingPercent;
+};
+
export const getPredictionFieldName = (analysis: AnalysisConfig) => {
// If undefined will be defaulted to dependent_variable when config is created
let predictionFieldName;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
index 263d43ceb2630..41430b163c029 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
@@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useMlContext } from '../../../../../contexts/ml';
+import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
@@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate(
interface Props {
jobId: string;
- jobStatus: DATA_FRAME_TASK_STATE;
}
-export const ClassificationExploration: FC = ({ jobId, jobStatus }) => {
+export const ClassificationExploration: FC = ({ jobId }) => {
const [jobConfig, setJobConfig] = useState(undefined);
+ const [jobStatus, setJobStatus] = useState(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined);
@@ -65,6 +66,15 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => {
setIsLoadingJobConfig(true);
try {
const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId);
+ const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId);
+ const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
+ ? analyticsStats.data_frame_analytics[0]
+ : undefined;
+
+ if (stats !== undefined && stats.state) {
+ setJobStatus(stats.state);
+ }
+
if (
Array.isArray(analyticsConfigs.data_frame_analytics) &&
analyticsConfigs.data_frame_analytics.length > 0
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
index 1c5563bdb4f83..91dae49ba5c49 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx
@@ -50,10 +50,47 @@ const defaultPanelWidth = 500;
interface Props {
jobConfig: DataFrameAnalyticsConfig;
- jobStatus: DATA_FRAME_TASK_STATE;
+ jobStatus?: DATA_FRAME_TASK_STATE;
searchQuery: ResultsSearchQuery;
}
+enum SUBSET_TITLE {
+ TRAINING = 'training',
+ TESTING = 'testing',
+ ENTIRE = 'entire',
+}
+
+const entireDatasetHelpText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixEntireHelpText',
+ {
+ defaultMessage: 'Normalized confusion matrix for entire dataset',
+ }
+);
+
+const testingDatasetHelpText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText',
+ {
+ defaultMessage: 'Normalized confusion matrix for testing dataset',
+ }
+);
+
+const trainingDatasetHelpText = i18n.translate(
+ 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText',
+ {
+ defaultMessage: 'Normalized confusion matrix for training dataset',
+ }
+);
+
+function getHelpText(dataSubsetTitle: string) {
+ let helpText = entireDatasetHelpText;
+ if (dataSubsetTitle === SUBSET_TITLE.TESTING) {
+ helpText = testingDatasetHelpText;
+ } else if (dataSubsetTitle === SUBSET_TITLE.TRAINING) {
+ helpText = trainingDatasetHelpText;
+ }
+ return helpText;
+}
+
export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => {
const {
services: { docLinks },
@@ -66,6 +103,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
const [popoverContents, setPopoverContents] = useState([]);
const [docsCount, setDocsCount] = useState(null);
const [error, setError] = useState(null);
+ const [dataSubsetTitle, setDataSubsetTitle] = useState(SUBSET_TITLE.ENTIRE);
const [panelWidth, setPanelWidth] = useState(defaultPanelWidth);
// Column visibility
const [visibleColumns, setVisibleColumns] = useState(() =>
@@ -197,6 +235,18 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
hasIsTrainingClause[0] &&
hasIsTrainingClause[0].match[`${resultsField}.is_training`];
+ const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined;
+
+ if (noTrainingQuery) {
+ setDataSubsetTitle(SUBSET_TITLE.ENTIRE);
+ } else {
+ setDataSubsetTitle(
+ isTrainingClause && isTrainingClause.query === 'true'
+ ? SUBSET_TITLE.TRAINING
+ : SUBSET_TITLE.TESTING
+ );
+ }
+
loadData({ isTrainingClause });
}, [JSON.stringify(searchQuery)]);
@@ -268,9 +318,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
@@ -302,14 +354,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
- {i18n.translate(
- 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText',
- {
- defaultMessage: 'Normalized confusion matrix',
- }
- )}
-
+ {getHelpText(dataSubsetTitle)}
>;
}
@@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo(
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
= React.memo(
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx
index 030447873f6a5..7cdd15e49bd14 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx
@@ -6,7 +6,6 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { MlContext } from '../../../../../contexts/ml';
import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value';
@@ -22,7 +21,7 @@ describe('Data Frame Analytics: ', () => {
test('Minimal initialization', () => {
const wrapper = shallow(
-
+
);
// Without the jobConfig being loaded, the component will just return empty.
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
index 214bc01c6a2ef..d686c605f1912 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx
@@ -27,7 +27,6 @@ import {
import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common';
-import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
import { useExploreData, TableItem } from '../../hooks/use_explore_data';
@@ -50,7 +49,6 @@ const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => (
interface ExplorationProps {
jobId: string;
- jobStatus: DATA_FRAME_TASK_STATE;
}
const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => {
@@ -63,11 +61,12 @@ const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) =>
).length;
};
-export const OutlierExploration: FC = React.memo(({ jobId, jobStatus }) => {
+export const OutlierExploration: FC = React.memo(({ jobId }) => {
const {
errorMessage,
indexPattern,
jobConfig,
+ jobStatus,
pagination,
searchQuery,
selectedFields,
@@ -173,9 +172,11 @@ export const OutlierExploration: FC = React.memo(({ jobId, job
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
index 74937bf761285..9f235ae6c45c0 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx
@@ -39,7 +39,7 @@ import {
interface Props {
jobConfig: DataFrameAnalyticsConfig;
- jobStatus: DATA_FRAME_TASK_STATE;
+ jobStatus?: DATA_FRAME_TASK_STATE;
searchQuery: ResultsSearchQuery;
}
@@ -248,9 +248,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
index 3dfd95a27f8a7..4f3c4048d40d5 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx
@@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
import { useMlContext } from '../../../../../contexts/ml';
+import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
@@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate(
interface Props {
jobId: string;
- jobStatus: DATA_FRAME_TASK_STATE;
}
-export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
+export const RegressionExploration: FC = ({ jobId }) => {
const [jobConfig, setJobConfig] = useState(undefined);
+ const [jobStatus, setJobStatus] = useState(undefined);
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined);
@@ -65,6 +66,15 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => {
setIsLoadingJobConfig(true);
try {
const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId);
+ const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId);
+ const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
+ ? analyticsStats.data_frame_analytics[0]
+ : undefined;
+
+ if (stats !== undefined && stats.state) {
+ setJobStatus(stats.state);
+ }
+
if (
Array.isArray(analyticsConfigs.data_frame_analytics) &&
analyticsConfigs.data_frame_analytics.length > 0
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
index 7a6b2b23ba7a3..b896c34a582f7 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx
@@ -86,7 +86,7 @@ const showingFirstDocs = i18n.translate(
interface Props {
jobConfig: DataFrameAnalyticsConfig;
- jobStatus: DATA_FRAME_TASK_STATE;
+ jobStatus?: DATA_FRAME_TASK_STATE;
setEvaluateSearchQuery: React.Dispatch>;
}
@@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo(
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
= React.memo(
-
- {getTaskStateBadge(jobStatus)}
-
+ {jobStatus !== undefined && (
+
+ {getTaskStateBadge(jobStatus)}
+
+ )}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts
index 6ad0a1822e490..d637057a4430d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts
@@ -19,6 +19,7 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_
import { getIndexPatternIdFromName } from '../../../../../util/index_utils';
import { getNestedProperty } from '../../../../../util/object_utils';
import { useMlContext } from '../../../../../contexts/ml';
+import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
import {
getDefaultSelectableFields,
@@ -31,6 +32,7 @@ import {
import { isKeywordAndTextType } from '../../../../common/fields';
import { getOutlierScoreFieldName } from './common';
+import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
export type TableItem = Record;
@@ -40,6 +42,7 @@ interface UseExploreDataReturnType {
errorMessage: string;
indexPattern: IndexPattern | undefined;
jobConfig: DataFrameAnalyticsConfig | undefined;
+ jobStatus: DATA_FRAME_TASK_STATE | undefined;
pagination: Pagination;
searchQuery: SavedSearchQuery;
selectedFields: EsFieldName[];
@@ -74,6 +77,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => {
const [indexPattern, setIndexPattern] = useState(undefined);
const [jobConfig, setJobConfig] = useState(undefined);
+ const [jobStatus, setJobStatus] = useState(undefined);
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@@ -90,6 +94,15 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => {
useEffect(() => {
(async function() {
const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId);
+ const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId);
+ const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
+ ? analyticsStats.data_frame_analytics[0]
+ : undefined;
+
+ if (stats !== undefined && stats.state) {
+ setJobStatus(stats.state);
+ }
+
if (
Array.isArray(analyticsConfigs.data_frame_analytics) &&
analyticsConfigs.data_frame_analytics.length > 0
@@ -215,6 +228,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => {
errorMessage,
indexPattern,
jobConfig,
+ jobStatus,
pagination,
rowCount,
searchQuery,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
index efbebc1564bf9..c8349084dbda8 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
@@ -27,13 +27,11 @@ import { RegressionExploration } from './components/regression_exploration';
import { ClassificationExploration } from './components/classification_exploration';
import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics';
-import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common';
export const Page: FC<{
jobId: string;
analysisType: ANALYSIS_CONFIG_TYPE;
- jobStatus: DATA_FRAME_TASK_STATE;
-}> = ({ jobId, analysisType, jobStatus }) => (
+}> = ({ jobId, analysisType }) => (
@@ -68,13 +66,13 @@ export const Page: FC<{
{analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && (
-
+
)}
{analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
-
+
)}
{analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
-
+
)}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
index 425e3bc903d04..4e19df9ae22a8 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx
@@ -33,13 +33,12 @@ export const AnalyticsViewAction = {
isPrimary: true,
render: (item: DataFrameAnalyticsListRow) => {
const analysisType = getAnalysisType(item.config.analysis);
- const jobStatus = item.stats.state;
const isDisabled =
!isRegressionAnalysis(item.config.analysis) &&
!isOutlierAnalysis(item.config.analysis) &&
!isClassificationAnalysis(item.config.analysis);
- const url = getResultsUrl(item.id, analysisType, jobStatus);
+ const url = getResultsUrl(item.id, analysisType);
return (
= ({ actions, state }) => {
- const {
- resetAdvancedEditorMessages,
- setAdvancedEditorRawString,
- setFormState,
- setJobConfig,
- } = actions;
+ const { setAdvancedEditorRawString, setFormState } = actions;
const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state;
@@ -45,12 +39,6 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac
const onChange = (str: string) => {
setAdvancedEditorRawString(str);
- try {
- const resultJobConfig = JSON.parse(collapseLiteralStrings(str));
- setJobConfig(resultJobConfig);
- } catch (e) {
- resetAdvancedEditorMessages();
- }
};
// Temp effect to close the context menu popover on Clone button click
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx
index 32384e1949d0a..b0f13e398cc50 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx
@@ -26,7 +26,14 @@ export const CreateAnalyticsFlyout: FC = ({
state,
}) => {
const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions;
- const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid, cloneJob } = state;
+ const {
+ isJobCreated,
+ isJobStarted,
+ isModalButtonDisabled,
+ isValid,
+ isAdvancedEditorValidJson,
+ cloneJob,
+ } = state;
const headerText = !!cloneJob
? i18n.translate('xpack.ml.dataframe.analytics.clone.flyoutHeaderTitle', {
@@ -61,7 +68,7 @@ export const CreateAnalyticsFlyout: FC = ({
{!isJobCreated && !isJobStarted && (
= ({ actions, sta
})}
>
setFormState({ trainingPercent: e.target.value })}
+ onChange={e => setFormState({ trainingPercent: +e.target.value })}
data-test-subj="mlAnalyticsCreateJobFlyoutTrainingPercentSlider"
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts
index 8112a0fdb9e29..c40ab31f6615f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts
@@ -16,9 +16,11 @@ type SourceIndex = DataFrameAnalyticsConfig['source']['index'];
const getMockState = ({
index,
+ trainingPercent = 75,
modelMemoryLimit = '100mb',
}: {
index: SourceIndex;
+ trainingPercent?: number;
modelMemoryLimit?: string;
}) =>
merge(getInitialState(), {
@@ -31,7 +33,9 @@ const getMockState = ({
jobConfig: {
source: { index },
dest: { index: 'the-destination-index' },
- analysis: {},
+ analysis: {
+ classification: { dependent_variable: 'the-variable', training_percent: trainingPercent },
+ },
model_memory_limit: modelMemoryLimit,
},
});
@@ -151,6 +155,24 @@ describe('useCreateAnalyticsForm', () => {
.isValid
).toBe(false);
});
+
+ test('validateAdvancedEditor(): check training percent validation', () => {
+ // valid training_percent value
+ expect(
+ validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 75 }))
+ .isValid
+ ).toBe(true);
+ // invalid training_percent numeric value
+ expect(
+ validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 102 }))
+ .isValid
+ ).toBe(false);
+ // invalid training_percent numeric value if 0
+ expect(
+ validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 0 }))
+ .isValid
+ ).toBe(false);
+ });
});
describe('validateMinMML', () => {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index d045749a1a0dd..28d8afbcd88cc 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -11,6 +11,8 @@ import numeral from '@elastic/numeral';
import { isEmpty } from 'lodash';
import { isValidIndexName } from '../../../../../../../common/util/es_utils';
+import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools';
+
import { Action, ACTION } from './actions';
import { getInitialState, getJobConfigFromFormState, State } from './state';
import {
@@ -29,9 +31,12 @@ import {
} from '../../../../../../../common/constants/validation';
import {
getDependentVar,
+ getTrainingPercent,
isRegressionAnalysis,
isClassificationAnalysis,
ANALYSIS_CONFIG_TYPE,
+ TRAINING_PERCENT_MIN,
+ TRAINING_PERCENT_MAX,
} from '../../../../common/analytics';
import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public';
@@ -141,6 +146,7 @@ export const validateAdvancedEditor = (state: State): State => {
let dependentVariableEmpty = false;
let excludesValid = true;
+ let trainingPercentValid = true;
if (
jobConfig.analysis === undefined &&
@@ -169,6 +175,30 @@ export const validateAdvancedEditor = (state: State): State => {
message: '',
});
}
+
+ const trainingPercent = getTrainingPercent(jobConfig.analysis);
+ if (
+ trainingPercent !== undefined &&
+ (isNaN(trainingPercent) ||
+ trainingPercent < TRAINING_PERCENT_MIN ||
+ trainingPercent > TRAINING_PERCENT_MAX)
+ ) {
+ trainingPercentValid = false;
+
+ state.advancedEditorMessages.push({
+ error: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.trainingPercentInvalid',
+ {
+ defaultMessage: 'The training percent must be a value between {min} and {max}.',
+ values: {
+ min: TRAINING_PERCENT_MIN,
+ max: TRAINING_PERCENT_MAX,
+ },
+ }
+ ),
+ message: '',
+ });
+ }
}
if (sourceIndexNameEmpty) {
@@ -249,6 +279,7 @@ export const validateAdvancedEditor = (state: State): State => {
state.isValid =
maxDistinctValuesError === undefined &&
excludesValid &&
+ trainingPercentValid &&
state.form.modelMemoryLimitUnitValid &&
!jobIdEmpty &&
jobIdValid &&
@@ -365,7 +396,23 @@ export function reducer(state: State, action: Action): State {
return getInitialState();
case ACTION.SET_ADVANCED_EDITOR_RAW_STRING:
- return { ...state, advancedEditorRawString: action.advancedEditorRawString };
+ let resultJobConfig;
+ try {
+ resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString));
+ } catch (e) {
+ return {
+ ...state,
+ advancedEditorRawString: action.advancedEditorRawString,
+ isAdvancedEditorValidJson: false,
+ advancedEditorMessages: [],
+ };
+ }
+
+ return {
+ ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }),
+ advancedEditorRawString: action.advancedEditorRawString,
+ isAdvancedEditorValidJson: true,
+ };
case ACTION.SET_FORM_STATE:
const newFormState = { ...state.form, ...action.payload };
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index 719bb6c5b07c7..fe741fe9a92d4 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -82,6 +82,7 @@ export interface State {
indexNames: EsIndexName[];
indexPatternsMap: SourceIndexMap;
isAdvancedEditorEnabled: boolean;
+ isAdvancedEditorValidJson: boolean;
isJobCreated: boolean;
isJobStarted: boolean;
isModalButtonDisabled: boolean;
@@ -140,6 +141,7 @@ export const getInitialState = (): State => ({
indexNames: [],
indexPatternsMap: {},
isAdvancedEditorEnabled: false,
+ isAdvancedEditorValidJson: true,
isJobCreated: false,
isJobStarted: false,
isModalVisible: false,
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
index 37b9fe5e1f2d0..1f2a57f999775 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js
@@ -388,17 +388,23 @@ function getUrlVars(url) {
}
export function getSelectedJobIdFromUrl(url) {
- if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) {
- const urlParams = getUrlVars(url);
- const decodedJson = rison.decode(urlParams.mlManagement);
- return decodedJson.jobId;
+ if (typeof url === 'string') {
+ url = decodeURIComponent(url);
+ if (url.includes('mlManagement') && url.includes('jobId')) {
+ const urlParams = getUrlVars(url);
+ const decodedJson = rison.decode(urlParams.mlManagement);
+ return decodedJson.jobId;
+ }
}
}
export function clearSelectedJobIdFromUrl(url) {
- if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) {
- const urlParams = getUrlVars(url);
- const clearedParams = `ml#/jobs?_g=${urlParams._g}`;
- window.history.replaceState({}, document.title, clearedParams);
+ if (typeof url === 'string') {
+ url = decodeURIComponent(url);
+ if (url.includes('mlManagement') && url.includes('jobId')) {
+ const urlParams = getUrlVars(url);
+ const clearedParams = `ml#/jobs?_g=${urlParams._g}`;
+ window.history.replaceState({}, document.title, clearedParams);
+ }
}
}
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
index e00ff0333bb73..2dde5426ec9a0 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
@@ -13,7 +13,6 @@ import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_exploration';
import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics';
-import { DATA_FRAME_TASK_STATE } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { ML_BREADCRUMB } from '../../breadcrumbs';
const breadcrumbs = [
@@ -46,11 +45,10 @@ const PageWrapper: FC = ({ location, deps }) => {
}
const jobId: string = globalState.ml.jobId;
const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType;
- const jobStatus: DATA_FRAME_TASK_STATE = globalState.ml.jobStatus;
return (
-
+
);
};
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap
index a25498a637c2f..7b8283b7bec0e 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap
@@ -51,6 +51,7 @@ exports[`LoginForm login selector renders as expected with login form 1`] = `
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
+ isInvalid={false}
label={
-
@@ -170,6 +174,7 @@ exports[`LoginForm renders as expected 1`] = `
fullWidth={false}
hasChildLabel={true}
hasEmptyLabelSpace={false}
+ isInvalid={false}
label={
-
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
index a028eb1ba4b70..01f5c40a69aeb 100644
--- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx
@@ -9,6 +9,7 @@ import ReactMarkdown from 'react-markdown';
import {
EuiButton,
EuiCallOut,
+ EuiFieldPassword,
EuiFieldText,
EuiFormRow,
EuiPanel,
@@ -18,6 +19,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public';
+import { LoginValidator, LoginValidationResult } from './validate_login';
import { parseNext } from '../../../../../common/parse_next';
import { LoginSelector } from '../../../../../common/login_state';
@@ -40,6 +42,7 @@ interface State {
message:
| { type: MessageType.None }
| { type: MessageType.Danger | MessageType.Info; content: string };
+ formError: LoginValidationResult | null;
}
enum LoadingStateType {
@@ -55,14 +58,21 @@ enum MessageType {
}
export class LoginForm extends Component {
- public state: State = {
- loadingState: { type: LoadingStateType.None },
- username: '',
- password: '',
- message: this.props.infoMessage
- ? { type: MessageType.Info, content: this.props.infoMessage }
- : { type: MessageType.None },
- };
+ private readonly validator: LoginValidator;
+
+ constructor(props: Props) {
+ super(props);
+ this.validator = new LoginValidator({ shouldValidate: false });
+ this.state = {
+ loadingState: { type: LoadingStateType.None },
+ username: '',
+ password: '',
+ message: this.props.infoMessage
+ ? { type: MessageType.Info, content: this.props.infoMessage }
+ : { type: MessageType.None },
+ formError: null,
+ };
+ }
public render() {
return (
@@ -90,6 +100,7 @@ export class LoginForm extends Component {
defaultMessage="Username"
/>
}
+ {...this.validator.validateUsername(this.state.username)}
>
{
defaultMessage="Password"
/>
}
+ {...this.validator.validatePassword(this.state.password)}
>
- {
}
}
- private isFormValid = () => {
- const { username, password } = this.state;
-
- return username && password;
- };
-
private onUsernameChange = (e: ChangeEvent) => {
this.setState({
username: e.target.value,
@@ -271,8 +276,15 @@ export class LoginForm extends Component {
) => {
e.preventDefault();
- if (!this.isFormValid()) {
+ this.validator.enableValidation();
+
+ const { username, password } = this.state;
+ const result = this.validator.validateForLogin(username, password);
+ if (result.isInvalid) {
+ this.setState({ formError: result });
return;
+ } else {
+ this.setState({ formError: null });
}
this.setState({
@@ -281,7 +293,6 @@ export class LoginForm extends Component {
});
const { http } = this.props;
- const { username, password } = this.state;
try {
await http.post('/internal/security/login', { body: JSON.stringify({ username, password }) });
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts
new file mode 100644
index 0000000000000..6cd582bbcb4c0
--- /dev/null
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts
@@ -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 { LoginValidator, LoginValidationResult } from './validate_login';
+
+function expectValid(result: LoginValidationResult) {
+ expect(result.isInvalid).toBe(false);
+}
+
+function expectInvalid(result: LoginValidationResult) {
+ expect(result.isInvalid).toBe(true);
+}
+
+describe('LoginValidator', () => {
+ describe('#validateUsername', () => {
+ it(`returns 'valid' if validation is disabled`, () => {
+ expectValid(new LoginValidator().validateUsername(''));
+ });
+
+ it(`returns 'invalid' if username is missing`, () => {
+ expectInvalid(new LoginValidator({ shouldValidate: true }).validateUsername(''));
+ });
+
+ it(`returns 'valid' for correct usernames`, () => {
+ expectValid(new LoginValidator({ shouldValidate: true }).validateUsername('u'));
+ });
+ });
+
+ describe('#validatePassword', () => {
+ it(`returns 'valid' if validation is disabled`, () => {
+ expectValid(new LoginValidator().validatePassword(''));
+ });
+
+ it(`returns 'invalid' if password is missing`, () => {
+ expectInvalid(new LoginValidator({ shouldValidate: true }).validatePassword(''));
+ });
+
+ it(`returns 'valid' for correct passwords`, () => {
+ expectValid(new LoginValidator({ shouldValidate: true }).validatePassword('p'));
+ });
+ });
+
+ describe('#validateForLogin', () => {
+ it(`returns 'valid' if validation is disabled`, () => {
+ expectValid(new LoginValidator().validateForLogin('', ''));
+ });
+
+ it(`returns 'invalid' if username is invalid`, () => {
+ expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('', 'p'));
+ });
+
+ it(`returns 'invalid' if password is invalid`, () => {
+ expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', ''));
+ });
+
+ it(`returns 'valid' if username and password are valid`, () => {
+ expectValid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', 'p'));
+ });
+ });
+});
diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts
new file mode 100644
index 0000000000000..0873098a0ff1d
--- /dev/null
+++ b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts
@@ -0,0 +1,97 @@
+/*
+ * 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';
+
+interface LoginValidatorOptions {
+ shouldValidate?: boolean;
+}
+
+export interface LoginValidationResult {
+ isInvalid: boolean;
+ error?: string;
+}
+
+export class LoginValidator {
+ private shouldValidate?: boolean;
+
+ constructor(options: LoginValidatorOptions = {}) {
+ this.shouldValidate = options.shouldValidate;
+ }
+
+ public enableValidation() {
+ this.shouldValidate = true;
+ }
+
+ public disableValidation() {
+ this.shouldValidate = false;
+ }
+
+ public validateUsername(username: string): LoginValidationResult {
+ if (!this.shouldValidate) {
+ return valid();
+ }
+
+ if (!username) {
+ // Elasticsearch has more stringent requirements for usernames in the Native realm. However, the login page is used for other realms,
+ // such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the username is not empty.
+ return invalid(
+ i18n.translate(
+ 'xpack.security.authentication.login.validateLogin.requiredUsernameErrorMessage',
+ {
+ defaultMessage: 'Username is required',
+ }
+ )
+ );
+ }
+
+ return valid();
+ }
+
+ public validatePassword(password: string): LoginValidationResult {
+ if (!this.shouldValidate) {
+ return valid();
+ }
+
+ if (!password) {
+ // Elasticsearch has more stringent requirements for passwords in the Native realm. However, the login page is used for other realms,
+ // such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the password is not empty.
+ return invalid(
+ i18n.translate(
+ 'xpack.security.authentication.login.validateLogin.requiredPasswordErrorMessage',
+ {
+ defaultMessage: 'Password is required',
+ }
+ )
+ );
+ }
+ return valid();
+ }
+
+ public validateForLogin(username: string, password: string): LoginValidationResult {
+ const { isInvalid: isUsernameInvalid } = this.validateUsername(username);
+ const { isInvalid: isPasswordInvalid } = this.validatePassword(password);
+
+ if (isUsernameInvalid || isPasswordInvalid) {
+ return invalid();
+ }
+
+ return valid();
+ }
+}
+
+function invalid(error?: string): LoginValidationResult {
+ return {
+ isInvalid: true,
+ error,
+ };
+}
+
+function valid(): LoginValidationResult {
+ return {
+ isInvalid: false,
+ };
+}
diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js
index 242ee890d4847..b0ca33b00fde8 100644
--- a/x-pack/scripts/functional_tests.js
+++ b/x-pack/scripts/functional_tests.js
@@ -27,6 +27,7 @@ const onlyNotInCoverageTests = [
require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'),
require.resolve('../test/pki_api_integration/config.ts'),
require.resolve('../test/login_selector_api_integration/config.ts'),
+ require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'),
require.resolve('../test/spaces_api_integration/spaces_only/config.ts'),
require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'),
require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'),
diff --git a/x-pack/test/api_integration/apis/apm/custom_link.ts b/x-pack/test/api_integration/apis/apm/custom_link.ts
new file mode 100644
index 0000000000000..8aefadd811775
--- /dev/null
+++ b/x-pack/test/api_integration/apis/apm/custom_link.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 querystring from 'querystring';
+// import {isEmpty} from 'lodash'
+import URL from 'url';
+import expect from '@kbn/expect';
+import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function customLinksTests({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const log = getService('log');
+
+ function searchCustomLinks(filters?: any) {
+ const path = URL.format({
+ pathname: `/api/apm/settings/custom_links`,
+ query: filters,
+ });
+ return supertest.get(path).set('kbn-xsrf', 'foo');
+ }
+
+ async function createCustomLink(customLink: CustomLink) {
+ log.debug('creating configuration', customLink);
+ const res = await supertest
+ .post(`/api/apm/settings/custom_links`)
+ .send(customLink)
+ .set('kbn-xsrf', 'foo');
+
+ throwOnError(res);
+
+ return res;
+ }
+
+ async function updateCustomLink(id: string, customLink: CustomLink) {
+ log.debug('updating configuration', id, customLink);
+ const res = await supertest
+ .put(`/api/apm/settings/custom_links/${id}`)
+ .send(customLink)
+ .set('kbn-xsrf', 'foo');
+
+ throwOnError(res);
+
+ return res;
+ }
+
+ async function deleteCustomLink(id: string) {
+ log.debug('deleting configuration', id);
+ const res = await supertest
+ .delete(`/api/apm/settings/custom_links/${id}`)
+ .set('kbn-xsrf', 'foo');
+
+ throwOnError(res);
+
+ return res;
+ }
+
+ function throwOnError(res: any) {
+ const { statusCode, req, body } = res;
+ if (statusCode !== 200) {
+ throw new Error(`
+ Endpoint: ${req.method} ${req.path}
+ Service: ${JSON.stringify(res.request._data.service)}
+ Status code: ${statusCode}
+ Response: ${body.message}`);
+ }
+ }
+
+ describe('custom links', () => {
+ before(async () => {
+ const customLink = {
+ url: 'https://elastic.co',
+ label: 'with filters',
+ filters: [
+ { key: 'service.name', value: 'baz' },
+ { key: 'transaction.type', value: 'qux' },
+ ],
+ } as CustomLink;
+ await createCustomLink(customLink);
+ });
+ it('fetches a custom link', async () => {
+ const { status, body } = await searchCustomLinks({
+ 'service.name': 'baz',
+ 'transaction.type': 'qux',
+ });
+ const { label, url, filters } = body[0];
+
+ expect(status).to.equal(200);
+ expect({ label, url, filters }).to.eql({
+ label: 'with filters',
+ url: 'https://elastic.co',
+ filters: [
+ { key: 'service.name', value: 'baz' },
+ { key: 'transaction.type', value: 'qux' },
+ ],
+ });
+ });
+ it('updates a custom link', async () => {
+ let { status, body } = await searchCustomLinks({
+ 'service.name': 'baz',
+ 'transaction.type': 'qux',
+ });
+ expect(status).to.equal(200);
+ await updateCustomLink(body[0].id, {
+ label: 'foo',
+ url: 'https://elastic.co?service.name={{service.name}}',
+ filters: [
+ { key: 'service.name', value: 'quz' },
+ { key: 'transaction.name', value: 'bar' },
+ ],
+ });
+ ({ status, body } = await searchCustomLinks({
+ 'service.name': 'quz',
+ 'transaction.name': 'bar',
+ }));
+ const { label, url, filters } = body[0];
+ expect(status).to.equal(200);
+ expect({ label, url, filters }).to.eql({
+ label: 'foo',
+ url: 'https://elastic.co?service.name={{service.name}}',
+ filters: [
+ { key: 'service.name', value: 'quz' },
+ { key: 'transaction.name', value: 'bar' },
+ ],
+ });
+ });
+ it('deletes a custom link', async () => {
+ let { status, body } = await searchCustomLinks({
+ 'service.name': 'quz',
+ 'transaction.name': 'bar',
+ });
+ expect(status).to.equal(200);
+ await deleteCustomLink(body[0].id);
+ ({ status, body } = await searchCustomLinks({
+ 'service.name': 'quz',
+ 'transaction.name': 'bar',
+ }));
+ expect(status).to.equal(200);
+ expect(body).to.eql([]);
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts
index 8ce55b8fb1d5f..9f76941935bb7 100644
--- a/x-pack/test/api_integration/apis/apm/feature_controls.ts
+++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts
@@ -149,12 +149,27 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
log.error(JSON.stringify(res, null, 2));
},
},
+ {
+ req: {
+ url: `/api/apm/settings/custom_links`,
+ },
+ expectForbidden: expect404,
+ expectResponse: expect200,
+ },
+ {
+ req: {
+ url: `/api/apm/settings/custom_links/transaction`,
+ },
+ expectForbidden: expect404,
+ expectResponse: expect200,
+ },
];
const elasticsearchPrivileges = {
indices: [
{ names: ['apm-*'], privileges: ['read', 'view_index_metadata'] },
{ names: ['.apm-agent-configuration'], privileges: ['read', 'write', 'view_index_metadata'] },
+ { names: ['.apm-custom-link'], privileges: ['read', 'write', 'view_index_metadata'] },
],
};
diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts
index 6f41f4abfecc3..4a4265cfd0739 100644
--- a/x-pack/test/api_integration/apis/apm/index.ts
+++ b/x-pack/test/api_integration/apis/apm/index.ts
@@ -10,5 +10,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
describe('APM specs', () => {
loadTestFile(require.resolve('./feature_controls'));
loadTestFile(require.resolve('./agent_configuration'));
+ loadTestFile(require.resolve('./custom_link'));
});
}
diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js
index 0a87dcb4b5bb0..10c06adea8564 100644
--- a/x-pack/test/api_integration/apis/index.js
+++ b/x-pack/test/api_integration/apis/index.js
@@ -27,7 +27,6 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./siem'));
loadTestFile(require.resolve('./short_urls'));
loadTestFile(require.resolve('./lens'));
- loadTestFile(require.resolve('./fleet'));
loadTestFile(require.resolve('./ingest'));
loadTestFile(require.resolve('./endpoint'));
loadTestFile(require.resolve('./ml'));
diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts
new file mode 100644
index 0000000000000..c1be2e98b3b99
--- /dev/null
+++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { resolve } from 'path';
+import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
+import { services } from './services';
+
+export default async function({ readConfigFile }: FtrConfigProviderContext) {
+ const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
+
+ return {
+ testFiles: [require.resolve('./tests')],
+ servers: xPackAPITestsConfig.get('servers'),
+ services,
+ junit: {
+ reportName: 'X-Pack Encrypted Saved Objects API Integration Tests',
+ },
+ esTestCluster: xPackAPITestsConfig.get('esTestCluster'),
+ kbnTestServer: {
+ ...xPackAPITestsConfig.get('kbnTestServer'),
+ serverArgs: [
+ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
+ `--plugin-path=${resolve(__dirname, './fixtures/api_consumer_plugin')}`,
+ ],
+ },
+ };
+}
diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json
new file mode 100644
index 0000000000000..92449d0136ce5
--- /dev/null
+++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "eso",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "requiredPlugins": ["encryptedSavedObjects", "spaces"],
+ "server": true,
+ "ui": false
+}
diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts
new file mode 100644
index 0000000000000..170b7e0c6d09d
--- /dev/null
+++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { CoreSetup, PluginInitializer } from '../../../../../../src/core/server';
+import { deepFreeze } from '../../../../../../src/core/utils';
+import {
+ EncryptedSavedObjectsPluginSetup,
+ EncryptedSavedObjectsPluginStart,
+} from '../../../../../plugins/encrypted_saved_objects/server';
+import { SpacesPluginSetup } from '../../../../../plugins/spaces/server';
+
+const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
+
+interface PluginsSetup {
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
+ spaces: SpacesPluginSetup;
+}
+
+interface PluginsStart {
+ encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
+ spaces: never;
+}
+
+export const plugin: PluginInitializer = () => ({
+ setup(core: CoreSetup, deps) {
+ core.savedObjects.registerType({
+ name: SAVED_OBJECT_WITH_SECRET_TYPE,
+ hidden: false,
+ namespaceAgnostic: false,
+ mappings: deepFreeze({
+ properties: {
+ publicProperty: { type: 'keyword' },
+ publicPropertyExcludedFromAAD: { type: 'keyword' },
+ privateProperty: { type: 'binary' },
+ },
+ }),
+ });
+
+ deps.encryptedSavedObjects.registerType({
+ type: SAVED_OBJECT_WITH_SECRET_TYPE,
+ attributesToEncrypt: new Set(['privateProperty']),
+ attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']),
+ });
+
+ core.http.createRouter().get(
+ {
+ path: '/api/saved_objects/get-decrypted-as-internal-user/{id}',
+ validate: { params: value => ({ value }) },
+ },
+ async (context, request, response) => {
+ const [, { encryptedSavedObjects }] = await core.getStartServices();
+ const spaceId = deps.spaces.spacesService.getSpaceId(request);
+ const namespace = deps.spaces.spacesService.spaceIdToNamespace(spaceId);
+
+ try {
+ return response.ok({
+ body: await encryptedSavedObjects.getDecryptedAsInternalUser(
+ SAVED_OBJECT_WITH_SECRET_TYPE,
+ request.params.id,
+ { namespace }
+ ),
+ });
+ } catch (err) {
+ if (encryptedSavedObjects.isEncryptionError(err)) {
+ return response.badRequest({ body: 'Failed to encrypt attributes' });
+ }
+
+ return response.customError({ body: err, statusCode: 500 });
+ }
+ }
+ );
+ },
+ start() {},
+ stop() {},
+});
diff --git a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts
new file mode 100644
index 0000000000000..e3add3748f56d
--- /dev/null
+++ b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.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.
+ */
+
+import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
+
+import { services } from './services';
+
+export type FtrProviderContext = GenericFtrProviderContext;
diff --git a/x-pack/test/encrypted_saved_objects_api_integration/services.ts b/x-pack/test/encrypted_saved_objects_api_integration/services.ts
new file mode 100644
index 0000000000000..b7398349cce5d
--- /dev/null
+++ b/x-pack/test/encrypted_saved_objects_api_integration/services.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 { services } from '../api_integration/services';
diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts
similarity index 99%
rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts
rename to x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts
index ab9f7d2cdd339..7fe3d28911211 100644
--- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts
+++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts
@@ -6,7 +6,7 @@
import expect from '@kbn/expect';
import { SavedObject } from 'src/core/server';
-import { FtrProviderContext } from '../../ftr_provider_context';
+import { FtrProviderContext } from '../ftr_provider_context';
export default function({ getService }: FtrProviderContext) {
const es = getService('legacyEs');
diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts
similarity index 88%
rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts
rename to x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts
index 424160e84495e..8c816a3404ddb 100644
--- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts
+++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { FtrProviderContext } from '../../ftr_provider_context';
+import { FtrProviderContext } from '../ftr_provider_context';
export default function({ loadTestFile }: FtrProviderContext) {
describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() {
diff --git a/x-pack/test/epm_api_integration/apis/file.ts b/x-pack/test/epm_api_integration/apis/file.ts
index 2989263af40a7..c67f472e8fb78 100644
--- a/x-pack/test/epm_api_integration/apis/file.ts
+++ b/x-pack/test/epm_api_integration/apis/file.ts
@@ -19,7 +19,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches a .png screenshot image', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png',
+ path: '/package/auditd/2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png',
reply: {
headers: { 'content-type': 'image/png' },
},
@@ -38,7 +38,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches an .svg icon image', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/img/icon.svg',
+ path: '/package/auditd/2.0.4/img/icon.svg',
reply: {
headers: { 'content-type': 'image/svg' },
},
@@ -54,7 +54,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches an auditbeat .conf rule file', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf',
+ path: '/package/auditd/2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf',
});
const supertest = getService('supertest');
@@ -70,7 +70,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches an auditbeat .yml config file', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/auditbeat/config/config.yml',
+ path: '/package/auditd/2.0.4/auditbeat/config/config.yml',
reply: {
headers: { 'content-type': 'text/yaml; charset=UTF-8' },
},
@@ -88,7 +88,7 @@ export default function({ getService }: FtrProviderContext) {
server.on({
method: 'GET',
path:
- '/package/auditd-2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json',
+ '/package/auditd/2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json',
});
const supertest = getService('supertest');
@@ -105,7 +105,7 @@ export default function({ getService }: FtrProviderContext) {
server.on({
method: 'GET',
path:
- '/package/auditd-2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json',
+ '/package/auditd/2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json',
});
const supertest = getService('supertest');
@@ -121,7 +121,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches an .json index pattern file', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/kibana/index-pattern/auditbeat-*.json',
+ path: '/package/auditd/2.0.4/kibana/index-pattern/auditbeat-*.json',
});
const supertest = getService('supertest');
@@ -135,7 +135,7 @@ export default function({ getService }: FtrProviderContext) {
it('fetches a .json search file', async () => {
server.on({
method: 'GET',
- path: '/package/auditd-2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json',
+ path: '/package/auditd/2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json',
});
const supertest = getService('supertest');
diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts
index 1f22ca59ab2d4..7e15ff436d12c 100644
--- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts
+++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts
@@ -138,7 +138,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
});
});
- describe('no advanced_settings privileges', function() {
+ // FLAKY: https://github.com/elastic/kibana/issues/57377
+ describe.skip('no advanced_settings privileges', function() {
this.tags(['skipCoverage']);
before(async () => {
await security.role.create('no_advanced_settings_privileges_role', {
diff --git a/x-pack/test/login_selector_api_integration/config.ts b/x-pack/test/login_selector_api_integration/config.ts
index 6ca9d19b74c17..d8e42b4583bed 100644
--- a/x-pack/test/login_selector_api_integration/config.ts
+++ b/x-pack/test/login_selector_api_integration/config.ts
@@ -130,11 +130,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) {
saml2: { order: 5, realm: 'saml2', maxRedirectURLSize: '100b' },
},
})}`,
- '--server.xsrf.whitelist',
- JSON.stringify([
- '/api/oidc_provider/token_endpoint',
- '/api/oidc_provider/userinfo_endpoint',
- ]),
],
},
};
diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts
index 557dea4d51b0e..9ef00320f0e60 100644
--- a/x-pack/test/oidc_api_integration/config.ts
+++ b/x-pack/test/oidc_api_integration/config.ts
@@ -51,12 +51,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) {
`--plugin-path=${plugin}`,
'--xpack.security.authc.providers=["oidc"]',
'--xpack.security.authc.oidc.realm="oidc1"',
- '--server.xsrf.whitelist',
- JSON.stringify([
- '/api/security/oidc/initiate_login',
- '/api/oidc_provider/token_endpoint',
- '/api/oidc_provider/userinfo_endpoint',
- ]),
],
},
};
diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js
deleted file mode 100644
index 3023479f7be9d..0000000000000
--- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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 Joi from 'joi';
-import { createTokens } from '../oidc_tools';
-
-export function initRoutes(server) {
- let nonce = '';
-
- server.route({
- path: '/api/oidc_provider/setup',
- method: 'POST',
- config: {
- auth: false,
- validate: {
- payload: Joi.object({
- nonce: Joi.string().required(),
- }),
- },
- },
- handler: request => {
- nonce = request.payload.nonce;
- return {};
- },
- });
-
- server.route({
- path: '/api/oidc_provider/token_endpoint',
- method: 'POST',
- // Token endpoint needs authentication (with the client credentials) but we don't attempt to
- // validate this OIDC behavior here
- config: {
- auth: false,
- validate: {
- payload: Joi.object({
- grant_type: Joi.string().optional(),
- code: Joi.string().optional(),
- redirect_uri: Joi.string().optional(),
- }),
- },
- },
- async handler(request) {
- const userId = request.payload.code.substring(4);
- const { accessToken, idToken } = createTokens(userId, nonce);
- try {
- const userId = request.payload.code.substring(4);
- return {
- access_token: accessToken,
- token_type: 'Bearer',
- refresh_token: `valid-refresh-token${userId}`,
- expires_in: 3600,
- id_token: idToken,
- };
- } catch (err) {
- return err;
- }
- },
- });
-
- server.route({
- path: '/api/oidc_provider/userinfo_endpoint',
- method: 'GET',
- config: {
- auth: false,
- },
- handler: request => {
- const accessToken = request.headers.authorization.substring(7);
- if (accessToken === 'valid-access-token1') {
- return {
- sub: 'user1',
- name: 'Tony Stark',
- given_name: 'Tony',
- family_name: 'Stark',
- preferred_username: 'ironman',
- email: 'ironman@avengers.com',
- };
- }
- if (accessToken === 'valid-access-token2') {
- return {
- sub: 'user2',
- name: 'Peter Parker',
- given_name: 'Peter',
- family_name: 'Parker',
- preferred_username: 'spiderman',
- email: 'spiderman@avengers.com',
- };
- }
- if (accessToken === 'valid-access-token3') {
- return {
- sub: 'user3',
- name: 'Bruce Banner',
- given_name: 'Bruce',
- family_name: 'Banner',
- preferred_username: 'hulk',
- email: 'hulk@avengers.com',
- };
- }
- return {};
- },
- });
-}
diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json
new file mode 100644
index 0000000000000..faaa0b9165828
--- /dev/null
+++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json
@@ -0,0 +1,7 @@
+{
+ "id": "oidc_provider_plugin",
+ "version": "8.0.0",
+ "kibanaVersion": "kibana",
+ "server": true,
+ "ui": false
+}
diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json
deleted file mode 100644
index 358c6e2020afe..0000000000000
--- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "oidc_provider_plugin",
- "version": "1.0.0",
- "kibana": {
- "version": "kibana",
- "templateVersion": "1.0.0"
- },
- "license": "Apache-2.0",
- "dependencies": {
- "joi": "^13.5.2",
- "jsonwebtoken": "^8.3.0"
- }
-}
diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts
similarity index 55%
rename from x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js
rename to x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts
index 17d45527397b8..456abecd201be 100644
--- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js
+++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts
@@ -4,16 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { PluginInitializer } from '../../../../../../src/core/server';
import { initRoutes } from './init_routes';
-export default function(kibana) {
- return new kibana.Plugin({
- name: 'oidcProvider',
- id: 'oidcProvider',
- require: ['elasticsearch'],
-
- init(server) {
- initRoutes(server);
- },
- });
-}
+export const plugin: PluginInitializer = () => ({
+ setup: core => initRoutes(core.http.createRouter()),
+ start: () => {},
+ stop: () => {},
+});
diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts
new file mode 100644
index 0000000000000..6d3248f4377b1
--- /dev/null
+++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 { IRouter } from '../../../../../../src/core/server';
+import { createTokens } from '../../oidc_tools';
+
+export function initRoutes(router: IRouter) {
+ let nonce = '';
+
+ router.post(
+ {
+ path: '/api/oidc_provider/setup',
+ validate: { body: value => ({ value }) },
+ options: { authRequired: false },
+ },
+ (context, request, response) => {
+ nonce = request.body.nonce;
+ return response.ok({ body: {} });
+ }
+ );
+
+ router.post(
+ {
+ path: '/api/oidc_provider/token_endpoint',
+ validate: { body: value => ({ value }) },
+ // Token endpoint needs authentication (with the client credentials) but we don't attempt to
+ // validate this OIDC behavior here
+ options: { authRequired: false, xsrfRequired: false },
+ },
+ (context, request, response) => {
+ const userId = request.body.code.substring(4);
+ const { accessToken, idToken } = createTokens(userId, nonce);
+ return response.ok({
+ body: {
+ access_token: accessToken,
+ token_type: 'Bearer',
+ refresh_token: `valid-refresh-token${userId}`,
+ expires_in: 3600,
+ id_token: idToken,
+ },
+ });
+ }
+ );
+
+ router.get(
+ {
+ path: '/api/oidc_provider/userinfo_endpoint',
+ validate: false,
+ options: { authRequired: false },
+ },
+ (context, request, response) => {
+ const accessToken = (request.headers.authorization as string).substring(7);
+ if (accessToken === 'valid-access-token1') {
+ return response.ok({
+ body: {
+ sub: 'user1',
+ name: 'Tony Stark',
+ given_name: 'Tony',
+ family_name: 'Stark',
+ preferred_username: 'ironman',
+ email: 'ironman@avengers.com',
+ },
+ });
+ }
+
+ if (accessToken === 'valid-access-token2') {
+ return response.ok({
+ body: {
+ sub: 'user2',
+ name: 'Peter Parker',
+ given_name: 'Peter',
+ family_name: 'Parker',
+ preferred_username: 'spiderman',
+ email: 'spiderman@avengers.com',
+ },
+ });
+ }
+
+ if (accessToken === 'valid-access-token3') {
+ return response.ok({
+ body: {
+ sub: 'user3',
+ name: 'Bruce Banner',
+ given_name: 'Bruce',
+ family_name: 'Banner',
+ preferred_username: 'hulk',
+ email: 'hulk@avengers.com',
+ },
+ });
+ }
+
+ return response.ok({ body: {} });
+ }
+ );
+}
diff --git a/x-pack/test/plugin_api_integration/config.js b/x-pack/test/plugin_api_integration/config.js
index 830933278f2bc..83e8b1f84a9e0 100644
--- a/x-pack/test/plugin_api_integration/config.js
+++ b/x-pack/test/plugin_api_integration/config.js
@@ -18,10 +18,7 @@ export default async function({ readConfigFile }) {
);
return {
- testFiles: [
- require.resolve('./test_suites/task_manager'),
- require.resolve('./test_suites/encrypted_saved_objects'),
- ],
+ testFiles: [require.resolve('./test_suites/task_manager')],
services,
servers: integrationConfig.get('servers'),
esTestCluster: integrationConfig.get('esTestCluster'),
diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts
deleted file mode 100644
index e61b8f24a1f69..0000000000000
--- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 { Request } from 'hapi';
-import { boomify, badRequest } from 'boom';
-import { Legacy } from 'kibana';
-import {
- EncryptedSavedObjectsPluginSetup,
- EncryptedSavedObjectsPluginStart,
-} from '../../../../plugins/encrypted_saved_objects/server';
-
-const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret';
-
-// eslint-disable-next-line import/no-default-export
-export default function esoPlugin(kibana: any) {
- return new kibana.Plugin({
- id: 'eso',
- require: ['encryptedSavedObjects'],
- uiExports: { mappings: require('./mappings.json') },
- init(server: Legacy.Server) {
- server.route({
- method: 'GET',
- path: '/api/saved_objects/get-decrypted-as-internal-user/{id}',
- async handler(request: Request) {
- const encryptedSavedObjectsStart = server.newPlatform.start.plugins
- .encryptedSavedObjects as EncryptedSavedObjectsPluginStart;
- const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request);
- try {
- return await encryptedSavedObjectsStart.getDecryptedAsInternalUser(
- SAVED_OBJECT_WITH_SECRET_TYPE,
- request.params.id,
- { namespace: namespace === 'default' ? undefined : namespace }
- );
- } catch (err) {
- if (encryptedSavedObjectsStart.isEncryptionError(err)) {
- return badRequest('Failed to encrypt attributes');
- }
-
- return boomify(err);
- }
- },
- });
-
- (server.newPlatform.setup.plugins
- .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({
- type: SAVED_OBJECT_WITH_SECRET_TYPE,
- attributesToEncrypt: new Set(['privateProperty']),
- attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']),
- });
- },
- });
-}
diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json
deleted file mode 100644
index b727850793bbe..0000000000000
--- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "saved-object-with-secret": {
- "properties": {
- "publicProperty": {
- "type": "keyword"
- },
- "publicPropertyExcludedFromAAD": {
- "type": "keyword"
- },
- "privateProperty": {
- "type": "binary"
- }
- }
- }
-}
diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json
deleted file mode 100644
index 723904757ae8a..0000000000000
--- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "eso",
- "version": "kibana"
-}
\ No newline at end of file
diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts
index 0580c28555d16..a92f11363b0fc 100644
--- a/x-pack/test/saml_api_integration/config.ts
+++ b/x-pack/test/saml_api_integration/config.ts
@@ -50,7 +50,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) {
serverArgs: [
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
'--optimize.enabled=false',
- '--server.xsrf.whitelist=["/api/security/saml/callback"]',
`--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`,
'--xpack.security.authc.saml.realm=saml1',
'--xpack.security.authc.saml.maxRedirectURLSize=100b',