;
+
+export interface EnhancementRegistryDefinition
+ extends PersistableStateDefinition
{
+ id: string;
+}
+
+export interface EnhancementRegistryItem
+ extends PersistableState
{
+ id: string;
+}
export type EmbeddableFactoryProvider = <
I extends EmbeddableInput = EmbeddableInput,
diff --git a/src/plugins/embeddable/server/index.ts b/src/plugins/embeddable/server/index.ts
new file mode 100644
index 00000000000000..1138478bff4b76
--- /dev/null
+++ b/src/plugins/embeddable/server/index.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { EmbeddableServerPlugin, EmbeddableSetup } from './plugin';
+
+export { EmbeddableSetup };
+
+export { EnhancementRegistryDefinition, EmbeddableRegistryDefinition } from './types';
+
+export const plugin = () => new EmbeddableServerPlugin();
diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts
new file mode 100644
index 00000000000000..f79c4b76201107
--- /dev/null
+++ b/src/plugins/embeddable/server/plugin.ts
@@ -0,0 +1,186 @@
+/*
+ * 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, CoreStart, Plugin, SavedObjectReference } from 'kibana/server';
+import { identity } from 'lodash';
+import {
+ EmbeddableFactoryRegistry,
+ EnhancementsRegistry,
+ EnhancementRegistryDefinition,
+ EnhancementRegistryItem,
+ EmbeddableRegistryDefinition,
+} from './types';
+import {
+ extractBaseEmbeddableInput,
+ injectBaseEmbeddableInput,
+ telemetryBaseEmbeddableInput,
+} from '../common/lib/migrate_base_input';
+import { SerializableState } from '../../kibana_utils/common';
+import { EmbeddableInput } from '../common/types';
+
+export interface EmbeddableSetup {
+ registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void;
+ registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void;
+}
+
+export class EmbeddableServerPlugin implements Plugin {
+ private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map();
+ private readonly enhancements: EnhancementsRegistry = new Map();
+
+ public setup(core: CoreSetup) {
+ return {
+ registerEmbeddableFactory: this.registerEmbeddableFactory,
+ registerEnhancement: this.registerEnhancement,
+ };
+ }
+
+ public start(core: CoreStart) {
+ return {
+ telemetry: this.telemetry,
+ extract: this.extract,
+ inject: this.inject,
+ };
+ }
+
+ public stop() {}
+
+ private telemetry = (state: EmbeddableInput, telemetryData: Record = {}) => {
+ const enhancements: Record = state.enhancements || {};
+ const factory = this.getEmbeddableFactory(state.id);
+
+ let telemetry = telemetryBaseEmbeddableInput(state, telemetryData);
+ if (factory) {
+ telemetry = factory.telemetry(state, telemetry);
+ }
+ Object.keys(enhancements).map((key) => {
+ if (!enhancements[key]) return;
+ telemetry = this.getEnhancement(key).telemetry(enhancements[key], telemetry);
+ });
+
+ return telemetry;
+ };
+
+ private extract = (state: EmbeddableInput) => {
+ const enhancements = state.enhancements || {};
+ const factory = this.getEmbeddableFactory(state.id);
+
+ const baseResponse = extractBaseEmbeddableInput(state);
+ let updatedInput = baseResponse.state;
+ const refs = baseResponse.references;
+
+ if (factory) {
+ const factoryResponse = factory.extract(state);
+ updatedInput = factoryResponse.state;
+ refs.push(...factoryResponse.references);
+ }
+
+ updatedInput.enhancements = {};
+ Object.keys(enhancements).forEach((key) => {
+ if (!enhancements[key]) return;
+ const enhancementResult = this.getEnhancement(key).extract(
+ enhancements[key] as SerializableState
+ );
+ refs.push(...enhancementResult.references);
+ updatedInput.enhancements![key] = enhancementResult.state;
+ });
+
+ return {
+ state: updatedInput,
+ references: refs,
+ };
+ };
+
+ private inject = (state: EmbeddableInput, references: SavedObjectReference[]) => {
+ const enhancements = state.enhancements || {};
+ const factory = this.getEmbeddableFactory(state.id);
+
+ let updatedInput = injectBaseEmbeddableInput(state, references);
+
+ if (factory) {
+ updatedInput = factory.inject(updatedInput, references);
+ }
+
+ updatedInput.enhancements = {};
+ Object.keys(enhancements).forEach((key) => {
+ if (!enhancements[key]) return;
+ updatedInput.enhancements![key] = this.getEnhancement(key).inject(
+ enhancements[key] as SerializableState,
+ references
+ );
+ });
+
+ return updatedInput;
+ };
+
+ private registerEnhancement = (enhancement: EnhancementRegistryDefinition) => {
+ if (this.enhancements.has(enhancement.id)) {
+ throw new Error(`enhancement with id ${enhancement.id} already exists in the registry`);
+ }
+ this.enhancements.set(enhancement.id, {
+ id: enhancement.id,
+ telemetry: enhancement.telemetry || (() => ({})),
+ inject: enhancement.inject || identity,
+ extract:
+ enhancement.extract ||
+ ((state: SerializableState) => {
+ return { state, references: [] };
+ }),
+ });
+ };
+
+ private getEnhancement = (id: string): EnhancementRegistryItem => {
+ return (
+ this.enhancements.get(id) || {
+ id: 'unknown',
+ telemetry: () => ({}),
+ inject: identity,
+ extract: (state: SerializableState) => {
+ return { state, references: [] };
+ },
+ }
+ );
+ };
+
+ private registerEmbeddableFactory = (factory: EmbeddableRegistryDefinition) => {
+ if (this.embeddableFactories.has(factory.id)) {
+ throw new Error(
+ `Embeddable factory [embeddableFactoryId = ${factory.id}] already registered in Embeddables API.`
+ );
+ }
+ this.embeddableFactories.set(factory.id, {
+ id: factory.id,
+ telemetry: factory.telemetry || (() => ({})),
+ inject: factory.inject || identity,
+ extract: factory.extract || ((state: EmbeddableInput) => ({ state, references: [] })),
+ });
+ };
+
+ private getEmbeddableFactory = (embeddableFactoryId: string) => {
+ return (
+ this.embeddableFactories.get(embeddableFactoryId) || {
+ id: 'unknown',
+ telemetry: () => ({}),
+ inject: (state: EmbeddableInput) => state,
+ extract: (state: EmbeddableInput) => {
+ return { state, references: [] };
+ },
+ }
+ );
+ };
+}
diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts
new file mode 100644
index 00000000000000..64f9325dad3cbb
--- /dev/null
+++ b/src/plugins/embeddable/server/types.ts
@@ -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 {
+ PersistableState,
+ PersistableStateDefinition,
+ SerializableState,
+} from '../../kibana_utils/common';
+import { EmbeddableInput } from '../common/types';
+
+export type EmbeddableFactoryRegistry = Map;
+export type EnhancementsRegistry = Map;
+
+export interface EnhancementRegistryDefinition
+ extends PersistableStateDefinition
{
+ id: string;
+}
+
+export interface EnhancementRegistryItem
+ extends PersistableState
{
+ id: string;
+}
+
+export interface EmbeddableRegistryDefinition
+ extends PersistableStateDefinition
{
+ id: string;
+}
+
+export interface EmbeddableRegistryItem
+ extends PersistableState
{
+ id: string;
+}
diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap
index 1b10756c2975c3..bf1e8c8f0b401a 100644
--- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap
+++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap
@@ -164,6 +164,7 @@ exports[`home directories should not render directory entry when showOnHomePage
{stackManagement ? (
-
+
`;
+
+exports[`ManageData render empty without any features 1`] = ` `;
diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx
index 5d00370caf2cce..0e86bf7dd3d84a 100644
--- a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx
+++ b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx
@@ -88,4 +88,9 @@ describe('ManageData', () => {
);
expect(component).toMatchSnapshot();
});
+
+ test('render empty without any features', () => {
+ const component = shallowWithIntl( );
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.tsx
index 0dfb4f949f0c73..85f1bc04f353b8 100644
--- a/src/plugins/home/public/application/components/manage_data/manage_data.tsx
+++ b/src/plugins/home/public/application/components/manage_data/manage_data.tsx
@@ -36,31 +36,37 @@ export const ManageData: FC = ({ addBasePath, features }) => (
<>
{features.length > 1 && }
-
-
-
-
-
-
+ {features.length > 0 && (
+
+
+
+
+
+
-
+
-
- {features.map((feature) => (
-
-
-
- ))}
-
-
+
+ {features.map((feature) => (
+
+
+
+ ))}
+
+
+ )}
>
);
diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap
index 4e8441bd64b117..ad92aac67d51b8 100644
--- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap
+++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap
@@ -3,6 +3,7 @@
exports[`SolutionPanel renders the solution panel for the given solution 1`] = `
diff --git a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx
index c2ae2f82eaa462..83572e238bffdf 100644
--- a/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx
+++ b/src/plugins/home/public/application/components/solutions_section/solution_panel.tsx
@@ -53,6 +53,7 @@ interface Props {
export const SolutionPanel: FC = ({ addBasePath, solution }) => (
{
expect(service.get()).toEqual([]);
});
});
+
+ describe('visibility filtering', () => {
+ test('retains items with no "visible" callback', () => {
+ const service = new FeatureCatalogueRegistry();
+ service.setup().register(DASHBOARD_FEATURE);
+ const capabilities = { catalogue: {} } as any;
+ service.start({ capabilities });
+ expect(service.get()).toEqual([DASHBOARD_FEATURE]);
+ });
+
+ test('retains items with a "visible" callback which returns "true"', () => {
+ const service = new FeatureCatalogueRegistry();
+ const feature = {
+ ...DASHBOARD_FEATURE,
+ visible: () => true,
+ };
+ service.setup().register(feature);
+ const capabilities = { catalogue: {} } as any;
+ service.start({ capabilities });
+ expect(service.get()).toEqual([feature]);
+ });
+
+ test('removes items with a "visible" callback which returns "false"', () => {
+ const service = new FeatureCatalogueRegistry();
+ const feature = {
+ ...DASHBOARD_FEATURE,
+ visible: () => false,
+ };
+ service.setup().register(feature);
+ const capabilities = { catalogue: {} } as any;
+ service.start({ capabilities });
+ expect(service.get()).toEqual([]);
+ });
+ });
});
describe('title sorting', () => {
diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts
index 766afb11a87c04..d965042b65cef5 100644
--- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts
+++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts
@@ -45,6 +45,8 @@ export interface FeatureCatalogueEntry {
readonly showOnHomePage: boolean;
/** An ordinal used to sort features relative to one another for display on the home page */
readonly order?: number;
+ /** Optional function to control visibility of this feature. */
+ readonly visible?: () => boolean;
}
/** @public */
@@ -103,7 +105,10 @@ export class FeatureCatalogueRegistry {
}
const capabilities = this.capabilities;
return [...this.features.values()]
- .filter((entry) => capabilities.catalogue[entry.id] !== false)
+ .filter(
+ (entry) =>
+ capabilities.catalogue[entry.id] !== false && (entry.visible ? entry.visible() : true)
+ )
.sort(compareByKey('title'));
}
diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts
index 1ec5737c5a38b1..e09290c811c7b3 100644
--- a/src/plugins/kibana_utils/common/index.ts
+++ b/src/plugins/kibana_utils/common/index.ts
@@ -29,3 +29,4 @@ export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_w
export { url } from './url';
export { now } from './now';
export { calculateObjectHash } from './calculate_object_hash';
+export * from './persistable_state';
diff --git a/src/plugins/kibana_utils/common/persistable_state/index.ts b/src/plugins/kibana_utils/common/persistable_state/index.ts
new file mode 100644
index 00000000000000..ae5e3d514554ce
--- /dev/null
+++ b/src/plugins/kibana_utils/common/persistable_state/index.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { SavedObjectReference } from '../../../../core/types';
+
+export type SerializableValue = string | number | boolean | null | undefined | SerializableState;
+export type Serializable = SerializableValue | SerializableValue[];
+
+// eslint-disable-next-line
+export type SerializableState = {
+ [key: string]: Serializable;
+};
+
+export interface PersistableState {
+ /**
+ * function to extract telemetry information
+ * @param state
+ * @param collector
+ */
+ telemetry: (state: P, collector: Record) => Record;
+ /**
+ * inject function receives state and a list of references and should return state with references injected
+ * default is identity function
+ * @param state
+ * @param references
+ */
+ inject: (state: P, references: SavedObjectReference[]) => P;
+ /**
+ * extract function receives state and should return state with references extracted and array of references
+ * default returns same state with empty reference array
+ * @param state
+ */
+ extract: (state: P) => { state: P; references: SavedObjectReference[] };
+}
+
+export type PersistableStateDefinition = Partial<
+ PersistableState
+>;
diff --git a/src/plugins/kibana_utils/public/ui/configurable.ts b/src/plugins/kibana_utils/public/ui/configurable.ts
index 3fa5cdc8b5e479..89bce5ae423ee6 100644
--- a/src/plugins/kibana_utils/public/ui/configurable.ts
+++ b/src/plugins/kibana_utils/public/ui/configurable.ts
@@ -18,11 +18,15 @@
*/
import { UiComponent } from '../../common/ui/ui_component';
+import { SerializableState } from '../../common';
/**
* Represents something that can be configured by user using UI.
*/
-export interface Configurable {
+export interface Configurable<
+ Config extends SerializableState = SerializableState,
+ Context = object
+> {
/**
* Create default config for this item, used when item is created for the first time.
*/
@@ -42,7 +46,10 @@ export interface Configurable
/**
* Props provided to `CollectConfig` component on every re-render.
*/
-export interface CollectConfigProps {
+export interface CollectConfigProps<
+ Config extends SerializableState = SerializableState,
+ Context = object
+> {
/**
* Current (latest) config of the item.
*/
diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts
index fafedf46c2bdaa..808578c470ae19 100644
--- a/src/plugins/management/public/plugin.ts
+++ b/src/plugins/management/public/plugin.ts
@@ -47,6 +47,8 @@ export class ManagementPlugin implements Plugin(() => ({}));
+ private hasAnyEnabledApps = true;
+
constructor(private initializerContext: PluginInitializerContext) {}
public setup(core: CoreSetup, { home }: ManagementSetupDependencies) {
@@ -65,6 +67,7 @@ export class ManagementPlugin implements Plugin this.hasAnyEnabledApps,
});
}
@@ -96,11 +99,11 @@ export class ManagementPlugin implements Plugin section.getAppsEnabled().length > 0);
- if (!hasAnyEnabledApps) {
+ if (!this.hasAnyEnabledApps) {
this.appUpdater.next(() => {
return {
status: AppStatus.inaccessible,
diff --git a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts
index 0cf6f3723a6397..3442f84599fb82 100644
--- a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts
+++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts
@@ -78,7 +78,6 @@ export function getTimelionRequestHandler({
filters: Filter[];
query: Query;
visParams: VisParams;
- forceFetch?: boolean;
}): Promise {
const expression = visParams.expression;
diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts
index 7be18a4774d94b..d3c6ca5d90371d 100644
--- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts
+++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts
@@ -76,7 +76,6 @@ export const getTimelionVisualizationConfig = (
query: get(input, 'query') as Query,
filters: get(input, 'filters') as Filter[],
visParams,
- forceFetch: true,
});
response.visType = TIMELION_VIS_NAME;
diff --git a/src/plugins/visualizations/public/expressions/visualization_function.ts b/src/plugins/visualizations/public/expressions/visualization_function.ts
index 68a153f4272a30..f4241808940b2f 100644
--- a/src/plugins/visualizations/public/expressions/visualization_function.ts
+++ b/src/plugins/visualizations/public/expressions/visualization_function.ts
@@ -117,7 +117,6 @@ export const visualization = (): ExpressionFunctionVisualization => ({
uiState,
inspectorAdapters,
queryFilter: getFilterManager(),
- forceFetch: true,
aggs,
});
}
diff --git a/src/plugins/visualizations/public/vis.test.ts b/src/plugins/visualizations/public/vis.test.ts
index a0da8d83bed518..c271888b7c7a4b 100644
--- a/src/plugins/visualizations/public/vis.test.ts
+++ b/src/plugins/visualizations/public/vis.test.ts
@@ -35,7 +35,7 @@ jest.mock('./services', () => {
// eslint-disable-next-line
const { BaseVisType } = require('./vis_types/base_vis_type');
// eslint-disable-next-line
- const { SearchSource } = require('../../data/public/search/search_source');
+ const { SearchSource } = require('../../data/common/search/search_source');
// eslint-disable-next-line
const fixturesStubbedLogstashIndexPatternProvider = require('../../../fixtures/stubbed_logstash_index_pattern');
const visType = new BaseVisType({
diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts
index e8f8982d7163cc..c12c633926c1c2 100644
--- a/test/functional/page_objects/home_page.ts
+++ b/test/functional/page_objects/home_page.ts
@@ -43,6 +43,14 @@ export function HomePageProvider({ getService, getPageObjects }: FtrProviderCont
return !(await testSubjects.exists(`addSampleDataSet${id}`));
}
+ async getVisibileSolutions() {
+ const solutionPanels = await testSubjects.findAll('~homSolutionPanel', 2000);
+ const panelAttributes = await Promise.all(
+ solutionPanels.map((panel) => panel.getAttribute('data-test-subj'))
+ );
+ return panelAttributes.map((attributeValue) => attributeValue.split('homSolutionPanel_')[1]);
+ }
+
async addSampleDataSet(id: string) {
const isInstalled = await this.isSampleDataSetInstalled(id);
if (!isInstalled) {
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
index ace6a48ed8ff58..87a1bc20920a46 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
@@ -12,7 +12,7 @@
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
- "@elastic/eui": "28.4.0",
+ "@elastic/eui": "29.0.0",
"@kbn/plugin-helpers": "1.0.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
index d98fa468bd6d19..8bbf6274bd15f9 100644
--- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
+++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
@@ -12,7 +12,7 @@
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
- "@elastic/eui": "28.4.0",
+ "@elastic/eui": "29.0.0",
"react": "^16.12.0",
"typescript": "4.0.2"
}
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 3ac03b444deafb..c0d9a03d02c32a 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -12,7 +12,7 @@
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
- "@elastic/eui": "28.4.0",
+ "@elastic/eui": "29.0.0",
"@kbn/plugin-helpers": "1.0.0",
"react": "^16.12.0",
"typescript": "4.0.2"
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index bdd0fbea35fa83..a700781438706a 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -49,7 +49,7 @@
"xpack.server": "legacy/server",
"xpack.securitySolution": "plugins/security_solution",
"xpack.snapshotRestore": "plugins/snapshot_restore",
- "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"],
+ "xpack.spaces": "plugins/spaces",
"xpack.taskManager": "legacy/plugins/task_manager",
"xpack.transform": "plugins/transform",
"xpack.triggersActionsUI": "plugins/triggers_actions_ui",
diff --git a/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json b/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json
new file mode 100644
index 00000000000000..2aab6c2d9093b6
--- /dev/null
+++ b/x-pack/examples/ui_actions_enhanced_examples/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "@typescript-eslint/consistent-type-definitions": 0
+ }
+}
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx
index fd782f5468c852..cac5f0b29dc6ec 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx
@@ -17,9 +17,9 @@ import {
export type ActionContext = ChartActionContext;
-export interface Config {
+export type Config = {
name: string;
-}
+};
const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN';
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx
index 7394690a61eae8..fa2f0825f93357 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_only_range_select_drilldown/index.tsx
@@ -13,9 +13,9 @@ import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/publ
import { SELECT_RANGE_TRIGGER } from '../../../../../src/plugins/ui_actions/public';
import { BaseActionFactoryContext } from '../../../../plugins/ui_actions_enhanced/public/dynamic_actions';
-export interface Config {
+export type Config = {
name: string;
-}
+};
const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT =
'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT';
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts
index a10e8ad707e972..692de571e8a00c 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts
@@ -9,7 +9,7 @@ import { ApplyGlobalFilterActionContext } from '../../../../../src/plugins/data/
export type ActionContext = ApplyGlobalFilterActionContext;
-export interface Config {
+export type Config = {
/**
* Whether to use a user selected index pattern, stored in `indexPatternId` field.
*/
@@ -30,6 +30,6 @@ export interface Config {
* Whether to carry over source dashboard time range.
*/
carryTimeRange: boolean;
-}
+};
export type CollectConfigProps = CollectConfigPropsBase;
diff --git a/x-pack/index.js b/x-pack/index.js
index 745b4bd72dde8e..cb68004c26d656 100644
--- a/x-pack/index.js
+++ b/x-pack/index.js
@@ -5,8 +5,7 @@
*/
import { xpackMain } from './legacy/plugins/xpack_main';
-import { spaces } from './legacy/plugins/spaces';
module.exports = function (kibana) {
- return [xpackMain(kibana), spaces(kibana)];
+ return [xpackMain(kibana)];
};
diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts
deleted file mode 100644
index aec06a4596203a..00000000000000
--- a/x-pack/legacy/plugins/spaces/index.ts
+++ /dev/null
@@ -1,83 +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 { resolve } from 'path';
-import KbnServer, { Server } from 'src/legacy/server/kbn_server';
-import { Legacy } from 'kibana';
-import { KibanaRequest } from '../../../../src/core/server';
-import { SpacesPluginSetup } from '../../../plugins/spaces/server';
-import { wrapError } from './server/lib/errors';
-
-export const spaces = (kibana: Record) =>
- new kibana.Plugin({
- id: 'spaces',
- configPrefix: 'xpack.spaces',
- publicDir: resolve(__dirname, 'public'),
- require: ['xpack_main'],
- config(Joi: any) {
- return Joi.object({
- enabled: Joi.boolean().default(true),
- })
- .unknown()
- .default();
- },
- uiExports: {
- injectDefaultVars(server: Server) {
- return {
- serverBasePath: server.config().get('server.basePath'),
- activeSpace: null,
- };
- },
- async replaceInjectedVars(
- vars: Record,
- request: Legacy.Request,
- server: Server
- ) {
- // NOTICE: use of `activeSpace` is deprecated and will not be made available in the New Platform.
- // Known usages:
- // - x-pack/plugins/infra/public/utils/use_kibana_space_id.ts
- const spacesPlugin = server.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
- if (!spacesPlugin) {
- throw new Error('New Platform XPack Spaces plugin is not available.');
- }
- const kibanaRequest = KibanaRequest.from(request);
- const spaceId = spacesPlugin.spacesService.getSpaceId(kibanaRequest);
- const spacesClient = await spacesPlugin.spacesService.scopedClient(kibanaRequest);
- try {
- vars.activeSpace = {
- valid: true,
- space: await spacesClient.get(spaceId),
- };
- } catch (e) {
- vars.activeSpace = {
- valid: false,
- error: wrapError(e).output.payload,
- };
- }
-
- return vars;
- },
- },
-
- async init(server: Server) {
- const kbnServer = (server as unknown) as KbnServer;
-
- const spacesPlugin = kbnServer.newPlatform.setup.plugins.spaces as SpacesPluginSetup;
- if (!spacesPlugin) {
- throw new Error('New Platform XPack Spaces plugin is not available.');
- }
-
- server.expose('getSpaceId', (request: Legacy.Request) =>
- spacesPlugin.spacesService.getSpaceId(request)
- );
- server.expose('getActiveSpace', (request: Legacy.Request) =>
- spacesPlugin.spacesService.getActiveSpace(request)
- );
- server.expose('spaceIdToNamespace', spacesPlugin.spacesService.spaceIdToNamespace);
- server.expose('namespaceToSpaceId', spacesPlugin.spacesService.namespaceToSpaceId);
- server.expose('getBasePath', spacesPlugin.spacesService.getBasePath);
- },
- });
diff --git a/x-pack/package.json b/x-pack/package.json
index 9a1d424da4a1dc..0560b1bebe42b8 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -107,7 +107,7 @@
"@types/papaparse": "^5.0.3",
"@types/pngjs": "^3.3.2",
"@types/pretty-ms": "^5.0.0",
- "@types/prop-types": "^15.5.3",
+ "@types/prop-types": "^15.7.3",
"@types/proper-lockfile": "^3.0.1",
"@types/puppeteer": "^1.20.1",
"@types/react": "^16.9.36",
@@ -275,7 +275,7 @@
"@babel/runtime": "^7.11.2",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
- "@elastic/eui": "28.4.0",
+ "@elastic/eui": "29.0.0",
"@elastic/filesaver": "1.1.2",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.0",
@@ -354,7 +354,7 @@
"papaparse": "^5.2.0",
"pdfmake": "^0.1.65",
"pngjs": "3.4.0",
- "prop-types": "^15.6.0",
+ "prop-types": "^15.7.2",
"proper-lockfile": "^3.2.0",
"puid": "1.0.7",
"puppeteer-core": "^1.19.0",
diff --git a/x-pack/plugins/actions/server/feature.ts b/x-pack/plugins/actions/server/feature.ts
index 321509a7b9de6b..abe5921fda7f1a 100644
--- a/x-pack/plugins/actions/server/feature.ts
+++ b/x-pack/plugins/actions/server/feature.ts
@@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from './saved_objects';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export const ACTIONS_FEATURE = {
id: 'actions',
@@ -14,6 +15,7 @@ export const ACTIONS_FEATURE = {
}),
icon: 'bell',
navLinkId: 'actions',
+ category: DEFAULT_APP_CATEGORIES.management,
app: [],
management: {
insightsAndAlerting: ['triggersActions'],
diff --git a/x-pack/plugins/alerting_builtins/server/feature.ts b/x-pack/plugins/alerting_builtins/server/feature.ts
index 316bae98bf8c1e..a7c8b940fbf06d 100644
--- a/x-pack/plugins/alerting_builtins/server/feature.ts
+++ b/x-pack/plugins/alerting_builtins/server/feature.ts
@@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type';
import { BUILT_IN_ALERTS_FEATURE_ID } from '../common';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export const BUILT_IN_ALERTS_FEATURE = {
id: BUILT_IN_ALERTS_FEATURE_ID,
@@ -15,6 +16,7 @@ export const BUILT_IN_ALERTS_FEATURE = {
}),
icon: 'bell',
app: [],
+ category: DEFAULT_APP_CATEGORIES.management,
management: {
insightsAndAlerting: ['triggersActions'],
},
diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
index 9515987af8dd97..b3c7ada26c4569 100644
--- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
+++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts
@@ -44,6 +44,7 @@ function mockFeature(appName: string, typeName?: string) {
id: appName,
name: appName,
app: [],
+ category: { id: 'foo', label: 'foo' },
...(typeName
? {
alerting: [typeName],
@@ -87,6 +88,7 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) {
id: appName,
name: appName,
app: [],
+ category: { id: 'foo', label: 'foo' },
...(typeName
? {
alerting: [typeName],
diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts
index 026aa0c5238dc5..b13a1c62f66022 100644
--- a/x-pack/plugins/alerts/server/plugin.test.ts
+++ b/x-pack/plugins/alerts/server/plugin.test.ts
@@ -164,6 +164,7 @@ function mockFeatures() {
id: 'appName',
name: 'appName',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
index b027609fd3a7f3..0135f0b3695377 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/index.tsx
@@ -28,7 +28,7 @@ export function CoreVitals({ data, loading }: Props) {
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
index bb5d37a10fb338..94c3acfaa97274 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx
@@ -23,7 +23,7 @@ export interface UXMetrics {
cls: string;
fid: string;
lcp: string;
- tbt: string;
+ tbt: number;
fcp: number;
lcpRanks: number[];
fidRanks: number[];
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx
index 790be81bb65c08..388a8824bc73d1 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__tests__/EmbeddedMap.test.tsx
@@ -8,7 +8,7 @@ import { render } from 'enzyme';
import React from 'react';
import { EmbeddedMap } from '../EmbeddedMap';
-import { KibanaContextProvider } from '../../../../../../../security_solution/public/common/lib/kibana';
+import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public';
import { embeddablePluginMock } from '../../../../../../../../../src/plugins/embeddable/public/mocks';
describe('Embedded Map', () => {
diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts
index 1cda70a140c673..14d8e2c3a4d506 100644
--- a/x-pack/plugins/apm/server/feature.ts
+++ b/x-pack/plugins/apm/server/feature.ts
@@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { LicenseType } from '../../licensing/common/types';
import { AlertType } from '../common/alert_types';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import {
LicensingPluginSetup,
LicensingRequestHandlerContext,
@@ -15,9 +16,10 @@ import {
export const APM_FEATURE = {
id: 'apm',
name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', {
- defaultMessage: 'APM',
+ defaultMessage: 'APM and Client Side Monitoring',
}),
order: 900,
+ category: DEFAULT_APP_CATEGORIES.observability,
icon: 'apmApp',
navLinkId: 'apm',
app: ['apm', 'csm', 'kibana'],
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
index 4fcfb53a058873..2ff0173b9ac12a 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts
@@ -127,7 +127,7 @@ export async function getWebCoreVitals({
cls: String(cls?.values['50.0']?.toFixed(2) || 0),
fid: ((fid?.values['50.0'] || 0) / 1000).toFixed(2),
lcp: ((lcp?.values['50.0'] || 0) / 1000).toFixed(2),
- tbt: ((tbt?.values['50.0'] || 0) / 1000).toFixed(2),
+ tbt: tbt?.values['50.0'] || 0,
fcp: fcp?.values['50.0'] || 0,
lcpRanks: getRanksPercentages(lcpRanks?.values ?? defaultRanks),
diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts
index 9a41a00883c139..ac5392c9d3dee2 100644
--- a/x-pack/plugins/canvas/server/plugin.ts
+++ b/x-pack/plugins/canvas/server/plugin.ts
@@ -10,6 +10,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
import { BfetchServerSetup } from 'src/plugins/bfetch/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HomeServerPluginSetup } from 'src/plugins/home/server';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { initRoutes } from './routes';
import { registerCanvasUsageCollector } from './collectors';
@@ -40,7 +41,8 @@ export class CanvasPlugin implements Plugin {
plugins.features.registerKibanaFeature({
id: 'canvas',
name: 'Canvas',
- order: 400,
+ order: 300,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'canvasApp',
navLinkId: 'canvas',
app: ['canvas', 'kibana'],
diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts
index c21109f8a596ad..330a501a78d391 100644
--- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts
+++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts
@@ -7,10 +7,11 @@
import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public';
import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public';
-export interface Config {
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type Config = {
dashboardId?: string;
useCurrentFilters: boolean;
useCurrentDateRange: boolean;
-}
+};
export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext;
diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts
index 8881b2063c8db3..e0960b83b23f91 100644
--- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts
@@ -5,6 +5,7 @@
*/
import {
+ DynamicActionsState,
UiActionsEnhancedAbstractActionStorage as AbstractActionStorage,
UiActionsEnhancedSerializedEvent as SerializedEvent,
} from '../../../ui_actions_enhanced/public';
@@ -13,12 +14,12 @@ import {
EmbeddableOutput,
IEmbeddable,
} from '../../../../../src/plugins/embeddable/public';
+import { SerializableState } from '../../../../../src/plugins/kibana_utils/common';
export interface EmbeddableWithDynamicActionsInput extends EmbeddableInput {
enhancements?: {
- dynamicActions?: {
- events: SerializedEvent[];
- };
+ dynamicActions: DynamicActionsState;
+ [key: string]: SerializableState;
};
}
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
index 3d28a05a4b7b48..a9bd03e8f97d4e 100644
--- a/x-pack/plugins/enterprise_search/server/plugin.ts
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -16,6 +16,7 @@ import {
KibanaRequest,
} from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { SecurityPluginSetup } from '../../security/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
@@ -82,6 +83,7 @@ export class EnterpriseSearchPlugin implements Plugin {
id: ENTERPRISE_SEARCH_PLUGIN.ID,
name: ENTERPRISE_SEARCH_PLUGIN.NAME,
order: 0,
+ category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
icon: 'logoEnterpriseSearch',
app: [
'kibana',
diff --git a/x-pack/plugins/features/common/kibana_feature.ts b/x-pack/plugins/features/common/kibana_feature.ts
index a600ada554afd4..32a75029567288 100644
--- a/x-pack/plugins/features/common/kibana_feature.ts
+++ b/x-pack/plugins/features/common/kibana_feature.ts
@@ -5,6 +5,7 @@
*/
import { RecursiveReadonly } from '@kbn/utility-types';
+import { AppCategory } from 'src/core/types';
import { FeatureKibanaPrivileges } from './feature_kibana_privileges';
import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature';
import { ReservedKibanaPrivilege } from './reserved_kibana_privilege';
@@ -29,6 +30,13 @@ export interface KibanaFeatureConfig {
*/
name: string;
+ /**
+ * The category for this feature.
+ * This will be used to organize the list of features for display within the
+ * Spaces and Roles management screens.
+ */
+ category: AppCategory;
+
/**
* An ordinal used to sort features relative to one another for display.
*/
@@ -158,6 +166,10 @@ export class KibanaFeature {
return this.config.order;
}
+ public get category() {
+ return this.config.category;
+ }
+
public get navLinkId() {
return this.config.navLinkId;
}
diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts
index e89cf06ec86214..aaaeccbd15e724 100644
--- a/x-pack/plugins/features/server/feature_registry.test.ts
+++ b/x-pack/plugins/features/server/feature_registry.test.ts
@@ -14,6 +14,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
@@ -35,6 +36,7 @@ describe('FeatureRegistry', () => {
icon: 'addDataApp',
navLinkId: 'someNavLink',
app: ['app1'],
+ category: { id: 'foo', label: 'foo' },
validLicenses: ['standard', 'basic', 'gold', 'platinum'],
catalogue: ['foo'],
management: {
@@ -143,11 +145,64 @@ describe('FeatureRegistry', () => {
expect(result[0].toRaw()).toEqual(feature);
});
+ describe('category', () => {
+ it('is required', () => {
+ const feature: KibanaFeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ privileges: null,
+ } as any;
+
+ const featureRegistry = new FeatureRegistry();
+ expect(() =>
+ featureRegistry.registerKibanaFeature(feature)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"child \\"category\\" fails because [\\"category\\" is required]"`
+ );
+ });
+
+ it('must have an id', () => {
+ const feature: KibanaFeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ privileges: null,
+ category: { label: 'foo' },
+ } as any;
+
+ const featureRegistry = new FeatureRegistry();
+ expect(() =>
+ featureRegistry.registerKibanaFeature(feature)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"child \\"category\\" fails because [child \\"id\\" fails because [\\"id\\" is required]]"`
+ );
+ });
+
+ it('must have a label', () => {
+ const feature: KibanaFeatureConfig = {
+ id: 'test-feature',
+ name: 'Test Feature',
+ app: [],
+ privileges: null,
+ category: { id: 'foo' },
+ } as any;
+
+ const featureRegistry = new FeatureRegistry();
+ expect(() =>
+ featureRegistry.registerKibanaFeature(feature)
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"child \\"category\\" fails because [child \\"label\\" fails because [\\"label\\" is required]]"`
+ );
+ });
+ });
+
it(`requires a value for privileges`, () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
} as any;
const featureRegistry = new FeatureRegistry();
@@ -163,6 +218,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
subFeatures: [
{
@@ -201,6 +257,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@@ -235,6 +292,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@@ -271,6 +329,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -303,6 +362,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
@@ -340,6 +400,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
@@ -347,6 +408,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Duplicate Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
@@ -367,6 +429,7 @@ describe('FeatureRegistry', () => {
name: 'some feature',
navLinkId: prohibitedChars,
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@@ -382,6 +445,7 @@ describe('FeatureRegistry', () => {
kibana: [prohibitedChars],
},
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@@ -395,6 +459,7 @@ describe('FeatureRegistry', () => {
name: 'some feature',
catalogue: [prohibitedChars],
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@@ -409,6 +474,7 @@ describe('FeatureRegistry', () => {
id: prohibitedId,
name: 'some feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
})
).toThrowErrorMatchingSnapshot();
@@ -420,6 +486,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['app1', 'app2'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
foo: {
name: 'Foo',
@@ -447,6 +514,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['bar'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -481,6 +549,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['foo', 'bar', 'baz'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -538,6 +607,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['bar'],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'something',
@@ -571,6 +641,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: ['foo', 'bar', 'baz'],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'something',
@@ -604,6 +675,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
privileges: {
all: {
@@ -641,6 +713,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['foo', 'bar', 'baz'],
privileges: {
all: {
@@ -701,6 +774,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
privileges: null,
reserved: {
@@ -736,6 +810,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['foo', 'bar', 'baz'],
privileges: null,
reserved: {
@@ -771,6 +846,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
alerting: ['bar'],
privileges: {
all: {
@@ -811,6 +887,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
alerting: ['foo', 'bar', 'baz'],
privileges: {
all: {
@@ -871,6 +948,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
alerting: ['bar'],
privileges: null,
reserved: {
@@ -906,6 +984,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
alerting: ['foo', 'bar', 'baz'],
privileges: null,
reserved: {
@@ -941,6 +1020,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@@ -987,6 +1067,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@@ -1060,6 +1141,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey'],
@@ -1101,6 +1183,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['bar'],
management: {
kibana: ['hey', 'hey-there'],
@@ -1142,6 +1225,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'my reserved privileges',
@@ -1184,6 +1268,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'my reserved privileges',
@@ -1216,12 +1301,14 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
const feature2: KibanaFeatureConfig = {
id: 'test-feature-2',
name: 'Test Feature 2',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
@@ -1346,6 +1433,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
@@ -1371,6 +1459,7 @@ describe('FeatureRegistry', () => {
id: 'test-feature',
name: 'Test Feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
};
diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts
index 06a3eb158d99d8..c6ec2d52c6d1ae 100644
--- a/x-pack/plugins/features/server/feature_schema.ts
+++ b/x-pack/plugins/features/server/feature_schema.ts
@@ -28,6 +28,14 @@ const managementSchema = Joi.object().pattern(
const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex));
const alertingSchema = Joi.array().items(Joi.string());
+const appCategorySchema = Joi.object({
+ id: Joi.string().required(),
+ label: Joi.string().required(),
+ ariaLabel: Joi.string(),
+ euiIconType: Joi.string(),
+ order: Joi.number(),
+}).required();
+
const kibanaPrivilegeSchema = Joi.object({
excludeFromBasePrivileges: Joi.boolean(),
management: managementSchema,
@@ -80,6 +88,7 @@ const kibanaFeatureSchema = Joi.object({
.invalid(...prohibitedFeatureIds)
.required(),
name: Joi.string().required(),
+ category: appCategorySchema,
order: Joi.number(),
excludeFromBasePrivileges: Joi.boolean(),
validLicenses: Joi.array().items(
diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts
index 3ff6b1b7bf44fd..4cec44d6fa19a3 100644
--- a/x-pack/plugins/features/server/oss_features.ts
+++ b/x-pack/plugins/features/server/oss_features.ts
@@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import { KibanaFeatureConfig } from '../common';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
export interface BuildOSSFeaturesParams {
savedObjectTypes: string[];
@@ -19,6 +20,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Discover',
}),
order: 100,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'discoverApp',
navLinkId: 'discover',
app: ['discover', 'kibana'],
@@ -78,7 +80,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
name: i18n.translate('xpack.features.visualizeFeatureName', {
defaultMessage: 'Visualize',
}),
- order: 200,
+ order: 700,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'visualizeApp',
navLinkId: 'visualize',
app: ['visualize', 'lens', 'kibana'],
@@ -138,7 +141,8 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
name: i18n.translate('xpack.features.dashboardFeatureName', {
defaultMessage: 'Dashboard',
}),
- order: 300,
+ order: 200,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'dashboardApp',
navLinkId: 'dashboards',
app: ['dashboards', 'kibana'],
@@ -217,6 +221,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Dev Tools',
}),
order: 1300,
+ category: DEFAULT_APP_CATEGORIES.management,
icon: 'devToolsApp',
navLinkId: 'dev_tools',
app: ['dev_tools', 'kibana'],
@@ -254,6 +259,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Advanced Settings',
}),
order: 1500,
+ category: DEFAULT_APP_CATEGORIES.management,
icon: 'advancedSettingsApp',
app: ['kibana'],
catalogue: ['advanced_settings'],
@@ -293,6 +299,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Index Pattern Management',
}),
order: 1600,
+ category: DEFAULT_APP_CATEGORIES.management,
icon: 'indexPatternApp',
app: ['kibana'],
catalogue: ['indexPatterns'],
@@ -332,6 +339,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
defaultMessage: 'Saved Objects Management',
}),
order: 1700,
+ category: DEFAULT_APP_CATEGORIES.management,
icon: 'savedObjectsApp',
app: ['kibana'],
catalogue: ['saved_objects'],
@@ -375,6 +383,7 @@ const timelionFeature: KibanaFeatureConfig = {
id: 'timelion',
name: 'Timelion',
order: 350,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'timelionApp',
navLinkId: 'timelion',
app: ['timelion', 'kibana'],
diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts
index ee11e0e2bbe2ea..ce6fb548ae6d2d 100644
--- a/x-pack/plugins/features/server/plugin.test.ts
+++ b/x-pack/plugins/features/server/plugin.test.ts
@@ -35,6 +35,7 @@ describe('Features Plugin', () => {
id: 'baz',
name: 'baz',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
@@ -63,6 +64,7 @@ describe('Features Plugin', () => {
id: 'baz',
name: 'baz',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts
index 30aa6d07f6b5a2..692a8892031317 100644
--- a/x-pack/plugins/features/server/routes/index.test.ts
+++ b/x-pack/plugins/features/server/routes/index.test.ts
@@ -28,6 +28,7 @@ describe('GET /api/features', () => {
id: 'feature_1',
name: 'Feature 1',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
@@ -36,6 +37,7 @@ describe('GET /api/features', () => {
name: 'Feature 2',
order: 2,
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
@@ -44,6 +46,7 @@ describe('GET /api/features', () => {
name: 'Feature 2',
order: 1,
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
@@ -51,6 +54,7 @@ describe('GET /api/features', () => {
id: 'licensed_feature',
name: 'Licensed Feature',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
validLicenses: ['gold'],
privileges: null,
});
diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
index 7532bc0573b08d..f5ba17a632c926 100644
--- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
+++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts
@@ -46,6 +46,7 @@ describe('populateUICapabilities', () => {
id: 'newFeature',
name: 'my new feature',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(),
read: createKibanaFeaturePrivilege(),
@@ -93,6 +94,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(),
@@ -146,6 +148,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['anotherFooEntry', 'anotherBarEntry'],
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
@@ -215,6 +218,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']),
@@ -245,6 +249,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: '',
@@ -289,6 +294,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@@ -360,6 +366,7 @@ describe('populateUICapabilities', () => {
name: 'my new feature',
navLinkId: 'newFeatureNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@@ -369,6 +376,7 @@ describe('populateUICapabilities', () => {
id: 'anotherNewFeature',
name: 'another new feature',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['capability3', 'capability4']),
@@ -379,6 +387,7 @@ describe('populateUICapabilities', () => {
name: 'yet another new feature',
navLinkId: 'yetAnotherNavLink',
app: ['bar-app'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: createKibanaFeaturePrivilege(['capability1', 'capability2']),
read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']),
diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
index 7bb9954fa30489..b93e27efccaef8 100644
--- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
+++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
@@ -13,7 +13,7 @@ Array [
},
],
"prepend": undefined,
- "title": "Canvas • Kibana",
+ "title": "Canvas • Kibana",
"url": "/app/test/Canvas",
},
Object {
@@ -27,7 +27,7 @@ Array [
},
],
"prepend": undefined,
- "title": "Discover • Kibana",
+ "title": "Discover • Kibana",
"url": "/app/test/Discover",
},
Object {
@@ -41,7 +41,7 @@ Array [
},
],
"prepend": undefined,
- "title": "Graph • Kibana",
+ "title": "Graph • Kibana",
"url": "/app/test/Graph",
},
]
@@ -60,7 +60,7 @@ Array [
},
],
"prepend": undefined,
- "title": "Discover • Kibana",
+ "title": "Discover • Kibana",
"url": "/app/test/Discover",
},
Object {
@@ -74,7 +74,7 @@ Array [
},
],
"prepend": undefined,
- "title": "My Dashboard • Test",
+ "title": "My Dashboard • Test",
"url": "/app/test/My Dashboard",
},
]
diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts
index d69c592655fb5a..21c50bf82f4bcf 100644
--- a/x-pack/plugins/graph/server/plugin.ts
+++ b/x-pack/plugins/graph/server/plugin.ts
@@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { Plugin, CoreSetup, CoreStart } from 'src/core/server';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { LicenseState } from './lib/license_state';
import { registerSearchRoute } from './routes/search';
@@ -46,7 +47,8 @@ export class GraphPlugin implements Plugin {
name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', {
defaultMessage: 'Graph',
}),
- order: 1200,
+ order: 600,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: 'graphApp',
navLinkId: 'graph',
app: ['graph', 'kibana'],
diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts
index 3075f9c89eb8d7..84b8fa35cfe9b9 100644
--- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts
@@ -82,10 +82,11 @@ export class IndexLifecycleManagementServerPlugin implements Plugin;
@@ -45,6 +46,7 @@ export const AlertPreview: React.FC = (props) => {
const {
alertParams,
alertInterval,
+ alertThrottle,
fetch,
alertType,
validate,
@@ -73,16 +75,27 @@ export const AlertPreview: React.FC = (props) => {
...alertParams,
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
alertInterval,
+ alertThrottle,
+ alertOnNoData: showNoDataResults ?? false,
} as AlertPreviewRequestParams,
alertType,
});
- setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval });
+ setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval, alertThrottle });
} catch (e) {
setPreviewError(e);
} finally {
setIsPreviewLoading(false);
}
- }, [alertParams, alertInterval, fetch, alertType, groupByDisplayName, previewLookbackInterval]);
+ }, [
+ alertParams,
+ alertInterval,
+ fetch,
+ alertType,
+ groupByDisplayName,
+ previewLookbackInterval,
+ alertThrottle,
+ showNoDataResults,
+ ]);
const previewIntervalError = useMemo(() => {
const intervalInSeconds = getIntervalInSeconds(alertInterval);
@@ -101,6 +114,13 @@ export const AlertPreview: React.FC = (props) => {
return hasValidationErrors || previewIntervalError;
}, [alertParams.criteria, previewIntervalError, validate]);
+ const showNumberOfNotifications = useMemo(() => {
+ if (!previewResult) return false;
+ const { notifications, fired, noData, error } = previewResult.resultTotals;
+ const unthrottledNotifications = fired + (showNoDataResults ? noData + error : 0);
+ return unthrottledNotifications > notifications;
+ }, [previewResult, showNoDataResults]);
+
return (
= (props) => {
<>
- {previewResult.resultTotals.fired}{' '}
- {previewResult.resultTotals.fired === 1
- ? firedTimeLabel
- : firedTimesLabel}
+
),
}}
@@ -173,7 +196,7 @@ export const AlertPreview: React.FC = (props) => {
) : null}
e.value === previewResult.previewLookbackInterval
@@ -211,6 +234,32 @@ export const AlertPreview: React.FC = (props) => {
defaultMessage="An error occurred when trying to evaluate some of the data."
/>
) : null}
+ {showNumberOfNotifications ? (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber',
+ {
+ defaultMessage:
+ '{notifs, plural, one {# notification} other {# notifications}}',
+ values: {
+ notifs: previewResult.resultTotals.notifications,
+ },
+ }
+ )}
+
+ ),
+ }}
+ />
+ >
+ ) : null}{' '}
>
)}
@@ -218,6 +267,7 @@ export const AlertPreview: React.FC = (props) => {
<>
= (props) => {
{previewError.body?.statusCode === 508 ? (
= (props) => {
) : (
= previewOptions.map((o) =>
omit(o, 'shortText')
);
-
-const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
- defaultMessage: 'time',
-});
-const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
- defaultMessage: 'times',
-});
diff --git a/x-pack/plugins/infra/public/alerting/common/index.ts b/x-pack/plugins/infra/public/alerting/common/index.ts
index e1b4a70cfb1fc4..384391578f0c6d 100644
--- a/x-pack/plugins/infra/public/alerting/common/index.ts
+++ b/x-pack/plugins/infra/public/alerting/common/index.ts
@@ -45,10 +45,3 @@ export const previewOptions = [
}),
},
];
-
-export const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
- defaultMessage: 'time',
-});
-export const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
- defaultMessage: 'times',
-});
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx
index ada7a30a859e08..60a00371e5ade5 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx
@@ -69,6 +69,7 @@ describe('Expression', () => {
Reflect.set(alertParams, key, value)}
diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
index 5ac2f407839e40..f47f30c280b2a5 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx
@@ -89,6 +89,7 @@ interface Props {
alertOnNoData?: boolean;
};
alertInterval: string;
+ alertThrottle: string;
alertsContext: AlertsContextValue;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
@@ -104,7 +105,14 @@ const defaultExpression = {
} as InventoryMetricConditions;
export const Expressions: React.FC = (props) => {
- const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
+ const {
+ setAlertParams,
+ alertParams,
+ errors,
+ alertsContext,
+ alertInterval,
+ alertThrottle,
+ } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@@ -378,6 +386,7 @@ export const Expressions: React.FC = (props) => {
{
Reflect.set(alertParams, key, value)}
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
index 6b102045fa516f..c71a3b6b13338e 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
@@ -51,6 +51,7 @@ interface Props {
alertParams: AlertParams;
alertsContext: AlertsContextValue;
alertInterval: string;
+ alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
}
@@ -65,7 +66,14 @@ const defaultExpression = {
export { defaultExpression };
export const Expressions: React.FC = (props) => {
- const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
+ const {
+ setAlertParams,
+ alertParams,
+ errors,
+ alertsContext,
+ alertInterval,
+ alertThrottle,
+ } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@@ -399,6 +407,7 @@ export const Expressions: React.FC = (props) => {
{
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
@@ -52,6 +56,10 @@ export const previewInventoryMetricThresholdAlert = async ({
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
+ const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
+ const executionsPerThrottle = Math.floor(
+ (throttleIntervalInSeconds / alertIntervalInSeconds) * alertResultsPerExecution
+ );
try {
const results = await Promise.all(
criteria.map((c) =>
@@ -66,6 +74,12 @@ export const previewInventoryMetricThresholdAlert = async ({
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
+ let numberOfNotifications = 0;
+ let throttleTracker = 0;
+ const notifyWithThrottle = () => {
+ if (throttleTracker === 0) numberOfNotifications++;
+ throttleTracker++;
+ };
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = results.every((result) => {
@@ -79,11 +93,27 @@ export const previewInventoryMetricThresholdAlert = async ({
const someConditionsErrorInMappedBucket = results.some((result) => {
return result[item].isError;
});
- if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
- if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
- if (someConditionsErrorInMappedBucket) numberOfErrors++;
+ if (someConditionsErrorInMappedBucket) {
+ numberOfErrors++;
+ if (alertOnNoData) {
+ notifyWithThrottle();
+ }
+ } else if (someConditionsNoDataInMappedBucket) {
+ numberOfNoDataResults++;
+ if (alertOnNoData) {
+ notifyWithThrottle();
+ }
+ } else if (allConditionsFiredInMappedBucket) {
+ numberOfTimesFired++;
+ notifyWithThrottle();
+ } else if (throttleTracker > 0) {
+ throttleTracker++;
+ }
+ if (throttleTracker === executionsPerThrottle) {
+ throttleTracker = 0;
+ }
}
- return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
+ return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
});
return previewResults;
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
index c26b44dfe8ff8d..73e17537476c89 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts
@@ -16,11 +16,14 @@ describe('Previewing the metric threshold alert type', () => {
...baseParams,
lookback: 'h',
alertInterval: '1m',
+ alertThrottle: '1m',
+ alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults] = ungroupedResult;
+ const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(30);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
+ expect(notifications).toBe(30);
});
test('returns the expected results using a bucket interval shorter than the alert interval', async () => {
@@ -28,22 +31,42 @@ describe('Previewing the metric threshold alert type', () => {
...baseParams,
lookback: 'h',
alertInterval: '3m',
+ alertThrottle: '3m',
+ alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults] = ungroupedResult;
+ const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(10);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
+ expect(notifications).toBe(10);
});
test('returns the expected results using a bucket interval longer than the alert interval', async () => {
const [ungroupedResult] = await previewMetricThresholdAlert({
...baseParams,
lookback: 'h',
alertInterval: '30s',
+ alertThrottle: '30s',
+ alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults] = ungroupedResult;
+ const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(60);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
+ expect(notifications).toBe(60);
+ });
+ test('returns the expected results using a throttle interval longer than the alert interval', async () => {
+ const [ungroupedResult] = await previewMetricThresholdAlert({
+ ...baseParams,
+ lookback: 'h',
+ alertInterval: '1m',
+ alertThrottle: '3m',
+ alertOnNoData: true,
+ });
+ const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
+ expect(firedResults).toBe(30);
+ expect(noDataResults).toBe(0);
+ expect(errorResults).toBe(0);
+ expect(notifications).toBe(15);
});
});
describe('querying with a groupBy parameter', () => {
@@ -56,15 +79,19 @@ describe('Previewing the metric threshold alert type', () => {
},
lookback: 'h',
alertInterval: '1m',
+ alertThrottle: '1m',
+ alertOnNoData: true,
});
- const [firedResultsA, noDataResultsA, errorResultsA] = resultA;
+ const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA;
expect(firedResultsA).toBe(30);
expect(noDataResultsA).toBe(0);
expect(errorResultsA).toBe(0);
- const [firedResultsB, noDataResultsB, errorResultsB] = resultB;
+ expect(notificationsA).toBe(30);
+ const [firedResultsB, noDataResultsB, errorResultsB, notificationsB] = resultB;
expect(firedResultsB).toBe(60);
expect(noDataResultsB).toBe(0);
expect(errorResultsB).toBe(0);
+ expect(notificationsB).toBe(60);
});
});
describe('querying a data set with a period of No Data', () => {
@@ -82,11 +109,14 @@ describe('Previewing the metric threshold alert type', () => {
},
lookback: 'h',
alertInterval: '1m',
+ alertThrottle: '1m',
+ alertOnNoData: true,
});
- const [firedResults, noDataResults, errorResults] = ungroupedResult;
+ const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(25);
expect(noDataResults).toBe(10);
expect(errorResults).toBe(0);
+ expect(notifications).toBe(35);
});
});
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
index 0f2afda663da81..e1615625d605ac 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
@@ -28,6 +28,8 @@ interface PreviewMetricThresholdAlertParams {
config: InfraSource['configuration'];
lookback: Unit;
alertInterval: string;
+ alertThrottle: string;
+ alertOnNoData: boolean;
end?: number;
overrideLookbackIntervalInSeconds?: number;
}
@@ -43,6 +45,8 @@ export const previewMetricThresholdAlert: (
config,
lookback,
alertInterval,
+ alertThrottle,
+ alertOnNoData,
end = Date.now(),
overrideLookbackIntervalInSeconds,
},
@@ -77,6 +81,11 @@ export const previewMetricThresholdAlert: (
// Now determine how to interpolate this histogram based on the alert interval
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
+ const throttleIntervalInSeconds = Math.max(
+ getIntervalInSeconds(alertThrottle),
+ alertIntervalInSeconds
+ );
+
const previewResults = await Promise.all(
groups.map(async (group) => {
// Interpolate the buckets returned by evaluateAlert and return a count of how many of these
@@ -90,6 +99,12 @@ export const previewMetricThresholdAlert: (
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
+ let numberOfNotifications = 0;
+ let throttleTracker = 0;
+ const notifyWithThrottle = () => {
+ if (throttleTracker === 0) numberOfNotifications++;
+ throttleTracker += alertIntervalInSeconds;
+ };
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = alertResults.every(
@@ -102,11 +117,27 @@ export const previewMetricThresholdAlert: (
const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => {
return alertResult[group].isError;
});
- if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
- if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
- if (someConditionsErrorInMappedBucket) numberOfErrors++;
+ if (someConditionsErrorInMappedBucket) {
+ numberOfErrors++;
+ if (alertOnNoData) {
+ notifyWithThrottle();
+ }
+ } else if (someConditionsNoDataInMappedBucket) {
+ numberOfNoDataResults++;
+ if (alertOnNoData) {
+ notifyWithThrottle();
+ }
+ } else if (allConditionsFiredInMappedBucket) {
+ numberOfTimesFired++;
+ notifyWithThrottle();
+ } else if (throttleTracker > 0) {
+ throttleTracker += alertIntervalInSeconds;
+ }
+ if (throttleTracker >= throttleIntervalInSeconds) {
+ throttleTracker = 0;
+ }
}
- return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
+ return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
})
);
return previewResults;
@@ -114,7 +145,15 @@ export const previewMetricThresholdAlert: (
if (isTooManyBucketsPreviewException(e)) {
// If there's too much data on the first request, recursively slice the lookback interval
// until all the data can be retrieved
- const basePreviewParams = { callCluster, params, config, lookback, alertInterval };
+ const basePreviewParams = {
+ callCluster,
+ params,
+ config,
+ lookback,
+ alertInterval,
+ alertThrottle,
+ alertOnNoData,
+ };
const { maxBuckets } = e;
// If this is still the first iteration, try to get the number of groups in order to
// calculate max buckets. If this fails, just estimate based on 1 group
@@ -159,7 +198,7 @@ export const previewMetricThresholdAlert: (
.reduce((a, b) => {
if (!a) return b;
if (!b) return a;
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
})
);
return zippedResult;
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index 40d09dadfe0505..1233e9d2d1357f 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -30,7 +30,16 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
},
},
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
- const { criteria, filterQuery, lookback, sourceId, alertType, alertInterval } = request.body;
+ const {
+ criteria,
+ filterQuery,
+ lookback,
+ sourceId,
+ alertType,
+ alertInterval,
+ alertThrottle,
+ alertOnNoData,
+ } = request.body;
const callCluster = (endpoint: string, opts: Record) => {
return callWithRequest(requestContext, endpoint, opts);
@@ -51,22 +60,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
lookback,
config: source.configuration,
alertInterval,
+ alertThrottle,
+ alertOnNoData,
});
const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce(
- (totals, [firedResult, noDataResult, errorResult]) => {
+ (totals, [firedResult, noDataResult, errorResult, notifications]) => {
return {
...totals,
fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
+ notifications: totals.notifications + notifications,
};
},
{
fired: 0,
noData: 0,
error: 0,
+ notifications: 0,
}
);
return response.ok({
@@ -84,22 +97,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
lookback,
source,
alertInterval,
+ alertThrottle,
+ alertOnNoData,
});
const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce(
- (totals, [firedResult, noDataResult, errorResult]) => {
+ (totals, [firedResult, noDataResult, errorResult, notifications]) => {
return {
...totals,
fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
+ notifications: totals.notifications + notifications,
};
},
{
fired: 0,
noData: 0,
error: 0,
+ notifications: 0,
}
);
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 47900415466b9d..f0f7bca29c99e8 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -16,6 +16,7 @@ import {
SavedObjectsClientContract,
} from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { LicensingPluginSetup, ILicense } from '../../licensing/server';
import {
EncryptedSavedObjectsPluginStart,
@@ -181,6 +182,7 @@ export class IngestManagerPlugin
id: PLUGIN_ID,
name: 'Ingest Manager',
icon: 'savedObjectsApp',
+ category: DEFAULT_APP_CATEGORIES.management,
navLinkId: PLUGIN_ID,
app: [PLUGIN_ID, 'kibana'],
catalogue: ['ingestManager'],
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
index 83ad08d09de760..dfa03ec9d527dd 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Logger, SavedObjectsClientContract } from 'kibana/server';
+import { SavedObjectsClientContract } from 'kibana/server';
import { saveInstalledEsRefs } from '../../packages/install';
import * as Registry from '../../registry';
@@ -33,100 +33,92 @@ export const installTransformForDataset = async (
registryPackage: RegistryPackage,
paths: string[],
callCluster: CallESAsCurrentUser,
- savedObjectsClient: SavedObjectsClientContract,
- logger: Logger
+ savedObjectsClient: SavedObjectsClientContract
) => {
- try {
- const installation = await getInstallation({
- savedObjectsClient,
- pkgName: registryPackage.name,
- });
- let previousInstalledTransformEsAssets: EsAssetReference[] = [];
- if (installation) {
- previousInstalledTransformEsAssets = installation.installed_es.filter(
- ({ type, id }) => type === ElasticsearchAssetType.transform
- );
- }
-
- // delete all previous transform
- await deleteTransforms(
- callCluster,
- previousInstalledTransformEsAssets.map((asset) => asset.id)
+ const installation = await getInstallation({
+ savedObjectsClient,
+ pkgName: registryPackage.name,
+ });
+ let previousInstalledTransformEsAssets: EsAssetReference[] = [];
+ if (installation) {
+ previousInstalledTransformEsAssets = installation.installed_es.filter(
+ ({ type, id }) => type === ElasticsearchAssetType.transform
);
- // install the latest dataset
- const datasets = registryPackage.datasets;
- if (!datasets?.length) return [];
- const installNameSuffix = `${registryPackage.version}`;
-
- const transformPaths = paths.filter((path) => isTransform(path));
- let installedTransforms: EsAssetReference[] = [];
- if (transformPaths.length > 0) {
- const transformPathDatasets = datasets.reduce((acc, dataset) => {
- transformPaths.forEach((path) => {
- if (isDatasetTransform(path, dataset.path)) {
- acc.push({ path, dataset });
- }
- });
- return acc;
- }, []);
-
- const transformRefs = transformPathDatasets.reduce(
- (acc, transformPathDataset) => {
- if (transformPathDataset) {
- acc.push({
- id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
- type: ElasticsearchAssetType.transform,
- });
- }
- return acc;
- },
- []
- );
-
- // get and save transform refs before installing transforms
- await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
-
- const transforms: TransformInstallation[] = transformPathDatasets.map(
- (transformPathDataset: TransformPathDataset) => {
- return {
- installationName: getTransformNameForInstallation(
- transformPathDataset,
- installNameSuffix
- ),
- content: getAsset(transformPathDataset.path).toString('utf-8'),
- };
- }
- );
+ }
- const installationPromises = transforms.map(async (transform) => {
- return installTransform({ callCluster, transform, logger });
+ // delete all previous transform
+ await deleteTransforms(
+ callCluster,
+ previousInstalledTransformEsAssets.map((asset) => asset.id)
+ );
+ // install the latest dataset
+ const datasets = registryPackage.datasets;
+ if (!datasets?.length) return [];
+ const installNameSuffix = `${registryPackage.version}`;
+
+ const transformPaths = paths.filter((path) => isTransform(path));
+ let installedTransforms: EsAssetReference[] = [];
+ if (transformPaths.length > 0) {
+ const transformPathDatasets = datasets.reduce((acc, dataset) => {
+ transformPaths.forEach((path) => {
+ if (isDatasetTransform(path, dataset.path)) {
+ acc.push({ path, dataset });
+ }
});
+ return acc;
+ }, []);
+
+ const transformRefs = transformPathDatasets.reduce(
+ (acc, transformPathDataset) => {
+ if (transformPathDataset) {
+ acc.push({
+ id: getTransformNameForInstallation(transformPathDataset, installNameSuffix),
+ type: ElasticsearchAssetType.transform,
+ });
+ }
+ return acc;
+ },
+ []
+ );
- installedTransforms = await Promise.all(installationPromises).then((results) =>
- results.flat()
- );
- }
+ // get and save transform refs before installing transforms
+ await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs);
+
+ const transforms: TransformInstallation[] = transformPathDatasets.map(
+ (transformPathDataset: TransformPathDataset) => {
+ return {
+ installationName: getTransformNameForInstallation(
+ transformPathDataset,
+ installNameSuffix
+ ),
+ content: getAsset(transformPathDataset.path).toString('utf-8'),
+ };
+ }
+ );
- if (previousInstalledTransformEsAssets.length > 0) {
- const currentInstallation = await getInstallation({
- savedObjectsClient,
- pkgName: registryPackage.name,
- });
+ const installationPromises = transforms.map(async (transform) => {
+ return installTransform({ callCluster, transform });
+ });
- // remove the saved object reference
- await deleteTransformRefs(
- savedObjectsClient,
- currentInstallation?.installed_es || [],
- registryPackage.name,
- previousInstalledTransformEsAssets.map((asset) => asset.id),
- installedTransforms.map((installed) => installed.id)
- );
- }
- return installedTransforms;
- } catch (err) {
- logger.error(err);
- throw err;
+ installedTransforms = await Promise.all(installationPromises).then((results) => results.flat());
}
+
+ if (previousInstalledTransformEsAssets.length > 0) {
+ const currentInstallation = await getInstallation({
+ savedObjectsClient,
+ pkgName: registryPackage.name,
+ });
+
+ // remove the saved object reference
+ await deleteTransformRefs(
+ savedObjectsClient,
+ currentInstallation?.installed_es || [],
+ registryPackage.name,
+ previousInstalledTransformEsAssets.map((asset) => asset.id),
+ installedTransforms.map((installed) => installed.id)
+ );
+ }
+ return installedTransforms;
};
const isTransform = (path: string) => {
@@ -147,31 +139,24 @@ const isDatasetTransform = (path: string, datasetName: string) => {
async function installTransform({
callCluster,
transform,
- logger,
}: {
callCluster: CallESAsCurrentUser;
transform: TransformInstallation;
- logger: Logger;
}): Promise {
- try {
- // defer validation on put if the source index is not available
- await callCluster('transport.request', {
- method: 'PUT',
- path: `/_transform/${transform.installationName}`,
- query: 'defer_validation=true',
- body: transform.content,
- });
-
- await callCluster('transport.request', {
- method: 'POST',
- path: `/_transform/${transform.installationName}/_start`,
- });
-
- return { id: transform.installationName, type: ElasticsearchAssetType.transform };
- } catch (err) {
- logger.error(err);
- throw err;
- }
+ // defer validation on put if the source index is not available
+ await callCluster('transport.request', {
+ method: 'PUT',
+ path: `/_transform/${transform.installationName}`,
+ query: 'defer_validation=true',
+ body: transform.content,
+ });
+
+ await callCluster('transport.request', {
+ method: 'POST',
+ path: `/_transform/${transform.installationName}/_start`,
+ });
+
+ return { id: transform.installationName, type: ElasticsearchAssetType.transform };
}
const getTransformNameForInstallation = (
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
index bb506ecad0ade2..c43a33df2db613 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts
@@ -4,9 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { loggingSystemMock } from '../../../../../../../../src/core/server/logging/logging_system.mock';
-
jest.mock('../../packages/get', () => {
return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
});
@@ -18,12 +15,7 @@ jest.mock('./common', () => {
});
import { installTransformForDataset } from './install';
-import {
- ILegacyScopedClusterClient,
- LoggerFactory,
- SavedObject,
- SavedObjectsClientContract,
-} from 'kibana/server';
+import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server';
import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types';
import { getInstallation, getInstallationObject } from '../../packages';
import { getAsset } from './common';
@@ -33,7 +25,6 @@ import { savedObjectsClientMock } from '../../../../../../../../src/core/server/
describe('test transform install', () => {
let legacyScopedClusterClient: jest.Mocked;
let savedObjectsClient: jest.Mocked;
- let logger: jest.Mocked;
beforeEach(() => {
legacyScopedClusterClient = {
callAsInternalUser: jest.fn(),
@@ -42,7 +33,6 @@ describe('test transform install', () => {
(getInstallation as jest.MockedFunction).mockReset();
(getInstallationObject as jest.MockedFunction).mockReset();
savedObjectsClient = savedObjectsClientMock.create();
- logger = loggingSystemMock.create();
});
afterEach(() => {
@@ -142,8 +132,7 @@ describe('test transform install', () => {
'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json',
],
legacyScopedClusterClient.callAsCurrentUser,
- savedObjectsClient,
- logger.get('ingest')
+ savedObjectsClient
);
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
[
@@ -297,8 +286,7 @@ describe('test transform install', () => {
} as unknown) as RegistryPackage,
['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'],
legacyScopedClusterClient.callAsCurrentUser,
- savedObjectsClient,
- logger.get('ingest')
+ savedObjectsClient
);
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
@@ -395,8 +383,7 @@ describe('test transform install', () => {
} as unknown) as RegistryPackage,
[],
legacyScopedClusterClient.callAsCurrentUser,
- savedObjectsClient,
- logger.get('ingest')
+ savedObjectsClient
);
expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 4179e82d6ad1de..54b9c4d3fbb172 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -36,7 +36,6 @@ import { deleteKibanaSavedObjectsAssets } from './remove';
import { PackageOutdatedError } from '../../../errors';
import { getPackageSavedObjects } from './get';
import { installTransformForDataset } from '../elasticsearch/transform/install';
-import { appContextService } from '../../app_context';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -197,8 +196,7 @@ export async function installPackage({
registryPackageInfo,
paths,
callCluster,
- savedObjectsClient,
- appContextService.getLogger()
+ savedObjectsClient
);
// if this is an update or retrying an update, delete the previous version's pipelines
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
index 91adbcecaf897d..077e07a89f788e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx
@@ -118,6 +118,7 @@ export const QueryInput = ({
return (
void }) => (
-
+
{label}
);
diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
index 365e430a460fad..50b8f4c6fc40b1 100644
--- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx
@@ -126,6 +126,7 @@ export function PieToolbar(props: VisualizationToolbarProps
& {
* Adjusts the borders for groupings
*/
groupPosition?: 'none' | 'left' | 'center' | 'right';
+ dataTestSubj?: string;
};
export const ToolbarButton: React.FunctionComponent = ({
@@ -42,6 +43,7 @@ export const ToolbarButton: React.FunctionComponent = ({
size = 'm',
hasArrow = true,
groupPosition = 'none',
+ dataTestSubj = '',
...rest
}) => {
const classes = classNames(
@@ -52,6 +54,7 @@ export const ToolbarButton: React.FunctionComponent = ({
);
return (
= ({
@@ -39,6 +40,7 @@ export const ToolbarPopover: React.FunctionComponent = ({
type,
isDisabled = false,
groupPosition,
+ buttonDataTestSubj,
}) => {
const [open, setOpen] = useState(false);
@@ -60,6 +62,7 @@ export const ToolbarPopover: React.FunctionComponent = ({
hasArrow={false}
isDisabled={isDisabled}
groupPosition={groupPosition}
+ dataTestSubj={buttonDataTestSubj}
>
diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
index 835f3e2cde7692..45ec7098aa6395 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
@@ -73,7 +73,12 @@ export interface AxisSettingsPopoverProps {
const popoverConfig = (
axis: AxesSettingsConfigKeys,
isHorizontal: boolean
-): { icon: IconType; groupPosition: ToolbarButtonProps['groupPosition']; popoverTitle: string } => {
+): {
+ icon: IconType;
+ groupPosition: ToolbarButtonProps['groupPosition'];
+ popoverTitle: string;
+ buttonDataTestSubj: string;
+} => {
switch (axis) {
case 'yLeft':
return {
@@ -86,6 +91,7 @@ const popoverConfig = (
: i18n.translate('xpack.lens.xyChart.leftAxisLabel', {
defaultMessage: 'Left axis',
}),
+ buttonDataTestSubj: 'lnsLeftAxisButton',
};
case 'yRight':
return {
@@ -98,6 +104,7 @@ const popoverConfig = (
: i18n.translate('xpack.lens.xyChart.rightAxisLabel', {
defaultMessage: 'Right axis',
}),
+ buttonDataTestSubj: 'lnsRightAxisButton',
};
case 'x':
default:
@@ -111,6 +118,8 @@ const popoverConfig = (
: i18n.translate('xpack.lens.xyChart.bottomAxisLabel', {
defaultMessage: 'Bottom axis',
}),
+
+ buttonDataTestSubj: 'lnsBottomAxisButton',
};
}
};
@@ -143,6 +152,7 @@ export const AxisSettingsPopover: React.FunctionComponent
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index 4aa5bd62c05a5e..c7781c2e1d50cb 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -208,6 +208,7 @@ export function XyToolbar(props: VisualizationToolbarProps) {
isDisabled={!hasNonBarSeries}
type="values"
groupPosition="left"
+ buttonDataTestSubj="lnsMissingValuesButton"
>
) {
})}
>
{
return {
@@ -488,6 +490,7 @@ const ColorPicker = ({
const colorPicker = (
{
expect(first.type).toBe('basic');
trigger$.next();
+ // waiting on a promise gives the exhaustMap time to complete and not de-dupe these calls
+ await Promise.resolve();
trigger$.next();
const [, second] = await license$.pipe(take(2), toArray()).toPromise();
@@ -89,18 +91,15 @@ describe('licensing update', () => {
expect(fetcher).toHaveBeenCalledTimes(1);
});
- it('handles fetcher race condition', async () => {
+ it('ignores trigger if license fetching is delayed ', async () => {
const delayMs = 100;
- let firstCall = true;
- const fetcher = jest.fn().mockImplementation(
+ const fetcher = jest.fn().mockImplementationOnce(
() =>
new Promise((resolve) => {
- if (firstCall) {
- firstCall = false;
- setTimeout(() => resolve(licenseMock.createLicense()), delayMs);
- } else {
- resolve(licenseMock.createLicense({ license: { type: 'gold' } }));
- }
+ setTimeout(
+ () => resolve(licenseMock.createLicense({ license: { type: 'gold' } })),
+ delayMs
+ );
})
);
const trigger$ = new Subject();
@@ -113,7 +112,7 @@ describe('licensing update', () => {
await delay(delayMs * 2);
- await expect(fetcher).toHaveBeenCalledTimes(2);
+ await expect(fetcher).toHaveBeenCalledTimes(1);
await expect(values).toHaveLength(1);
await expect(values[0].type).toBe('gold');
});
@@ -144,7 +143,7 @@ describe('licensing update', () => {
expect(fetcher).toHaveBeenCalledTimes(0);
});
- it('refreshManually guarantees license fetching', async () => {
+ it(`refreshManually multiple times gets new license`, async () => {
const trigger$ = new Subject();
const firstLicense = licenseMock.createLicense({ license: { uid: 'first', type: 'basic' } });
const secondLicense = licenseMock.createLicense({ license: { uid: 'second', type: 'gold' } });
diff --git a/x-pack/plugins/licensing/common/license_update.ts b/x-pack/plugins/licensing/common/license_update.ts
index 0197ca5396ad11..cd5052b0b49a31 100644
--- a/x-pack/plugins/licensing/common/license_update.ts
+++ b/x-pack/plugins/licensing/common/license_update.ts
@@ -5,32 +5,41 @@
*/
import { ConnectableObservable, Observable, Subject, from, merge } from 'rxjs';
-import { filter, map, pairwise, switchMap, publishReplay, takeUntil } from 'rxjs/operators';
+import {
+ filter,
+ map,
+ pairwise,
+ exhaustMap,
+ publishReplay,
+ share,
+ take,
+ takeUntil,
+} from 'rxjs/operators';
import { hasLicenseInfoChanged } from './has_license_info_changed';
import { ILicense } from './types';
export function createLicenseUpdate(
- trigger$: Observable,
+ triggerRefresh$: Observable,
stop$: Observable,
fetcher: () => Promise,
initialValues?: ILicense
) {
- const triggerRefresh$ = trigger$.pipe(switchMap(fetcher));
- const manuallyFetched$ = new Subject();
+ const manuallyRefresh$ = new Subject();
+ const fetched$ = merge(triggerRefresh$, manuallyRefresh$).pipe(exhaustMap(fetcher), share());
- const fetched$ = merge(triggerRefresh$, manuallyFetched$).pipe(
+ const cached$ = fetched$.pipe(
takeUntil(stop$),
publishReplay(1)
// have to cast manually as pipe operator cannot return ConnectableObservable
// https://github.com/ReactiveX/rxjs/issues/2972
) as ConnectableObservable;
- const fetchSubscription = fetched$.connect();
- stop$.subscribe({ complete: () => fetchSubscription.unsubscribe() });
+ const cachedSubscription = cached$.connect();
+ stop$.subscribe({ complete: () => cachedSubscription.unsubscribe() });
const initialValues$ = initialValues ? from([undefined, initialValues]) : from([undefined]);
- const license$: Observable = merge(initialValues$, fetched$).pipe(
+ const license$: Observable = merge(initialValues$, cached$).pipe(
pairwise(),
filter(([previous, next]) => hasLicenseInfoChanged(previous, next!)),
map(([, next]) => next!)
@@ -38,10 +47,10 @@ export function createLicenseUpdate(
return {
license$,
- async refreshManually() {
- const license = await fetcher();
- manuallyFetched$.next(license);
- return license;
+ refreshManually() {
+ const licensePromise = fetched$.pipe(take(1)).toPromise();
+ manuallyRefresh$.next();
+ return licensePromise;
},
};
}
diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts
index 960fe3699e2105..c20563dd159138 100644
--- a/x-pack/plugins/licensing/public/plugin.test.ts
+++ b/x-pack/plugins/licensing/public/plugin.test.ts
@@ -115,7 +115,9 @@ describe('licensing plugin', () => {
refresh();
} else if (i === 2) {
expect(value.type).toBe('gold');
- refresh();
+ // since this is a synchronous subscription, we need to give the exhaustMap a chance
+ // to mark the subscription as complete before emitting another value on the Subject
+ process.nextTick(() => refresh());
} else if (i === 3) {
expect(value.type).toBe('platinum');
done();
diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts
index 5c768a00783a80..af3ec42ab4ec5d 100644
--- a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts
+++ b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts
@@ -25,7 +25,23 @@ describe('createOnPreResponseHandler', () => {
},
});
});
- it('sets license.signature header after refresh for non-error responses', async () => {
+ it('sets license.signature header immediately for 429 error responses', async () => {
+ const refresh = jest.fn();
+ const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' }));
+ const toolkit = httpServiceMock.createOnPreResponseToolkit();
+
+ const interceptor = createOnPreResponseHandler(refresh, license$);
+ await interceptor(httpServerMock.createKibanaRequest(), { statusCode: 429 }, toolkit);
+
+ expect(refresh).toHaveBeenCalledTimes(0);
+ expect(toolkit.next).toHaveBeenCalledTimes(1);
+ expect(toolkit.next).toHaveBeenCalledWith({
+ headers: {
+ 'kbn-license-sig': 'foo',
+ },
+ });
+ });
+ it('sets license.signature header after refresh for other error responses', async () => {
const updatedLicense = licenseMock.createLicense({ signature: 'bar' });
const license$ = new BehaviorSubject(licenseMock.createLicense({ signature: 'foo' }));
const refresh = jest.fn().mockImplementation(
diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.ts
index c8befceb4fe322..6428e41b180583 100644
--- a/x-pack/plugins/licensing/server/on_pre_response_handler.ts
+++ b/x-pack/plugins/licensing/server/on_pre_response_handler.ts
@@ -15,9 +15,11 @@ export function createOnPreResponseHandler(
return async (req, res, t) => {
// If we're returning an error response, refresh license info from
// Elasticsearch in case the error is due to a change in license information
- // in Elasticsearch.
- // https://github.com/elastic/x-pack-kibana/pull/2876
- if (res.statusCode >= 400) {
+ // in Elasticsearch. https://github.com/elastic/x-pack-kibana/pull/2876
+ // We're explicit ignoring a 429 "Too Many Requests". This is being used to communicate
+ // that back-pressure should be applied, and we don't need to refresh the license in these
+ // situations.
+ if (res.statusCode >= 400 && res.statusCode !== 429) {
await refresh();
}
const license = await license$.pipe(take(1)).toPromise();
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
index 2e0ba7cf3efee0..f565321f87ef79 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
@@ -15,7 +15,7 @@ import {
RENDER_AS,
SOURCE_TYPES,
} from '../../../../common/constants';
-import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source';
+import { SearchSource } from 'src/plugins/data/public';
export class MockSearchSource {
setField = jest.fn();
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts
index 3223d0c94178ff..0bc9bba7816ca6 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.test.ts
@@ -9,7 +9,7 @@ jest.mock('../../../kibana_services');
jest.mock('./load_index_settings');
import { getIndexPatternService, getSearchService, getHttp } from '../../../kibana_services';
-import { SearchSource } from '../../../../../../../src/plugins/data/public/search/search_source';
+import { SearchSource } from 'src/plugins/data/public';
// @ts-expect-error
import { loadIndexSettings } from './load_index_settings';
diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts
index 5eb0482905e36a..46e39fcdac27a2 100644
--- a/x-pack/plugins/maps/server/plugin.ts
+++ b/x-pack/plugins/maps/server/plugin.ts
@@ -6,6 +6,7 @@
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'src/core/server';
import { take } from 'rxjs/operators';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
// @ts-ignore
import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects';
@@ -168,7 +169,8 @@ export class MapsPlugin implements Plugin {
name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', {
defaultMessage: 'Maps',
}),
- order: 600,
+ order: 400,
+ category: DEFAULT_APP_CATEGORIES.kibana,
icon: APP_ICON,
navLinkId: APP_ID,
app: [APP_ID, 'kibana'],
diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
index a8b775c8d5f609..9dc3896e9be48b 100644
--- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts
+++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
@@ -29,6 +29,8 @@ export interface FindFileStructureResponse {
count: number;
cardinality: number;
top_hits: Array<{ count: number; value: any }>;
+ max_value?: number;
+ min_value?: number;
};
};
sample_start: string;
@@ -42,7 +44,7 @@ export interface FindFileStructureResponse {
delimiter: string;
need_client_timezone: boolean;
num_lines_analyzed: number;
- column_names: string[];
+ column_names?: string[];
explanation?: string[];
grok_pattern?: string;
multiline_start_pattern?: string;
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx
new file mode 100644
index 00000000000000..610b29c85a0627
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_field_label.tsx
@@ -0,0 +1,20 @@
+/*
+ * 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 { EuiText } from '@elastic/eui';
+
+import { CombinedField } from './types';
+
+export function CombinedFieldLabel({ combinedField }: { combinedField: CombinedField }) {
+ return {getCombinedFieldLabel(combinedField)} ;
+}
+
+function getCombinedFieldLabel(combinedField: CombinedField) {
+ return `${combinedField.fieldNames.join(combinedField.delimiter)} => ${
+ combinedField.combinedFieldName
+ } (${combinedField.mappingType})`;
+}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx
new file mode 100644
index 00000000000000..fdfe10c2acf023
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_form.tsx
@@ -0,0 +1,237 @@
+/*
+ * 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';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { Component } from 'react';
+
+import {
+ EuiFormRow,
+ EuiPopover,
+ EuiContextMenu,
+ EuiButtonEmpty,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import { CombinedField } from './types';
+import { GeoPointForm } from './geo_point';
+import { CombinedFieldLabel } from './combined_field_label';
+import {
+ addCombinedFieldsToMappings,
+ addCombinedFieldsToPipeline,
+ getNameCollisionMsg,
+ removeCombinedFieldsFromMappings,
+ removeCombinedFieldsFromPipeline,
+} from './utils';
+import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
+
+interface Props {
+ mappingsString: string;
+ pipelineString: string;
+ onMappingsStringChange(): void;
+ onPipelineStringChange(): void;
+ combinedFields: CombinedField[];
+ onCombinedFieldsChange(combinedFields: CombinedField[]): void;
+ results: FindFileStructureResponse;
+ isDisabled: boolean;
+}
+
+interface State {
+ isPopoverOpen: boolean;
+}
+
+export class CombinedFieldsForm extends Component {
+ state: State = {
+ isPopoverOpen: false,
+ };
+
+ togglePopover = () => {
+ this.setState((prevState) => ({
+ isPopoverOpen: !prevState.isPopoverOpen,
+ }));
+ };
+
+ closePopover = () => {
+ this.setState({
+ isPopoverOpen: false,
+ });
+ };
+
+ addCombinedField = (combinedField: CombinedField) => {
+ if (this.hasNameCollision(combinedField.combinedFieldName)) {
+ throw new Error(getNameCollisionMsg(combinedField.combinedFieldName));
+ }
+
+ const mappings = this.parseMappings();
+ const pipeline = this.parsePipeline();
+
+ this.props.onMappingsStringChange(
+ // @ts-expect-error
+ JSON.stringify(addCombinedFieldsToMappings(mappings, [combinedField]), null, 2)
+ );
+ this.props.onPipelineStringChange(
+ // @ts-expect-error
+ JSON.stringify(addCombinedFieldsToPipeline(pipeline, [combinedField]), null, 2)
+ );
+ this.props.onCombinedFieldsChange([...this.props.combinedFields, combinedField]);
+
+ this.closePopover();
+ };
+
+ removeCombinedField = (index: number) => {
+ let mappings;
+ let pipeline;
+ try {
+ mappings = this.parseMappings();
+ pipeline = this.parsePipeline();
+ } catch (error) {
+ // how should remove error be surfaced?
+ return;
+ }
+
+ const updatedCombinedFields = [...this.props.combinedFields];
+ const removedCombinedFields = updatedCombinedFields.splice(index, 1);
+
+ this.props.onMappingsStringChange(
+ // @ts-expect-error
+ JSON.stringify(removeCombinedFieldsFromMappings(mappings, removedCombinedFields), null, 2)
+ );
+ this.props.onPipelineStringChange(
+ // @ts-expect-error
+ JSON.stringify(removeCombinedFieldsFromPipeline(pipeline, removedCombinedFields), null, 2)
+ );
+ this.props.onCombinedFieldsChange(updatedCombinedFields);
+ };
+
+ parseMappings() {
+ try {
+ return JSON.parse(this.props.mappingsString);
+ } catch (error) {
+ throw new Error(
+ i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.mappingsParseError', {
+ defaultMessage: 'Error parsing mappings: {error}',
+ values: { error: error.message },
+ })
+ );
+ }
+ }
+
+ parsePipeline() {
+ try {
+ return JSON.parse(this.props.pipelineString);
+ } catch (error) {
+ throw new Error(
+ i18n.translate('xpack.ml.fileDatavisualizer.combinedFieldsForm.pipelineParseError', {
+ defaultMessage: 'Error parsing pipeline: {error}',
+ values: { error: error.message },
+ })
+ );
+ }
+ }
+
+ hasNameCollision = (name: string) => {
+ if (this.props.results.column_names?.includes(name)) {
+ // collision with column name
+ return true;
+ }
+
+ if (
+ this.props.combinedFields.some((combinedField) => combinedField.combinedFieldName === name)
+ ) {
+ // collision with combined field name
+ return true;
+ }
+
+ const mappings = this.parseMappings();
+ return mappings.properties.hasOwnProperty(name);
+ };
+
+ render() {
+ const geoPointLabel = i18n.translate('xpack.ml.fileDatavisualizer.geoPointCombinedFieldLabel', {
+ defaultMessage: 'Add geo point field',
+ });
+ const panels = [
+ {
+ id: 0,
+ items: [
+ {
+ name: geoPointLabel,
+ panel: 1,
+ },
+ ],
+ },
+ {
+ id: 1,
+ title: geoPointLabel,
+ content: (
+
+ ),
+ },
+ ];
+ return (
+
+
+ {this.props.combinedFields.map((combinedField: CombinedField, idx: number) => (
+
+
+
+
+ {!this.props.isDisabled && (
+
+
+
+ )}
+
+ ))}
+
+
+
+ }
+ isOpen={this.state.isPopoverOpen}
+ closePopover={this.closePopover}
+ anchorPosition="rightCenter"
+ >
+
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.tsx
new file mode 100644
index 00000000000000..c37e27e39a7aba
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/combined_fields_read_only_form.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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';
+import React from 'react';
+
+import { EuiFormRow } from '@elastic/eui';
+
+import { CombinedField } from './types';
+import { CombinedFieldLabel } from './combined_field_label';
+
+export function CombinedFieldsReadOnlyForm({
+ combinedFields,
+}: {
+ combinedFields: CombinedField[];
+}) {
+ return combinedFields.length ? (
+
+
+ {combinedFields.map((combinedField: CombinedField, idx: number) => (
+
+ ))}
+
+
+ ) : null;
+}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx
new file mode 100644
index 00000000000000..831ae8de8081a9
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/geo_point.tsx
@@ -0,0 +1,189 @@
+/*
+ * 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';
+import debounce from 'lodash/debounce';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { ChangeEvent, Component, Fragment } from 'react';
+
+import {
+ EuiFormRow,
+ EuiFieldText,
+ EuiTextAlign,
+ EuiSpacer,
+ EuiButton,
+ EuiSelect,
+ EuiSelectOption,
+ EuiFormErrorText,
+} from '@elastic/eui';
+
+import { CombinedField } from './types';
+import {
+ createGeoPointCombinedField,
+ isWithinLatRange,
+ isWithinLonRange,
+ getFieldNames,
+ getNameCollisionMsg,
+} from './utils';
+import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
+
+interface Props {
+ addCombinedField: (combinedField: CombinedField) => void;
+ hasNameCollision: (name: string) => boolean;
+ results: FindFileStructureResponse;
+}
+
+interface State {
+ latField: string;
+ lonField: string;
+ geoPointField: string;
+ geoPointFieldError: string;
+ latFields: EuiSelectOption[];
+ lonFields: EuiSelectOption[];
+ submitError: string;
+}
+
+export class GeoPointForm extends Component {
+ constructor(props: Props) {
+ super(props);
+
+ const latFields: EuiSelectOption[] = [{ value: '', text: '' }];
+ const lonFields: EuiSelectOption[] = [{ value: '', text: '' }];
+ getFieldNames(props.results).forEach((columnName: string) => {
+ if (isWithinLatRange(columnName, props.results.field_stats)) {
+ latFields.push({ value: columnName, text: columnName });
+ }
+ if (isWithinLonRange(columnName, props.results.field_stats)) {
+ lonFields.push({ value: columnName, text: columnName });
+ }
+ });
+
+ this.state = {
+ latField: '',
+ lonField: '',
+ geoPointField: '',
+ geoPointFieldError: '',
+ submitError: '',
+ latFields,
+ lonFields,
+ };
+ }
+
+ onLatFieldChange = (e: ChangeEvent) => {
+ this.setState({ latField: e.target.value });
+ };
+
+ onLonFieldChange = (e: ChangeEvent) => {
+ this.setState({ lonField: e.target.value });
+ };
+
+ onGeoPointFieldChange = (e: ChangeEvent) => {
+ const geoPointField = e.target.value;
+ this.setState({ geoPointField });
+ this.hasNameCollision(geoPointField);
+ };
+
+ hasNameCollision = debounce((name: string) => {
+ try {
+ const geoPointFieldError = this.props.hasNameCollision(name) ? getNameCollisionMsg(name) : '';
+ this.setState({ geoPointFieldError });
+ } catch (error) {
+ this.setState({ submitError: error.message });
+ }
+ }, 200);
+
+ onSubmit = () => {
+ try {
+ this.props.addCombinedField(
+ createGeoPointCombinedField(
+ this.state.latField,
+ this.state.lonField,
+ this.state.geoPointField
+ )
+ );
+ this.setState({ submitError: '' });
+ } catch (error) {
+ this.setState({ submitError: error.message });
+ }
+ };
+
+ render() {
+ let error;
+ if (this.state.submitError) {
+ error = {this.state.submitError} ;
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {error}
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts
new file mode 100644
index 00000000000000..90b6bbab789f3b
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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 {
+ addCombinedFieldsToPipeline,
+ addCombinedFieldsToMappings,
+ getDefaultCombinedFields,
+} from './utils';
+
+export { CombinedFieldsReadOnlyForm } from './combined_fields_read_only_form';
+export { CombinedFieldsForm } from './combined_fields_form';
+export { CombinedField } from './types';
diff --git a/x-pack/legacy/plugins/spaces/server/lib/errors.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts
similarity index 57%
rename from x-pack/legacy/plugins/spaces/server/lib/errors.ts
rename to x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts
index 4d8d71dca7af67..1ec66f5c966610 100644
--- a/x-pack/legacy/plugins/spaces/server/lib/errors.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/types.ts
@@ -4,12 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { boomify, isBoom } from 'boom';
-
-export function wrapError(error: any) {
- if (isBoom(error)) {
- return error;
- }
-
- return boomify(error, { statusCode: error.status });
+export interface CombinedField {
+ mappingType: string;
+ delimiter: string;
+ combinedFieldName: string;
+ fieldNames: string[];
}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts
new file mode 100644
index 00000000000000..17b39f9041ec07
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.test.ts
@@ -0,0 +1,235 @@
+/*
+ * 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 {
+ addCombinedFieldsToMappings,
+ addCombinedFieldsToPipeline,
+ createGeoPointCombinedField,
+ isWithinLatRange,
+ isWithinLonRange,
+ removeCombinedFieldsFromMappings,
+ removeCombinedFieldsFromPipeline,
+} from './utils';
+
+const combinedFields = [createGeoPointCombinedField('lat', 'lon', 'location')];
+
+test('addCombinedFieldsToMappings', () => {
+ const mappings = {
+ _meta: {
+ created_by: '',
+ },
+ properties: {
+ lat: {
+ type: 'number',
+ },
+ lon: {
+ type: 'number',
+ },
+ },
+ };
+ expect(addCombinedFieldsToMappings(mappings, combinedFields)).toEqual({
+ _meta: {
+ created_by: '',
+ },
+ properties: {
+ lat: {
+ type: 'number',
+ },
+ lon: {
+ type: 'number',
+ },
+ location: {
+ type: 'geo_point',
+ },
+ },
+ });
+});
+
+test('removeCombinedFieldsFromMappings', () => {
+ const mappings = {
+ _meta: {
+ created_by: '',
+ },
+ properties: {
+ lat: {
+ type: 'number',
+ },
+ lon: {
+ type: 'number',
+ },
+ location: {
+ type: 'geo_point',
+ },
+ },
+ };
+ expect(removeCombinedFieldsFromMappings(mappings, combinedFields)).toEqual({
+ _meta: {
+ created_by: '',
+ },
+ properties: {
+ lat: {
+ type: 'number',
+ },
+ lon: {
+ type: 'number',
+ },
+ },
+ });
+});
+
+test('addCombinedFieldsToPipeline', () => {
+ const pipeline = {
+ description: '',
+ processors: [
+ {
+ set: {
+ field: 'anotherfield',
+ value: '{{value}}',
+ },
+ },
+ ],
+ };
+ expect(addCombinedFieldsToPipeline(pipeline, combinedFields)).toEqual({
+ description: '',
+ processors: [
+ {
+ set: {
+ field: 'anotherfield',
+ value: '{{value}}',
+ },
+ },
+ {
+ set: {
+ field: 'location',
+ value: '{{lat}},{{lon}}',
+ },
+ },
+ ],
+ });
+});
+
+test('removeCombinedFieldsFromPipeline', () => {
+ const pipeline = {
+ description: '',
+ processors: [
+ {
+ set: {
+ field: 'anotherfield',
+ value: '{{value}}',
+ },
+ },
+ {
+ set: {
+ field: 'location',
+ value: '{{lat}},{{lon}}',
+ },
+ },
+ ],
+ };
+ expect(removeCombinedFieldsFromPipeline(pipeline, combinedFields)).toEqual({
+ description: '',
+ processors: [
+ {
+ set: {
+ field: 'anotherfield',
+ value: '{{value}}',
+ },
+ },
+ ],
+ });
+});
+
+test('isWithinLatRange', () => {
+ expect(isWithinLatRange('fieldAlpha', {})).toBe(false);
+ expect(
+ isWithinLatRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 1 }],
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLatRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 100 }],
+ max_value: 100,
+ min_value: 0,
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLatRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: -100 }],
+ max_value: 0,
+ min_value: -100,
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLatRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 0 }],
+ max_value: 0,
+ min_value: 0,
+ },
+ })
+ ).toBe(true);
+});
+
+test('isWithinLonRange', () => {
+ expect(isWithinLonRange('fieldAlpha', {})).toBe(false);
+ expect(
+ isWithinLonRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 1 }],
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLonRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 200 }],
+ max_value: 200,
+ min_value: 0,
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLonRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: -200 }],
+ max_value: 0,
+ min_value: -200,
+ },
+ })
+ ).toBe(false);
+ expect(
+ isWithinLonRange('fieldAlpha', {
+ fieldAlpha: {
+ count: 1,
+ cardinality: 1,
+ top_hits: [{ count: 1, value: 0 }],
+ max_value: 0,
+ min_value: 0,
+ },
+ })
+ ).toBe(true);
+});
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts
new file mode 100644
index 00000000000000..5e7de14f451c20
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/combined_fields/utils.ts
@@ -0,0 +1,174 @@
+/*
+ * 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';
+import _ from 'lodash';
+import uuid from 'uuid/v4';
+import { CombinedField } from './types';
+import {
+ FindFileStructureResponse,
+ IngestPipeline,
+ Mappings,
+} from '../../../../../../common/types/file_datavisualizer';
+
+const COMMON_LAT_NAMES = ['latitude', 'lat'];
+const COMMON_LON_NAMES = ['longitude', 'long', 'lon'];
+
+export function getDefaultCombinedFields(results: FindFileStructureResponse) {
+ const combinedFields: CombinedField[] = [];
+ const geoPointField = getGeoPointField(results);
+ if (geoPointField) {
+ combinedFields.push(geoPointField);
+ }
+ return combinedFields;
+}
+
+export function addCombinedFieldsToMappings(
+ mappings: Mappings,
+ combinedFields: CombinedField[]
+): Mappings {
+ const updatedMappings = { ...mappings };
+ combinedFields.forEach((combinedField) => {
+ updatedMappings.properties[combinedField.combinedFieldName] = {
+ type: combinedField.mappingType,
+ };
+ });
+ return updatedMappings;
+}
+
+export function removeCombinedFieldsFromMappings(
+ mappings: Mappings,
+ combinedFields: CombinedField[]
+) {
+ const updatedMappings = { ...mappings };
+ combinedFields.forEach((combinedField) => {
+ delete updatedMappings.properties[combinedField.combinedFieldName];
+ });
+ return updatedMappings;
+}
+
+export function addCombinedFieldsToPipeline(
+ pipeline: IngestPipeline,
+ combinedFields: CombinedField[]
+) {
+ const updatedPipeline = _.cloneDeep(pipeline);
+ combinedFields.forEach((combinedField) => {
+ updatedPipeline.processors.push({
+ set: {
+ field: combinedField.combinedFieldName,
+ value: combinedField.fieldNames
+ .map((fieldName) => {
+ return `{{${fieldName}}}`;
+ })
+ .join(combinedField.delimiter),
+ },
+ });
+ });
+ return updatedPipeline;
+}
+
+export function removeCombinedFieldsFromPipeline(
+ pipeline: IngestPipeline,
+ combinedFields: CombinedField[]
+) {
+ return {
+ ...pipeline,
+ processors: pipeline.processors.filter((processor) => {
+ return 'set' in processor
+ ? !combinedFields.some((combinedField) => {
+ return processor.set.field === combinedField.combinedFieldName;
+ })
+ : true;
+ }),
+ };
+}
+
+export function isWithinLatRange(
+ fieldName: string,
+ fieldStats: FindFileStructureResponse['field_stats']
+) {
+ return (
+ fieldName in fieldStats &&
+ 'max_value' in fieldStats[fieldName] &&
+ fieldStats[fieldName]!.max_value! <= 90 &&
+ 'min_value' in fieldStats[fieldName] &&
+ fieldStats[fieldName]!.min_value! >= -90
+ );
+}
+
+export function isWithinLonRange(
+ fieldName: string,
+ fieldStats: FindFileStructureResponse['field_stats']
+) {
+ return (
+ fieldName in fieldStats &&
+ 'max_value' in fieldStats[fieldName] &&
+ fieldStats[fieldName]!.max_value! <= 180 &&
+ 'min_value' in fieldStats[fieldName] &&
+ fieldStats[fieldName]!.min_value! >= -180
+ );
+}
+
+export function createGeoPointCombinedField(
+ latField: string,
+ lonField: string,
+ geoPointField: string
+): CombinedField {
+ return {
+ mappingType: 'geo_point',
+ delimiter: ',',
+ combinedFieldName: geoPointField,
+ fieldNames: [latField, lonField],
+ };
+}
+
+export function getNameCollisionMsg(name: string) {
+ return i18n.translate('xpack.ml.fileDatavisualizer.nameCollisionMsg', {
+ defaultMessage: '"{name}" already exists, please provide a unique name',
+ values: { name },
+ });
+}
+
+export function getFieldNames(results: FindFileStructureResponse): string[] {
+ return results.column_names !== undefined
+ ? results.column_names
+ : Object.keys(results.field_stats);
+}
+
+function getGeoPointField(results: FindFileStructureResponse) {
+ const fieldNames = getFieldNames(results);
+
+ const latField = fieldNames.find((columnName) => {
+ return (
+ COMMON_LAT_NAMES.includes(columnName.toLowerCase()) &&
+ isWithinLatRange(columnName, results.field_stats)
+ );
+ });
+
+ const lonField = fieldNames.find((columnName) => {
+ return (
+ COMMON_LON_NAMES.includes(columnName.toLowerCase()) &&
+ isWithinLonRange(columnName, results.field_stats)
+ );
+ });
+
+ if (!latField || !lonField) {
+ return null;
+ }
+
+ const combinedFieldNames = [
+ 'location',
+ 'point_location',
+ `${latField}_${lonField}`,
+ `location_${uuid()}`,
+ ];
+ // Use first combinedFieldNames that does not have a naming collision
+ const geoPointField = combinedFieldNames.find((name) => {
+ return !fieldNames.includes(name);
+ });
+
+ return geoPointField ? createGeoPointCombinedField(latField, lonField, geoPointField) : null;
+}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx
index a79a7d36f32945..2b49746170f46c 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/advanced.tsx
@@ -17,7 +17,9 @@ import {
EuiFlexItem,
} from '@elastic/eui';
+import { CombinedField, CombinedFieldsForm } from '../combined_fields';
import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
+import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
const EDITOR_HEIGHT = '300px';
interface Props {
@@ -36,6 +38,9 @@ interface Props {
onPipelineStringChange(): void;
indexNameError: string;
indexPatternNameError: string;
+ combinedFields: CombinedField[];
+ onCombinedFieldsChange(combinedFields: CombinedField[]): void;
+ results: FindFileStructureResponse;
}
export const AdvancedSettings: FC = ({
@@ -54,6 +59,9 @@ export const AdvancedSettings: FC = ({
onPipelineStringChange,
indexNameError,
indexPatternNameError,
+ combinedFields,
+ onCombinedFieldsChange,
+ results,
}) => {
return (
@@ -123,6 +131,17 @@ export const AdvancedSettings: FC = ({
/>
+
+
= ({
@@ -46,6 +51,9 @@ export const ImportSettings: FC = ({
onPipelineStringChange,
indexNameError,
indexPatternNameError,
+ combinedFields,
+ onCombinedFieldsChange,
+ results,
}) => {
const tabs = [
{
@@ -64,6 +72,7 @@ export const ImportSettings: FC = ({
createIndexPattern={createIndexPattern}
onCreateIndexPatternChange={onCreateIndexPatternChange}
indexNameError={indexNameError}
+ combinedFields={combinedFields}
/>
),
@@ -93,6 +102,9 @@ export const ImportSettings: FC = ({
onPipelineStringChange={onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
+ combinedFields={combinedFields}
+ onCombinedFieldsChange={onCombinedFieldsChange}
+ results={results}
/>
),
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx
index 1e716824729e33..f6cd5909cbb802 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_settings/simple.tsx
@@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC } from 'react';
import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui';
+import { CombinedField, CombinedFieldsReadOnlyForm } from '../combined_fields';
interface Props {
index: string;
@@ -17,6 +18,7 @@ interface Props {
createIndexPattern: boolean;
onCreateIndexPatternChange(): void;
indexNameError: string;
+ combinedFields: CombinedField[];
}
export const SimpleSettings: FC = ({
@@ -26,6 +28,7 @@ export const SimpleSettings: FC = ({
createIndexPattern,
onCreateIndexPatternChange,
indexNameError,
+ combinedFields,
}) => {
return (
@@ -75,6 +78,10 @@ export const SimpleSettings: FC = ({
onChange={onCreateIndexPatternChange}
data-test-subj="mlFileDataVisCreateIndexPatternCheckbox"
/>
+
+
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js
index 36b77a5a25e091..08b61a5fa4eed2 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js
+++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js
@@ -26,6 +26,11 @@ import { ImportProgress, IMPORT_STATUS } from '../import_progress';
import { ImportErrors } from '../import_errors';
import { ImportSummary } from '../import_summary';
import { ImportSettings } from '../import_settings';
+import {
+ addCombinedFieldsToPipeline,
+ addCombinedFieldsToMappings,
+ getDefaultCombinedFields,
+} from '../combined_fields';
import { ExperimentalBadge } from '../experimental_badge';
import { getIndexPatternNames, loadIndexPatterns } from '../../../../util/index_utils';
import { ml } from '../../../../services/ml_api_service';
@@ -68,6 +73,7 @@ const DEFAULT_STATE = {
timeFieldName: undefined,
isFilebeatFlyoutVisible: false,
checkingValidIndex: false,
+ combinedFields: [],
};
export class ImportView extends Component {
@@ -386,6 +392,10 @@ export class ImportView extends Component {
});
};
+ onCombinedFieldsChange = (combinedFields) => {
+ this.setState({ combinedFields });
+ };
+
setImportProgress = (progress) => {
this.setState({
uploadProgress: progress,
@@ -444,6 +454,7 @@ export class ImportView extends Component {
timeFieldName,
isFilebeatFlyoutVisible,
checkingValidIndex,
+ combinedFields,
} = this.state;
const createPipeline = pipelineString !== '';
@@ -513,6 +524,9 @@ export class ImportView extends Component {
onPipelineStringChange={this.onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
+ combinedFields={combinedFields}
+ onCombinedFieldsChange={this.onCombinedFieldsChange}
+ results={this.props.results}
/>
@@ -644,12 +658,22 @@ function getDefaultState(state, results) {
? JSON.stringify(DEFAULT_INDEX_SETTINGS, null, 2)
: state.indexSettingsString;
+ const combinedFields = state.combinedFields.length
+ ? state.combinedFields
+ : getDefaultCombinedFields(results);
+
const mappingsString =
- state.mappingsString === '' ? JSON.stringify(results.mappings, null, 2) : state.mappingsString;
+ state.mappingsString === ''
+ ? JSON.stringify(addCombinedFieldsToMappings(results.mappings, combinedFields), null, 2)
+ : state.mappingsString;
const pipelineString =
state.pipelineString === '' && results.ingest_pipeline !== undefined
- ? JSON.stringify(results.ingest_pipeline, null, 2)
+ ? JSON.stringify(
+ addCombinedFieldsToPipeline(results.ingest_pipeline, combinedFields),
+ null,
+ 2
+ )
: state.pipelineString;
const timeFieldName = results.timestamp_field;
@@ -660,6 +684,7 @@ function getDefaultState(state, results) {
mappingsString,
pipelineString,
timeFieldName,
+ combinedFields,
};
}
diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts
index d8e0c7274b5495..8feef489fdde1b 100644
--- a/x-pack/plugins/ml/public/plugin.ts
+++ b/x-pack/plugins/ml/public/plugin.ts
@@ -106,12 +106,17 @@ export class MlPlugin implements Plugin {
const licensing = pluginsSetup.licensing.license$.pipe(take(1));
licensing.subscribe(async (license) => {
const [coreStart] = await core.getStartServices();
+
if (isMlEnabled(license)) {
// add ML to home page
if (pluginsSetup.home) {
registerFeature(pluginsSetup.home);
}
+ // the mlUrlGenerator should be registered even without full license
+ // for other plugins to access ML links
+ registerUrlGenerator(pluginsSetup.share, core);
+
const { capabilities } = coreStart.application;
// register ML for the index pattern management no data screen.
@@ -129,7 +134,6 @@ export class MlPlugin implements Plugin {
}
registerEmbeddables(pluginsSetup.embeddable, core);
registerMlUiActions(pluginsSetup.uiActions, core);
- registerUrlGenerator(pluginsSetup.share, core);
} else if (managementApp) {
managementApp.disable();
}
diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts
index cf248fcc608965..7224eacf84e902 100644
--- a/x-pack/plugins/ml/server/plugin.ts
+++ b/x-pack/plugins/ml/server/plugin.ts
@@ -15,6 +15,7 @@ import {
CapabilitiesStart,
IClusterClient,
} from 'kibana/server';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PluginsSetup, RouteInitialization } from './types';
import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app';
import { MlCapabilities } from '../common/types/capabilities';
@@ -74,6 +75,7 @@ export class MlServerPlugin implements Plugin;
+ router: IRouter;
features: FeaturesPluginSetup;
elasticsearch: ElasticsearchServiceSetup;
licensing: LicensingPluginSetup;
- basePath: BasePath['get'];
- router: IRouter;
security?: SecurityPluginSetup;
+ spaces?: SpacesPluginSetup;
}
export interface ReportingInternalStart {
@@ -50,7 +54,7 @@ export class ReportingCore {
private exportTypesRegistry = getExportTypesRegistry();
private config?: ReportingConfig;
- constructor() {}
+ constructor(private logger: LevelLogger) {}
/*
* Register setupDeps
@@ -180,9 +184,9 @@ export class ReportingCore {
return this.getPluginSetupDeps().elasticsearch;
}
- public async getSavedObjectsClient(fakeRequest: KibanaRequest) {
+ private async getSavedObjectsClient(request: KibanaRequest) {
const { savedObjects } = await this.getPluginStartDeps();
- return savedObjects.getScopedClient(fakeRequest) as SavedObjectsClientContract;
+ return savedObjects.getScopedClient(request) as SavedObjectsClientContract;
}
public async getUiSettingsServiceFactory(savedObjectsClient: SavedObjectsClientContract) {
@@ -190,4 +194,48 @@ export class ReportingCore {
const scopedUiSettingsService = uiSettingsService.asScopedToClient(savedObjectsClient);
return scopedUiSettingsService;
}
+
+ public getSpaceId(request: KibanaRequest): string | undefined {
+ const spacesService = this.getPluginSetupDeps().spaces?.spacesService;
+ if (spacesService) {
+ const spaceId = spacesService?.getSpaceId(request);
+
+ if (spaceId !== DEFAULT_SPACE_ID) {
+ this.logger.info(`Request uses Space ID: ` + spaceId);
+ return spaceId;
+ } else {
+ this.logger.info(`Request uses default Space`);
+ }
+ }
+ }
+
+ public getFakeRequest(baseRequest: object, spaceId?: string) {
+ const fakeRequest = KibanaRequest.from({
+ path: '/',
+ route: { settings: {} },
+ url: { href: '/' },
+ raw: { req: { url: '/' } },
+ ...baseRequest,
+ } as Hapi.Request);
+
+ const spacesService = this.getPluginSetupDeps().spaces?.spacesService;
+ if (spacesService) {
+ if (spaceId && spaceId !== DEFAULT_SPACE_ID) {
+ this.logger.info(`Generating request for space: ` + spaceId);
+ this.getPluginSetupDeps().basePath.set(fakeRequest, `/s/${spaceId}`);
+ }
+ }
+
+ return fakeRequest;
+ }
+
+ public async getUiSettingsClient(request: KibanaRequest) {
+ const spacesService = this.getPluginSetupDeps().spaces?.spacesService;
+ const spaceId = this.getSpaceId(request);
+ if (spacesService && spaceId) {
+ this.logger.info(`Creating UI Settings Client for space: ${spaceId}`);
+ }
+ const savedObjectsClient = await this.getSavedObjectsClient(request);
+ return await this.getUiSettingsServiceFactory(savedObjectsClient);
+ }
}
diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts
index 5ab029bfd9f29c..4f0088467dd689 100644
--- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts
@@ -11,7 +11,6 @@ interface HasEncryptedHeaders {
headers?: string;
}
-// TODO merge functionality with CSV execute job
export const decryptJobHeaders = async <
JobParamsType,
TaskPayloadType extends HasEncryptedHeaders
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts
index cb792fbd6ae039..0b06beabfd24dc 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.test.ts
@@ -7,7 +7,7 @@
import { getAbsoluteUrlFactory } from './get_absolute_url';
const defaultOptions = {
- defaultBasePath: 'sbp',
+ basePath: 'sbp',
protocol: 'http:',
hostname: 'localhost',
port: 5601,
@@ -64,8 +64,8 @@ test(`uses the provided hash with queryString`, () => {
});
test(`uses the provided basePath`, () => {
- const getAbsoluteUrl = getAbsoluteUrlFactory(defaultOptions);
- const absoluteUrl = getAbsoluteUrl({ basePath: '/s/marketing' });
+ const getAbsoluteUrl = getAbsoluteUrlFactory({ ...defaultOptions, basePath: '/s/marketing' });
+ const absoluteUrl = getAbsoluteUrl();
expect(absoluteUrl).toBe(`http://localhost:5601/s/marketing/app/kibana`);
});
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts
index f996a49e5eadca..72305f47e71897 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_absolute_url.ts
@@ -7,7 +7,7 @@
import url from 'url';
interface AbsoluteURLFactoryOptions {
- defaultBasePath: string;
+ basePath: string;
protocol: string;
hostname: string;
port: string | number;
@@ -17,14 +17,9 @@ export const getAbsoluteUrlFactory = ({
protocol,
hostname,
port,
- defaultBasePath,
+ basePath,
}: AbsoluteURLFactoryOptions) => {
- return function getAbsoluteUrl({
- basePath = defaultBasePath,
- hash = '',
- path = '/app/kibana',
- search = '',
- } = {}) {
+ return function getAbsoluteUrl({ hash = '', path = '/app/kibana', search = '' } = {}) {
return url.format({
protocol,
hostname,
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
index a0d8ff08525447..794ea9febb5c08 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
@@ -5,24 +5,16 @@
*/
import { ReportingConfig } from '../../';
-import { ReportingCore } from '../../core';
-import {
- createMockConfig,
- createMockConfigSchema,
- createMockReportingCore,
-} from '../../test_helpers';
+import { createMockConfig, createMockConfigSchema } from '../../test_helpers';
import { BasePayload } from '../../types';
-import { TaskPayloadPDF } from '../printable_pdf/types';
-import { getConditionalHeaders, getCustomLogo } from './';
+import { getConditionalHeaders } from './';
let mockConfig: ReportingConfig;
-let mockReportingPlugin: ReportingCore;
beforeEach(async () => {
const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } };
const mockSchema = createMockConfigSchema(reportingConfig);
mockConfig = createMockConfig(mockSchema);
- mockReportingPlugin = await createMockReportingCore(mockConfig);
});
describe('conditions', () => {
@@ -32,7 +24,7 @@ describe('conditions', () => {
baz: 'quix',
};
- const conditionalHeaders = await getConditionalHeaders({
+ const conditionalHeaders = getConditionalHeaders({
job: {} as BasePayload,
filteredHeaders: permittedHeaders,
config: mockConfig,
@@ -51,83 +43,6 @@ describe('conditions', () => {
});
});
-test('uses basePath from job when creating saved object service', async () => {
- const mockGetSavedObjectsClient = jest.fn();
- mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient;
-
- const permittedHeaders = {
- foo: 'bar',
- baz: 'quix',
- };
- const conditionalHeaders = await getConditionalHeaders({
- job: {} as BasePayload,
- filteredHeaders: permittedHeaders,
- config: mockConfig,
- });
- const jobBasePath = '/sbp/s/marketing';
- await getCustomLogo({
- reporting: mockReportingPlugin,
- job: { basePath: jobBasePath } as TaskPayloadPDF,
- conditionalHeaders,
- config: mockConfig,
- });
-
- const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath;
- expect(getBasePath()).toBe(jobBasePath);
-});
-
-test(`uses basePath from server if job doesn't have a basePath when creating saved object service`, async () => {
- const mockGetSavedObjectsClient = jest.fn();
- mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient;
-
- const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } };
- const mockSchema = createMockConfigSchema(reportingConfig);
- mockConfig = createMockConfig(mockSchema);
-
- const permittedHeaders = {
- foo: 'bar',
- baz: 'quix',
- };
- const conditionalHeaders = await getConditionalHeaders({
- job: {} as BasePayload,
- filteredHeaders: permittedHeaders,
- config: mockConfig,
- });
-
- await getCustomLogo({
- reporting: mockReportingPlugin,
- job: {} as TaskPayloadPDF,
- conditionalHeaders,
- config: mockConfig,
- });
-
- const getBasePath = mockGetSavedObjectsClient.mock.calls[0][0].getBasePath;
- expect(getBasePath()).toBe(`/sbp`);
- expect(mockGetSavedObjectsClient.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "getBasePath": [Function],
- "headers": Object {
- "baz": "quix",
- "foo": "bar",
- },
- "path": "/",
- "raw": Object {
- "req": Object {
- "url": "/",
- },
- },
- "route": Object {
- "settings": Object {},
- },
- "url": Object {
- "href": "/",
- },
- },
- ]
- `);
-});
-
describe('config formatting', () => {
test(`lowercases kibanaServer.hostname`, async () => {
const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } };
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts
deleted file mode 100644
index ee61d76c8a9330..00000000000000
--- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts
+++ /dev/null
@@ -1,40 +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 { ReportingConfig, ReportingCore } from '../../';
-import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants';
-import { ConditionalHeaders } from '../../types';
-import { TaskPayloadPDF } from '../printable_pdf/types'; // Logo is PDF only
-
-export const getCustomLogo = async ({
- reporting,
- config,
- job,
- conditionalHeaders,
-}: {
- reporting: ReportingCore;
- config: ReportingConfig;
- job: TaskPayloadPDF;
- conditionalHeaders: ConditionalHeaders;
-}) => {
- const serverBasePath: string = config.kbnConfig.get('server', 'basePath');
- const fakeRequest: any = {
- headers: conditionalHeaders.headers,
- // This is used by the spaces SavedObjectClientWrapper to determine the existing space.
- // We use the basePath from the saved job, which we'll have post spaces being implemented;
- // or we use the server base path, which uses the default space
- getBasePath: () => job.basePath || serverBasePath,
- path: '/',
- route: { settings: {} },
- url: { href: '/' },
- raw: { req: { url: '/' } },
- };
-
- const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest);
- const uiSettings = await reporting.getUiSettingsServiceFactory(savedObjectsClient);
- const logo: string = await uiSettings.get(UI_SETTINGS_CUSTOM_PDF_LOGO);
- return { conditionalHeaders, logo };
-};
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts
index d6f472e18bc7b2..f4e3a7b723c088 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts
@@ -36,12 +36,7 @@ export function getFullUrls({
config.get('kibanaServer', 'hostname'),
config.get('kibanaServer', 'port'),
] as string[];
- const getAbsoluteUrl = getAbsoluteUrlFactory({
- defaultBasePath: basePath,
- protocol,
- hostname,
- port,
- });
+ const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port });
// PDF and PNG job params put in the url differently
let relativeUrls: string[] = [];
@@ -61,7 +56,6 @@ export function getFullUrls({
const urls = relativeUrls.map((relativeUrl) => {
const parsedRelative: UrlWithStringQuery = urlParse(relativeUrl);
const jobUrl = getAbsoluteUrl({
- basePath: job.basePath,
path: parsedRelative.pathname,
hash: parsedRelative.hash,
search: parsedRelative.search,
diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts
index e0d03eb4864cad..80eaa52d0951b6 100644
--- a/x-pack/plugins/reporting/server/export_types/common/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/index.ts
@@ -6,7 +6,6 @@
export { decryptJobHeaders } from './decrypt_job_headers';
export { getConditionalHeaders } from './get_conditional_headers';
-export { getCustomLogo } from './get_custom_logo';
export { getFullUrls } from './get_full_urls';
export { omitBlockedHeaders } from './omit_blocked_headers';
export { validateUrls } from './validate_urls';
diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts
index be18bd7fff361d..d768dc6f8e0843 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts
@@ -25,6 +25,7 @@ export const createJobFnFactory: CreateJobFnFactory {
- const decryptHeaders = async () => {
- try {
- if (typeof headers !== 'string') {
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage',
- {
- defaultMessage: 'Job headers are missing',
- }
- )
- );
- }
- return await crypto.decrypt(headers);
- } catch (err) {
- logger.error(err);
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage',
- {
- defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}',
- values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() },
- }
- )
- ); // prettier-ignore
- }
- };
-
- return KibanaRequest.from({
- headers: await decryptHeaders(),
- // This is used by the spaces SavedObjectClientWrapper to determine the existing space.
- // We use the basePath from the saved job, which we'll have post spaces being implemented;
- // or we use the server base path, which uses the default space
- path: '/',
- route: { settings: {} },
- url: { href: '/' },
- app: {},
- raw: { req: { url: '/' } },
- } as Hapi.Request);
-};
-
export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) {
const config = reporting.getConfig();
- const crypto = cryptoFactory(config.get('encryptionKey'));
const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']);
return async function runTask(jobId, job, cancellationToken) {
@@ -67,16 +21,15 @@ export const runTaskFnFactory: RunTaskFnFactory
callAsCurrentUser(endpoint, clientParams, options);
- const savedObjectsClient = await reporting.getSavedObjectsClient(fakeRequest);
- const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient);
-
const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv(
job,
config,
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts
index 915d5010a4885d..1f3354debc3053 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/get_ui_settings.ts
@@ -6,6 +6,10 @@
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/server';
+import {
+ UI_SETTINGS_CSV_QUOTE_VALUES,
+ UI_SETTINGS_CSV_SEPARATOR,
+} from '../../../../common/constants';
import { ReportingConfig } from '../../../';
import { LevelLogger } from '../../../lib';
@@ -38,8 +42,8 @@ export const getUiSettings = async (
// Separator, QuoteValues
const [separator, quoteValues] = await Promise.all([
- client.get('csv:separator'),
- client.get('csv:quoteValues'),
+ client.get(UI_SETTINGS_CSV_SEPARATOR),
+ client.get(UI_SETTINGS_CSV_QUOTE_VALUES),
]);
return {
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
index e383f21143149c..6ecddae12a9889 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
@@ -37,9 +37,7 @@ interface SearchRequest {
}
export interface GenerateCsvParams {
- jobParams: {
- browserTimezone: string;
- };
+ browserTimezone: string;
searchRequest: SearchRequest;
indexPatternSavedObject: IndexPatternSavedObject;
fields: string[];
@@ -57,12 +55,7 @@ export function createGenerateCsv(logger: LevelLogger) {
callEndpoint: EndpointCaller,
cancellationToken: CancellationToken
): Promise {
- const settings = await getUiSettings(
- job.jobParams?.browserTimezone,
- uiSettingsClient,
- config,
- logger
- );
+ const settings = await getUiSettings(job.browserTimezone, uiSettingsClient, config, logger);
const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues);
const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : '';
const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom);
diff --git a/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts b/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.ts
deleted file mode 100644
index 09e6becc2baec0..00000000000000
--- a/x-pack/plugins/reporting/server/export_types/csv/lib/get_request.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 { Crypto } from '@elastic/node-crypto';
-import { i18n } from '@kbn/i18n';
-import Hapi from 'hapi';
-import { KibanaRequest } from '../../../../../../../src/core/server';
-import { LevelLogger } from '../../../lib';
-
-export const getRequest = async (
- headers: string | undefined,
- crypto: Crypto,
- logger: LevelLogger
-) => {
- const decryptHeaders = async () => {
- try {
- if (typeof headers !== 'string') {
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage',
- {
- defaultMessage: 'Job headers are missing',
- }
- )
- );
- }
- return await crypto.decrypt(headers);
- } catch (err) {
- logger.error(err);
- throw new Error(
- i18n.translate(
- 'xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage',
- {
- defaultMessage: 'Failed to decrypt report job data. Please ensure that {encryptionKey} is set and re-generate this report. {err}',
- values: { encryptionKey: 'xpack.reporting.encryptionKey', err: err.toString() },
- }
- )
- ); // prettier-ignore
- }
- };
-
- return KibanaRequest.from({
- headers: await decryptHeaders(),
- // This is used by the spaces SavedObjectClientWrapper to determine the existing space.
- // We use the basePath from the saved job, which we'll have post spaces being implemented;
- // or we use the server base path, which uses the default space
- path: '/',
- route: { settings: {} },
- url: { href: '/' },
- raw: { req: { url: '/' } },
- } as Hapi.Request);
-};
diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts
index f420d8b033170d..214157db51cb79 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts
@@ -29,6 +29,7 @@ export interface IndexPatternSavedObject {
}
export interface JobParamsDiscoverCsv extends BaseParams {
+ browserTimezone: string;
indexPatternId: string;
title: string;
searchRequest: SearchRequest;
@@ -38,6 +39,7 @@ export interface JobParamsDiscoverCsv extends BaseParams {
}
export interface TaskPayloadCSV extends BasePayload {
+ browserTimezone: string;
basePath: string;
searchRequest: any;
fields: any;
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts
index 3a5deda176b8c1..0ca80581fcc83d 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts
@@ -48,9 +48,8 @@ export const runTaskFnFactory: RunTaskFnFactory = function e
jobLogger.debug(`Execute job generating [${visType}] csv`);
const savedObjectsClient = context.core.savedObjects.client;
-
- const uiConfig = await reporting.getUiSettingsServiceFactory(savedObjectsClient);
- const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiConfig);
+ const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient);
+ const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiSettingsClient);
const elasticsearch = reporting.getElasticsearchService();
const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req);
@@ -58,7 +57,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e
const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv(
job,
config,
- uiConfig,
+ uiSettingsClient,
callAsCurrentUser,
new CancellationToken() // can not be cancelled
);
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts
index 9646d7eecd5b56..b387245406fbbf 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts
@@ -45,6 +45,7 @@ describe('Get CSV Job', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
+ "browserTimezone": "PST",
"conflictedTypesFields": Array [],
"fields": Array [],
"indexPatternSavedObject": Object {
@@ -57,9 +58,6 @@ describe('Get CSV Job', () => {
"timeFieldName": null,
"title": null,
},
- "jobParams": Object {
- "browserTimezone": "PST",
- },
"metaFields": Array [],
"searchRequest": Object {
"body": Object {
@@ -99,6 +97,7 @@ describe('Get CSV Job', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
+ "browserTimezone": "PST",
"conflictedTypesFields": Array [],
"fields": Array [],
"indexPatternSavedObject": Object {
@@ -111,9 +110,6 @@ describe('Get CSV Job', () => {
"timeFieldName": null,
"title": null,
},
- "jobParams": Object {
- "browserTimezone": "PST",
- },
"metaFields": Array [],
"searchRequest": Object {
"body": Object {
@@ -156,6 +152,7 @@ describe('Get CSV Job', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
+ "browserTimezone": "Africa/Timbuktu",
"conflictedTypesFields": Array [],
"fields": Array [],
"indexPatternSavedObject": Object {
@@ -168,9 +165,6 @@ describe('Get CSV Job', () => {
"timeFieldName": null,
"title": null,
},
- "jobParams": Object {
- "browserTimezone": "Africa/Timbuktu",
- },
"metaFields": Array [],
"searchRequest": Object {
"body": Object {
@@ -212,6 +206,7 @@ describe('Get CSV Job', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
+ "browserTimezone": "Africa/Timbuktu",
"conflictedTypesFields": Array [],
"fields": Array [
"@test_time",
@@ -226,9 +221,6 @@ describe('Get CSV Job', () => {
"timeFieldName": "@test_time",
"title": "test search",
},
- "jobParams": Object {
- "browserTimezone": "Africa/Timbuktu",
- },
"metaFields": Array [],
"searchRequest": Object {
"body": Object {
@@ -286,6 +278,7 @@ describe('Get CSV Job', () => {
);
expect(result).toMatchInlineSnapshot(`
Object {
+ "browserTimezone": "Africa/Timbuktu",
"conflictedTypesFields": Array [],
"fields": Array [
"@test_time",
@@ -300,9 +293,6 @@ describe('Get CSV Job', () => {
"timeFieldName": "@test_time",
"title": "test search",
},
- "jobParams": Object {
- "browserTimezone": "Africa/Timbuktu",
- },
"metaFields": Array [],
"searchRequest": Object {
"body": Object {
diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts
index 0fc29c5b208d9a..26a4b17aaf71fe 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts
@@ -12,6 +12,8 @@ import {
IIndexPattern,
Query,
} from '../../../../../../../src/plugins/data/server';
+import { TimeRangeParams } from '../../../types';
+import { GenerateCsvParams } from '../../csv/generate_csv';
import {
DocValueFields,
IndexPatternField,
@@ -23,7 +25,6 @@ import {
} from '../types';
import { getDataSource } from './get_data_source';
import { getFilters } from './get_filters';
-import { GenerateCsvParams } from '../../csv/generate_csv';
export const getEsQueryConfig = async (config: IUiSettingsClient) => {
const configs = await Promise.all([
@@ -49,7 +50,7 @@ export const getGenerateCsvParams = async (
savedObjectsClient: SavedObjectsClientContract,
uiConfig: IUiSettingsClient
): Promise => {
- let timerange;
+ let timerange: TimeRangeParams;
if (jobParams.post?.timerange) {
timerange = jobParams.post?.timerange;
} else {
@@ -136,7 +137,7 @@ export const getGenerateCsvParams = async (
};
return {
- jobParams: { browserTimezone: timerange.timezone },
+ browserTimezone: timerange.timezone,
indexPatternSavedObject,
searchRequest,
fields: includes,
diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
index 173a67ad18edf7..3727b2ec7b432b 100644
--- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts
@@ -25,13 +25,13 @@ export const createJobFnFactory: CreateJobFnFactory {
- basePath?: string;
browserTimezone: string;
forceNow?: string;
layout: LayoutParams;
diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts
index 96e634337e6a94..cae706a479b7f1 100644
--- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts
@@ -25,10 +25,10 @@ export const createJobFnFactory: CreateJobFnFactory>;
@@ -42,9 +42,7 @@ export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFac
mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })),
map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })),
map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })),
- mergeMap((conditionalHeaders) =>
- getCustomLogo({ reporting, config, job, conditionalHeaders })
- ),
+ mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)),
mergeMap(({ logo, conditionalHeaders }) => {
const urls = getFullUrls({ config, job });
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts
similarity index 74%
rename from x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts
rename to x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts
index ec4e54632eef55..8fa8fa5cbe3cba 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts
@@ -4,14 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ReportingConfig, ReportingCore } from '../../';
+import { ReportingConfig, ReportingCore } from '../../../';
import {
createMockConfig,
createMockConfigSchema,
createMockReportingCore,
-} from '../../test_helpers';
-import { TaskPayloadPDF } from '../printable_pdf/types';
-import { getConditionalHeaders, getCustomLogo } from './';
+} from '../../../test_helpers';
+import { getConditionalHeaders } from '../../common';
+import { TaskPayloadPDF } from '../types';
+import { getCustomLogo } from './get_custom_logo';
let mockConfig: ReportingConfig;
let mockReportingPlugin: ReportingCore;
@@ -38,18 +39,13 @@ test(`gets logo from uiSettings`, async () => {
get: mockGet,
});
- const conditionalHeaders = await getConditionalHeaders({
+ const conditionalHeaders = getConditionalHeaders({
job: {} as TaskPayloadPDF,
filteredHeaders: permittedHeaders,
config: mockConfig,
});
- const { logo } = await getCustomLogo({
- reporting: mockReportingPlugin,
- config: mockConfig,
- job: {} as TaskPayloadPDF,
- conditionalHeaders,
- });
+ const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders);
expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo');
expect(logo).toBe('purple pony');
diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts
new file mode 100644
index 00000000000000..35ab7001ecbe48
--- /dev/null
+++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 { ReportingCore } from '../../../';
+import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants';
+import { ConditionalHeaders } from '../../../types';
+
+export const getCustomLogo = async (
+ reporting: ReportingCore,
+ conditionalHeaders: ConditionalHeaders,
+ spaceId?: string
+) => {
+ const fakeRequest = reporting.getFakeRequest({ headers: conditionalHeaders.headers }, spaceId);
+ const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest);
+
+ const logo: string = await uiSettingsClient.get(UI_SETTINGS_CUSTOM_PDF_LOGO);
+
+ // continue the pipeline
+ return { conditionalHeaders, logo };
+};
diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts
index 3020cbb5f28b00..7fd176e71f2d58 100644
--- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts
+++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts
@@ -16,7 +16,6 @@ export interface JobParamsPDF extends BaseParams {
// Job payload: structure of stored job data provided by create_job
export interface TaskPayloadPDF extends BasePayload {
- basePath?: string;
browserTimezone: string;
forceNow?: string;
layout: LayoutParams;
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index 0aae8b567bcdb1..03d88ca60e2c0a 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -12,6 +12,7 @@ import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../type
import { indexTimestamp } from './index_timestamp';
import { mapping } from './mapping';
import { Report } from './report';
+
interface JobSettings {
timeout: number;
browser_type: string;
diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts
index adb89abe202805..6a93a35bfcc845 100644
--- a/x-pack/plugins/reporting/server/plugin.ts
+++ b/x-pack/plugins/reporting/server/plugin.ts
@@ -34,7 +34,7 @@ export class ReportingPlugin
constructor(context: PluginInitializerContext) {
this.logger = new LevelLogger(context.logger.get());
this.initializerContext = context;
- this.reportingCore = new ReportingCore();
+ this.reportingCore = new ReportingCore(this.logger);
}
public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
@@ -70,11 +70,11 @@ export class ReportingPlugin
});
const { elasticsearch, http } = core;
- const { features, licensing, security } = plugins;
+ const { features, licensing, security, spaces } = plugins;
const { initializerContext: initContext, reportingCore } = this;
const router = http.createRouter();
- const basePath = http.basePath.get;
+ const basePath = http.basePath;
reportingCore.pluginSetup({
features,
@@ -83,6 +83,7 @@ export class ReportingPlugin
basePath,
router,
security,
+ spaces,
});
registerReportingUsageCollector(reportingCore, plugins);
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts
index 979283f9f037c3..0acf384869dedb 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts
@@ -35,19 +35,8 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
config.get('kibanaServer', 'port'),
] as string[];
- const getAbsoluteUrl = getAbsoluteUrlFactory({
- defaultBasePath: basePath,
- protocol,
- hostname,
- port,
- });
-
- const hashUrl = getAbsoluteUrl({
- basePath,
- path: '/',
- hash: '',
- search: '',
- });
+ const getAbsoluteUrl = getAbsoluteUrlFactory({ basePath, protocol, hostname, port });
+ const hashUrl = getAbsoluteUrl({ path: '/', hash: '', search: '' });
// Hack the layout to make the base/login page work
const layout = {
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
index 6ec35db5caec66..72772f9f7b755d 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
@@ -37,18 +37,19 @@ const createMockPluginSetup = (
return {
features: featuresPluginMock.createSetup(),
elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } },
- basePath: setupMock.basePath || '/all-about-that-basepath',
+ basePath: { set: jest.fn() },
router: setupMock.router,
security: setupMock.security,
licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any,
};
};
+const logger = createMockLevelLogger();
+
const createMockPluginStart = (
mockReportingCore: ReportingCore,
startMock?: any
): ReportingInternalStart => {
- const logger = createMockLevelLogger();
const store = new ReportingStore(mockReportingCore, logger);
return {
browserDriverFactory: startMock.browserDriverFactory,
@@ -134,7 +135,7 @@ export const createMockReportingCore = async (
}
config = config || {};
- const core = new ReportingCore();
+ const core = new ReportingCore(logger);
core.pluginSetup(setupDepsMock);
core.setConfig(config);
diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts
index c67a95c2de754f..a3c63a0fb539d5 100644
--- a/x-pack/plugins/reporting/server/types.ts
+++ b/x-pack/plugins/reporting/server/types.ts
@@ -8,6 +8,7 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DataPluginStart } from 'src/plugins/data/server/plugin';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { SpacesPluginSetup } from '../../spaces/server';
import { CancellationToken } from '../../../plugins/reporting/common';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { LicensingPluginSetup } from '../../licensing/server';
@@ -53,6 +54,7 @@ export interface BasePayload {
jobParams: JobParamsType;
title: string;
type: string;
+ spaceId?: string;
}
export interface JobSource {
@@ -95,6 +97,7 @@ export interface ReportingSetupDeps {
licensing: LicensingPluginSetup;
features: FeaturesPluginSetup;
security?: SecurityPluginSetup;
+ spaces?: SpacesPluginSetup;
usageCollection?: UsageCollectionSetup;
}
@@ -121,7 +124,6 @@ export interface BaseParams {
}
export interface BaseParamsEncryptedFields extends BaseParams {
- basePath?: string; // for screenshot type reports
headers: string; // encrypted headers
}
diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
index 2b78355787ff23..1bab51e70a4947 100644
--- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
+++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts
@@ -21,6 +21,7 @@ export const createFeature = (
icon: 'discoverApp',
navLinkId: 'discover',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: [],
privileges:
privileges === null
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
index bf791b37087bda..7dff2912e6aa3d 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx
@@ -32,6 +32,7 @@ const buildFeatures = () => {
name: 'Feature 1',
icon: 'addDataApp',
app: ['feature1App'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
app: ['feature1App'],
@@ -56,6 +57,7 @@ const buildFeatures = () => {
name: 'Feature 2',
icon: 'addDataApp',
app: ['feature2App'],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
app: ['feature2App'],
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
index 7ecf32ee45b857..77b6da2a004871 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx
@@ -18,6 +18,7 @@ const buildProps = (customProps: any = {}) => {
id: 'feature1',
name: 'Feature 1',
app: ['app'],
+ category: { id: 'foo', label: 'foo' },
icon: 'spacesApp',
privileges: {
all: {
diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx
index bc606133459107..0242fddc957c92 100644
--- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx
+++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx
@@ -28,6 +28,7 @@ const features = [
id: 'normal',
name: 'normal feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: { all: [], read: [] },
@@ -43,6 +44,7 @@ const features = [
id: 'normal_with_sub',
name: 'normal feature with sub features',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: { all: [], read: [] },
@@ -96,6 +98,7 @@ const features = [
id: 'bothPrivilegesExcludedFromBase',
name: 'bothPrivilegesExcludedFromBase',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
excludeFromBasePrivileges: true,
@@ -113,6 +116,7 @@ const features = [
id: 'allPrivilegeExcludedFromBase',
name: 'allPrivilegeExcludedFromBase',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
excludeFromBasePrivileges: true,
diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
index 98faae6edab2cd..ea24560c8ddc9b 100644
--- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
+++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts
@@ -80,6 +80,7 @@ describe('usingPrivileges', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['fooApp', 'foo'],
+ category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),
@@ -168,6 +169,7 @@ describe('usingPrivileges', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['foo'],
+ category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),
@@ -322,6 +324,7 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
navLinkId: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
new KibanaFeature({
@@ -329,6 +332,7 @@ describe('usingPrivileges', () => {
name: 'Bar KibanaFeature',
navLinkId: 'bar',
app: ['bar'],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
],
@@ -469,6 +473,7 @@ describe('usingPrivileges', () => {
name: 'Foo KibanaFeature',
navLinkId: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
new KibanaFeature({
@@ -476,6 +481,7 @@ describe('usingPrivileges', () => {
name: 'Bar KibanaFeature',
navLinkId: 'bar',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
],
@@ -552,6 +558,7 @@ describe('all', () => {
id: 'fooFeature',
name: 'Foo KibanaFeature',
app: ['foo'],
+ category: { id: 'foo', label: 'foo' },
navLinkId: 'foo',
privileges: null,
}),
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
index dc261e2eec9826..5f19c911fd5d38 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts
@@ -33,6 +33,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@@ -64,6 +65,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@@ -101,6 +103,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
@@ -148,6 +151,7 @@ describe(`feature_privilege_builder`, () => {
id: 'my-feature',
name: 'my-feature',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: privilege,
read: privilege,
diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
index 033040fd2f14b5..bdf2c87f40f0bd 100644
--- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts
@@ -14,6 +14,7 @@ describe('featurePrivilegeIterator', () => {
name: 'foo',
privileges: null,
app: [],
+ category: { id: 'foo', label: 'foo' },
});
const actualPrivileges = Array.from(
@@ -29,6 +30,7 @@ describe('featurePrivilegeIterator', () => {
const feature = new KibanaFeature({
id: 'foo',
name: 'foo',
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -120,6 +122,7 @@ describe('featurePrivilegeIterator', () => {
const feature = new KibanaFeature({
id: 'foo',
name: 'foo',
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -194,6 +197,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -317,6 +321,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -440,6 +445,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -567,6 +573,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -690,6 +697,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
@@ -815,6 +823,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -923,6 +932,7 @@ describe('featurePrivilegeIterator', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
api: ['all-api', 'read-api'],
diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
index dd8ac44386dbd7..6f721c91fbd67a 100644
--- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts
@@ -21,6 +21,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
management: {
foo: ['management-1', 'management-2'],
@@ -66,6 +67,7 @@ describe('features', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -165,6 +167,7 @@ describe('features', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
];
@@ -207,6 +210,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@@ -327,6 +331,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@@ -409,6 +414,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@@ -467,6 +473,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@@ -532,6 +539,7 @@ describe('features', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['ignore-me-1', 'ignore-me-2'],
management: {
foo: ['ignore-me-1', 'ignore-me-2'],
@@ -602,6 +610,7 @@ describe('reserved', () => {
icon: 'arrowDown',
navLinkId: 'kibana:foo',
app: ['app-1', 'app-2'],
+ category: { id: 'foo', label: 'foo' },
catalogue: ['catalogue-1', 'catalogue-2'],
management: {
foo: ['management-1', 'management-2'],
@@ -644,6 +653,7 @@ describe('reserved', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
privileges: [
@@ -708,6 +718,7 @@ describe('reserved', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -749,6 +760,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -876,6 +888,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -1075,6 +1088,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
privileges: {
all: {
@@ -1216,6 +1230,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -1379,6 +1394,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
excludeFromBasePrivileges: true,
privileges: {
all: {
@@ -1508,6 +1524,7 @@ describe('subFeatures', () => {
name: 'Foo KibanaFeature',
icon: 'arrowDown',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts
index 8e6d72670c8d99..d449eb29d53d8f 100644
--- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts
@@ -12,6 +12,7 @@ it('allows features to be defined without privileges', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
});
@@ -23,6 +24,7 @@ it('allows features with reserved privileges to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -49,6 +51,7 @@ it('allows features with sub-features to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -112,6 +115,7 @@ it('does not allow features with sub-features which have id conflicts with the m
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -162,6 +166,7 @@ it('does not allow features with sub-features which have id conflicts with the p
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
@@ -212,6 +217,7 @@ it('does not allow features with sub-features which have id conflicts each other
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
savedObject: {
diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts
index d91a4d41513162..0c7d12f67f4b9b 100644
--- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts
@@ -13,6 +13,7 @@ it('allows features to be defined without privileges', () => {
name: 'foo',
app: [],
privileges: null,
+ category: { id: 'foo', label: 'foo' },
});
validateReservedPrivileges([feature]);
@@ -23,6 +24,7 @@ it('allows features with a single reserved privilege to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -49,6 +51,7 @@ it('allows multiple features with reserved privileges to be defined', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -71,6 +74,7 @@ it('allows multiple features with reserved privileges to be defined', () => {
id: 'foo2',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -97,6 +101,7 @@ it('prevents a feature from specifying the same reserved privilege id', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -135,6 +140,7 @@ it('prevents features from sharing a reserved privilege id', () => {
id: 'foo',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
@@ -157,6 +163,7 @@ it('prevents features from sharing a reserved privilege id', () => {
id: 'foo2',
name: 'foo',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
reserved: {
description: 'foo',
diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts
index 6e9b88f30479f7..811ea080b43166 100644
--- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts
+++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts
@@ -87,6 +87,7 @@ const putRoleTest = (
id: 'feature_1',
name: 'feature 1',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: {
all: {
ui: [],
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
index f002e13a07cf18..5fbba84467ecf4 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
@@ -299,6 +299,7 @@ export const type = t.keyof({
query: null,
saved_query: null,
threshold: null,
+ threat_match: null,
});
export type Type = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts
index b666b95ea1e976..777256ff961f90 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts
@@ -48,3 +48,104 @@ export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSc
exceptions_list: [],
rule_id: 'rule-1',
});
+
+export const getAddPrepackagedThreatMatchRulesSchemaMock = (): AddPrepackagedRulesSchema => ({
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: 'rule-1',
+ version: 1,
+ threat_query: '*:*',
+ threat_index: 'list-index',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
+
+export const getAddPrepackagedThreatMatchRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({
+ author: [],
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ severity_mapping: [],
+ type: 'threat_match',
+ risk_score: 55,
+ risk_score_mapping: [],
+ language: 'kuery',
+ references: [],
+ actions: [],
+ enabled: false,
+ false_positives: [],
+ from: 'now-6m',
+ interval: '5m',
+ max_signals: DEFAULT_MAX_SIGNALS,
+ tags: [],
+ to: 'now',
+ threat: [],
+ throttle: null,
+ version: 1,
+ exceptions_list: [],
+ rule_id: 'rule-1',
+ threat_query: '*:*',
+ threat_index: 'list-index',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
index 9b90cf9fdf7828..69538f025d95dd 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
@@ -45,6 +45,12 @@ import {
RiskScoreMapping,
SeverityMapping,
} from '../common/schemas';
+import {
+ threat_index,
+ threat_query,
+ threat_filters,
+ threat_mapping,
+} from '../types/threat_mapping';
import {
DefaultStringArray,
@@ -116,6 +122,10 @@ export const addPrepackagedRulesSchema = t.intersection([
references: DefaultStringArray, // defaults to empty array of strings if not set during decode
note, // defaults to "undefined" if not set during decode
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
+ threat_filters, // defaults to "undefined" if not set during decode
+ threat_mapping, // defaults to "undefined" if not set during decode
+ threat_query, // defaults to "undefined" if not set during decode
+ threat_index, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
index 137b40eb648baa..8c916e4f013b42 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts
@@ -17,6 +17,8 @@ import { left } from 'fp-ts/lib/Either';
import {
getAddPrepackagedRulesSchemaMock,
getAddPrepackagedRulesSchemaDecodedMock,
+ getAddPrepackagedThreatMatchRulesSchemaMock,
+ getAddPrepackagedThreatMatchRulesSchemaDecodedMock,
} from './add_prepackaged_rules_schema.mock';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
import { getListArrayMock } from '../types/lists.mock';
@@ -1597,4 +1599,16 @@ describe('add prepackaged rules schema', () => {
expect(message.schema).toEqual(expected);
});
});
+
+ describe('threat_mapping', () => {
+ test('You can set a threat query, index, mapping, filters on a pre-packaged rule', () => {
+ const payload = getAddPrepackagedThreatMatchRulesSchemaMock();
+ const decoded = addPrepackagedRulesSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ const expected = getAddPrepackagedThreatMatchRulesSchemaDecodedMock();
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(expected);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts
index f1e87bdb11e75f..32299be500b457 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts
@@ -55,3 +55,103 @@ export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => (
exceptions_list: [],
rule_id: 'rule-1',
});
+
+export const getCreateThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): CreateRulesSchema => ({
+ description: 'Detecting root and admin users',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: ruleId,
+ threat_query: '*:*',
+ threat_index: 'list-index',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
+
+export const getCreateThreatMatchRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({
+ author: [],
+ severity_mapping: [],
+ risk_score_mapping: [],
+ description: 'Detecting root and admin users',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ references: [],
+ actions: [],
+ enabled: true,
+ false_positives: [],
+ from: 'now-6m',
+ interval: '5m',
+ max_signals: DEFAULT_MAX_SIGNALS,
+ tags: [],
+ to: 'now',
+ threat: [],
+ throttle: null,
+ version: 1,
+ exceptions_list: [],
+ rule_id: 'rule-1',
+ threat_query: '*:*',
+ threat_index: 'list-index',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
index 56bc68a275ee47..19517017743f15 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts
@@ -16,6 +16,8 @@ import { left } from 'fp-ts/lib/Either';
import {
getCreateRulesSchemaMock,
getCreateRulesSchemaDecodedMock,
+ getCreateThreatMatchRulesSchemaMock,
+ getCreateThreatMatchRulesSchemaDecodedMock,
} from './create_rules_schema.mock';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
import { getListArrayMock } from '../types/lists.mock';
@@ -1661,4 +1663,16 @@ describe('create rules schema', () => {
expect(message.schema).toEqual(expected);
});
});
+
+ describe('threat_mapping', () => {
+ test('You can set a threat query, index, mapping, filters when creating a rule', () => {
+ const payload = getCreateThreatMatchRulesSchemaMock();
+ const decoded = createRulesSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ const expected = getCreateThreatMatchRulesSchemaDecodedMock();
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(expected);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
index 7b6b98383cc338..c024ba1c48f8d8 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts
@@ -46,6 +46,12 @@ import {
RiskScoreMapping,
SeverityMapping,
} from '../common/schemas';
+import {
+ threat_index,
+ threat_query,
+ threat_filters,
+ threat_mapping,
+} from '../types/threat_mapping';
import {
DefaultStringArray,
@@ -112,6 +118,10 @@ export const createRulesSchema = t.intersection([
note, // defaults to "undefined" if not set during decode
version: DefaultVersionNumber, // defaults to 1 if not set during decode
exceptions_list: DefaultListArray, // defaults to empty array if not set during decode
+ threat_mapping, // defaults to "undefined" if not set during decode
+ threat_query, // defaults to "undefined" if not set during decode
+ threat_filters, // defaults to "undefined" if not set during decode
+ threat_index, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
index 43f0901912271c..75ad92578318ce 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.test.ts
@@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getCreateRulesSchemaMock } from './create_rules_schema.mock';
+import {
+ getCreateRulesSchemaMock,
+ getCreateThreatMatchRulesSchemaMock,
+} from './create_rules_schema.mock';
import { CreateRulesSchema } from './create_rules_schema';
import { createRuleValidateTypeDependents } from './create_rules_type_dependents';
@@ -87,4 +90,39 @@ describe('create_rules_type_dependents', () => {
const errors = createRuleValidateTypeDependents(schema);
expect(errors).toEqual(['"threshold.value" has to be bigger than 0']);
});
+
+ test('threat_index, threat_query, and threat_mapping are required when type is "threat_match" and validates with it', () => {
+ const schema: CreateRulesSchema = {
+ ...getCreateRulesSchemaMock(),
+ type: 'threat_match',
+ };
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual([
+ 'when "type" is "threat_match", "threat_index" is required',
+ 'when "type" is "threat_match", "threat_query" is required',
+ 'when "type" is "threat_match", "threat_mapping" is required',
+ ]);
+ });
+
+ test('validates with threat_index, threat_query, and threat_mapping when type is "threat_match"', () => {
+ const schema = getCreateThreatMatchRulesSchemaMock();
+ const { threat_filters: threatFilters, ...noThreatFilters } = schema;
+ const errors = createRuleValidateTypeDependents(noThreatFilters);
+ expect(errors).toEqual([]);
+ });
+
+ test('does NOT validate when threat_mapping is an empty array', () => {
+ const schema: CreateRulesSchema = {
+ ...getCreateThreatMatchRulesSchemaMock(),
+ threat_mapping: [],
+ };
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual(['threat_mapping" must have at least one element']);
+ });
+
+ test('validates with threat_index, threat_query, threat_mapping, and an optional threat_filters, when type is "threat_match"', () => {
+ const schema = getCreateThreatMatchRulesSchemaMock();
+ const errors = createRuleValidateTypeDependents(schema);
+ expect(errors).toEqual([]);
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
index 91b14fa9b999c2..c2a41005ebf4d6 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_type_dependents.ts
@@ -5,7 +5,7 @@
*/
import { isMlRule } from '../../../machine_learning/helpers';
-import { isThresholdRule } from '../../utils';
+import { isThreatMatchRule, isThresholdRule } from '../../utils';
import { CreateRulesSchema } from './create_rules_schema';
export const validateAnomalyThreshold = (rule: CreateRulesSchema): string[] => {
@@ -107,6 +107,24 @@ export const validateThreshold = (rule: CreateRulesSchema): string[] => {
return [];
};
+export const validateThreatMapping = (rule: CreateRulesSchema): string[] => {
+ let errors: string[] = [];
+ if (isThreatMatchRule(rule.type)) {
+ if (!rule.threat_mapping) {
+ errors = ['when "type" is "threat_match", "threat_mapping" is required', ...errors];
+ } else if (rule.threat_mapping.length === 0) {
+ errors = ['threat_mapping" must have at least one element', ...errors];
+ }
+ if (!rule.threat_query) {
+ errors = ['when "type" is "threat_match", "threat_query" is required', ...errors];
+ }
+ if (!rule.threat_index) {
+ errors = ['when "type" is "threat_match", "threat_index" is required', ...errors];
+ }
+ }
+ return errors;
+};
+
export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): string[] => {
return [
...validateAnomalyThreshold(schema),
@@ -117,5 +135,6 @@ export const createRuleValidateTypeDependents = (schema: CreateRulesSchema): str
...validateTimelineId(schema),
...validateTimelineTitle(schema),
...validateThreshold(schema),
+ ...validateThreatMapping(schema),
];
};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts
index e3b4196c90c6c9..160dbb92b74cd2 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts
@@ -76,3 +76,94 @@ export const ruleIdsToNdJsonString = (ruleIds: string[]) => {
const rules = ruleIds.map((ruleId) => getImportRulesSchemaMock(ruleId));
return rulesToNdJsonString(rules);
};
+
+export const getImportThreatMatchRulesSchemaMock = (ruleId = 'rule-1'): ImportRulesSchema => ({
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ type: 'threat_match',
+ risk_score: 55,
+ language: 'kuery',
+ rule_id: ruleId,
+ threat_index: 'index-123',
+ threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
+ threat_query: '*:*',
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
+
+export const getImportThreatMatchRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({
+ author: [],
+ description: 'some description',
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ severity: 'high',
+ severity_mapping: [],
+ type: 'threat_match',
+ risk_score: 55,
+ risk_score_mapping: [],
+ language: 'kuery',
+ references: [],
+ actions: [],
+ enabled: true,
+ false_positives: [],
+ from: 'now-6m',
+ interval: '5m',
+ max_signals: DEFAULT_MAX_SIGNALS,
+ tags: [],
+ to: 'now',
+ threat: [],
+ throttle: null,
+ version: 1,
+ exceptions_list: [],
+ rule_id: 'rule-1',
+ immutable: false,
+ threat_query: '*:*',
+ threat_index: 'index-123',
+ threat_mapping: [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ],
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
index 0515bee0052d78..bd25a63e153dde 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts
@@ -20,6 +20,8 @@ import {
import {
getImportRulesSchemaMock,
getImportRulesSchemaDecodedMock,
+ getImportThreatMatchRulesSchemaMock,
+ getImportThreatMatchRulesSchemaDecodedMock,
} from './import_rules_schema.mock';
import { DEFAULT_MAX_SIGNALS } from '../../../constants';
import { getListArrayMock } from '../types/lists.mock';
@@ -1792,4 +1794,16 @@ describe('import rules schema', () => {
expect(message.schema).toEqual(expected);
});
});
+
+ describe('threat_mapping', () => {
+ test('You can set a threat query, index, mapping, filters on an imported rule', () => {
+ const payload = getImportThreatMatchRulesSchemaMock();
+ const decoded = importRulesSchema.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ const expected = getImportThreatMatchRulesSchemaDecodedMock();
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(expected);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
index 698716fea696e8..b63d70783b7b52 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
@@ -52,6 +52,12 @@ import {
RiskScoreMapping,
SeverityMapping,
} from '../common/schemas';
+import {
+ threat_index,
+ threat_query,
+ threat_filters,
+ threat_mapping,
+} from '../types/threat_mapping';
import {
DefaultStringArray,
@@ -135,6 +141,10 @@ export const importRulesSchema = t.intersection([
updated_at, // defaults "undefined" if not set during decode
created_by, // defaults "undefined" if not set during decode
updated_by, // defaults "undefined" if not set during decode
+ threat_filters, // defaults to "undefined" if not set during decode
+ threat_mapping, // defaults to "undefined" if not set during decode
+ threat_query, // defaults to "undefined" if not set during decode
+ threat_index, // defaults to "undefined" if not set during decode
})
),
]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
index ed9fb8930ea1bb..a462b297d37f84 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
@@ -82,3 +82,31 @@ export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSch
machine_learning_job_id: 'some_machine_learning_job_id',
};
};
+
+export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => {
+ return {
+ ...getRulesSchemaMock(anchorDate),
+ type: 'threat_match',
+ threat_index: 'index-123',
+ threat_mapping: [{ entries: [{ field: 'host.name', type: 'mapping', value: 'host.name' }] }],
+ threat_query: '*:*',
+ threat_filters: [
+ {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ ],
+ };
+};
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
index 36fc063761840f..3a47d4af6ac145 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts
@@ -17,11 +17,16 @@ import {
addQueryFields,
addTimelineTitle,
addMlFields,
+ addThreatMatchFields,
} from './rules_schema';
import { exactCheck } from '../../../exact_check';
import { foldLeftRight, getPaths } from '../../../test_utils';
import { TypeAndTimelineOnly } from './type_timeline_only_schema';
-import { getRulesSchemaMock, getRulesMlSchemaMock } from './rules_schema.mocks';
+import {
+ getRulesSchemaMock,
+ getRulesMlSchemaMock,
+ getThreatMatchingSchemaMock,
+} from './rules_schema.mocks';
import { ListArray } from '../types/lists';
export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z';
@@ -593,6 +598,36 @@ describe('rules_schema', () => {
expect(getPaths(left(message.errors))).toEqual(['invalid keys "query,language"']);
expect(message.schema).toEqual({});
});
+
+ test('it validates a threat_match response', () => {
+ const payload = getThreatMatchingSchemaMock();
+
+ const dependents = getDependents(payload);
+ const decoded = dependents.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+ const expected = getThreatMatchingSchemaMock();
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(expected);
+ });
+
+ test('it rejects a response with threat_match properties but type of "query"', () => {
+ const payload: RulesSchema = {
+ ...getThreatMatchingSchemaMock(),
+ type: 'query',
+ };
+
+ const dependents = getDependents(payload);
+ const decoded = dependents.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'invalid keys "threat_index,threat_mapping,[{"entries":[{"field":"host.name","type":"mapping","value":"host.name"}]}],threat_query,threat_filters,[{"bool":{"must":[{"query_string":{"query":"host.name: linux","analyze_wildcard":true,"time_zone":"Zulu"}}],"filter":[],"should":[],"must_not":[]}}]"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
});
describe('addSavedId', () => {
@@ -647,6 +682,11 @@ describe('rules_schema', () => {
const fields = addQueryFields({ type: 'saved_query' });
expect(fields.length).toEqual(2);
});
+
+ test('should return two fields for a rule of type "threat_match"', () => {
+ const fields = addQueryFields({ type: 'threat_match' });
+ expect(fields.length).toEqual(2);
+ });
});
describe('addMlFields', () => {
@@ -704,4 +744,17 @@ describe('rules_schema', () => {
expect(message.schema).toEqual({ ...payload, exceptions_list: [] });
});
});
+
+ describe('addThreatMatchFields', () => {
+ test('should return empty array if type is not "threat_match"', () => {
+ const fields = addThreatMatchFields({ type: 'query' });
+ const expected: t.Mixed[] = [];
+ expect(fields).toEqual(expected);
+ });
+
+ test('should return 5 fields for a rule of type "threat_match"', () => {
+ const fields = addThreatMatchFields({ type: 'threat_match' });
+ expect(fields.length).toEqual(5);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
index c26a7efb0c2882..1c2254f9f8f099 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
@@ -60,6 +60,13 @@ import {
rule_name_override,
timestamp_override,
} from '../common/schemas';
+import {
+ threat_index,
+ threat_query,
+ threat_filters,
+ threat_mapping,
+} from '../types/threat_mapping';
+
import { DefaultListArray } from '../types/lists_default_array';
import {
DefaultStringArray,
@@ -114,7 +121,7 @@ export const dependentRulesSchema = t.partial({
language,
query,
- // when type = saved_query, saved_is is required
+ // when type = saved_query, saved_id is required
saved_id,
// These two are required together or not at all.
@@ -127,6 +134,12 @@ export const dependentRulesSchema = t.partial({
// Threshold fields
threshold,
+
+ // Threat Match fields
+ threat_filters,
+ threat_index,
+ threat_query,
+ threat_mapping,
});
/**
@@ -206,7 +219,9 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi
};
export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => {
- if (['eql', 'query', 'saved_query', 'threshold'].includes(typeAndTimelineOnly.type)) {
+ if (
+ ['eql', 'query', 'saved_query', 'threshold', 'threat_match'].includes(typeAndTimelineOnly.type)
+ ) {
return [
t.exact(t.type({ query: dependentRulesSchema.props.query })),
t.exact(t.type({ language: dependentRulesSchema.props.language })),
@@ -240,6 +255,20 @@ export const addThresholdFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.
}
};
+export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => {
+ if (typeAndTimelineOnly.type === 'threat_match') {
+ return [
+ t.exact(t.type({ threat_query: dependentRulesSchema.props.threat_query })),
+ t.exact(t.type({ threat_index: dependentRulesSchema.props.threat_index })),
+ t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })),
+ t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })),
+ t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })),
+ ];
+ } else {
+ return [];
+ }
+};
+
export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => {
const dependents: t.Mixed[] = [
t.exact(requiredRulesSchema),
@@ -249,6 +278,7 @@ export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed
...addQueryFields(typeAndTimelineOnly),
...addMlFields(typeAndTimelineOnly),
...addThresholdFields(typeAndTimelineOnly),
+ ...addThreatMatchFields(typeAndTimelineOnly),
];
if (dependents.length > 1) {
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts
new file mode 100644
index 00000000000000..63d593ea84e67f
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.test.ts
@@ -0,0 +1,176 @@
+/*
+ * 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 {
+ ThreatMapping,
+ threatMappingEntries,
+ ThreatMappingEntries,
+ threat_mapping,
+} from './threat_mapping';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+import { foldLeftRight, getPaths } from '../../../test_utils';
+import { exactCheck } from '../../../exact_check';
+
+describe('threat_mapping', () => {
+ describe('threatMappingEntries', () => {
+ test('it should validate an entry', () => {
+ const payload: ThreatMappingEntries = [
+ {
+ field: 'field.one',
+ type: 'mapping',
+ value: 'field.one',
+ },
+ ];
+ const decoded = threatMappingEntries.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+
+ test('it should NOT validate an extra entry item', () => {
+ const payload: ThreatMappingEntries & Array<{ extra: string }> = [
+ {
+ field: 'field.one',
+ type: 'mapping',
+ value: 'field.one',
+ extra: 'blah',
+ },
+ ];
+ const decoded = threatMappingEntries.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should NOT validate a non string', () => {
+ const payload = ([
+ {
+ field: 5,
+ type: 'mapping',
+ value: 'field.one',
+ },
+ ] as unknown) as ThreatMappingEntries[];
+ const decoded = threatMappingEntries.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should NOT validate a wrong type', () => {
+ const payload = ([
+ {
+ field: 'field.one',
+ type: 'invalid',
+ value: 'field.one',
+ },
+ ] as unknown) as ThreatMappingEntries[];
+ const decoded = threatMappingEntries.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "invalid" supplied to "type"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+ });
+
+ describe('threat_mapping', () => {
+ test('it should validate a threat mapping', () => {
+ const payload: ThreatMapping = [
+ {
+ entries: [
+ {
+ field: 'field.one',
+ type: 'mapping',
+ value: 'field.one',
+ },
+ ],
+ },
+ ];
+ const decoded = threat_mapping.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([]);
+ expect(message.schema).toEqual(payload);
+ });
+ });
+
+ test('it should NOT validate an extra key', () => {
+ const payload: ThreatMapping & Array<{ extra: string }> = [
+ {
+ entries: [
+ {
+ field: 'field.one',
+ type: 'mapping',
+ value: 'field.one',
+ },
+ ],
+ extra: 'invalid',
+ },
+ ];
+
+ const decoded = threat_mapping.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should NOT validate an extra inner entry', () => {
+ const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [
+ {
+ entries: [
+ {
+ field: 'field.one',
+ type: 'mapping',
+ value: 'field.one',
+ extra: 'blah',
+ },
+ ],
+ },
+ ];
+
+ const decoded = threat_mapping.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']);
+ expect(message.schema).toEqual({});
+ });
+
+ test('it should NOT validate an extra inner entry with the wrong data type', () => {
+ const payload = ([
+ {
+ entries: [
+ {
+ field: 5,
+ type: 'mapping',
+ value: 'field.one',
+ },
+ ],
+ },
+ ] as unknown) as ThreatMapping;
+
+ const decoded = threat_mapping.decode(payload);
+ const checked = exactCheck(payload, decoded);
+ const message = pipe(checked, foldLeftRight);
+
+ expect(getPaths(left(message.errors))).toEqual([
+ 'Invalid value "5" supplied to "entries,field"',
+ ]);
+ expect(message.schema).toEqual({});
+ });
+});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
new file mode 100644
index 00000000000000..f2b4754c2d113c
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable @typescript-eslint/naming-convention */
+
+import * as t from 'io-ts';
+import { NonEmptyString } from './non_empty_string';
+
+export const threat_query = t.string;
+export type ThreatQuery = t.TypeOf;
+export const threatQueryOrUndefined = t.union([threat_query, t.undefined]);
+export type ThreatQueryOrUndefined = t.TypeOf;
+
+export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet
+export type ThreatFilters = t.TypeOf;
+export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]);
+export type ThreatFiltersOrUndefined = t.TypeOf;
+
+export const threatMappingEntries = t.array(
+ t.exact(
+ t.type({
+ field: NonEmptyString,
+ type: t.keyof({ mapping: null }),
+ value: NonEmptyString,
+ })
+ )
+);
+export type ThreatMappingEntries = t.TypeOf;
+
+export const threat_mapping = t.array(
+ t.exact(
+ t.type({
+ entries: threatMappingEntries,
+ })
+ )
+);
+export type ThreatMapping = t.TypeOf;
+
+export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]);
+export type ThreatMappingOrUndefined = t.TypeOf;
+
+export const threat_index = t.string;
+export const threatIndexOrUndefined = t.union([threat_index, t.undefined]);
+export type ThreatIndexOrUndefined = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
index 99680ffe41d444..ea50acc9b46be5 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { hasLargeValueList, hasNestedEntry } from './utils';
+import { hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
import { EntriesArray } from '../shared_imports';
describe('#hasLargeValueList', () => {
@@ -102,4 +102,14 @@ describe('#hasNestedEntry', () => {
expect(hasLists).toBeFalsy();
});
+
+ describe('isThreatMatchRule', () => {
+ test('it returns true if a threat match rule', () => {
+ expect(isThreatMatchRule('threat_match')).toEqual(true);
+ });
+
+ test('it returns false if not a threat match rule', () => {
+ expect(isThreatMatchRule('query')).toEqual(false);
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
index 170d28cb5a725a..f76417099bb173 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
@@ -17,6 +17,7 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => {
return found.length > 0;
};
-export const isEqlRule = (ruleType: Type | undefined) => ruleType === 'eql';
-export const isThresholdRule = (ruleType: Type | undefined) => ruleType === 'threshold';
-export const isQueryRule = (ruleType: Type | undefined) => ruleType === 'query';
+export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === 'eql';
+export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold';
+export const isQueryRule = (ruleType: Type | undefined): boolean => ruleType === 'query';
+export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx
index 073cb46d3949a3..f2eb5cf5b94f38 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx
@@ -429,5 +429,11 @@ describe('helpers', () => {
expect(result.description).toEqual('Threshold');
});
+
+ it('returns a humanized description for a threat_match type', () => {
+ const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'threat_match');
+
+ expect(result.description).toEqual('Threat Match');
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
index 0c866ae0bd926d..4d46d4dc868467 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx
@@ -391,6 +391,14 @@ export const buildRuleTypeDescription = (label: string, ruleType: Type): ListIte
},
];
}
+ case 'threat_match': {
+ return [
+ {
+ title: label,
+ description: i18n.THREAT_MATCH_TYPE_DESCRIPTION,
+ },
+ ];
+ }
default:
return assertUnreachable(ruleType);
}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
index 124ef9e6484034..d714f04f519d4e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/translations.tsx
@@ -55,6 +55,13 @@ export const THRESHOLD_TYPE_DESCRIPTION = i18n.translate(
}
);
+export const THREAT_MATCH_TYPE_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.detectionEngine.createRule.threatMatchRuleTypeDescription',
+ {
+ defaultMessage: 'Threat Match',
+ }
+);
+
export const ML_JOB_STARTED = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDescription.mlJobStartedDescription',
{
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index 2acb3e57c5a3b9..65a5c6aca00508 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -94,6 +94,7 @@ export const filterRuleFieldsForType = (fields: T, type: T
case 'threshold':
const { anomalyThreshold, machineLearningJobId, ...thresholdRuleFields } = fields;
return thresholdRuleFields;
+ case 'threat_match':
case 'query':
case 'saved_query':
case 'eql':
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
index dbfb0333e48ee2..42fbe40d690ea7 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
@@ -315,6 +315,7 @@ const getRuleSpecificRuleParamKeys = (ruleType: Type) => {
return ['anomaly_threshold', 'machine_learning_job_id'];
case 'threshold':
return ['threshold', ...queryRuleParams];
+ case 'threat_match':
case 'query':
case 'saved_query':
case 'eql':
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
index c0c9cdf227643f..218cef36ed50a4 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
@@ -34,7 +34,7 @@ describe('TrustedAppsPage', () => {
});
});
- test('rendering', () => {
+ test.skip('rendering', () => {
expect(render()).toMatchSnapshot();
});
@@ -78,7 +78,7 @@ describe('TrustedAppsPage', () => {
expect(history.location.search).toBe('?page_index=2&page_size=20&show=create');
});
- it('should display create form', async () => {
+ it.skip('should display create form', async () => {
const { getByTestId } = await renderAndClickAddButton();
expect(getByTestId('addTrustedAppFlyout-createForm')).toMatchSnapshot();
});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx
deleted file mode 100644
index db4b514a6c748b..00000000000000
--- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx
+++ /dev/null
@@ -1,521 +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.
- */
-
-/* eslint-disable react/display-name */
-
-import React, { memo } from 'react';
-import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json';
-import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json';
-import { htmlIdGenerator, ButtonColor } from '@elastic/eui';
-import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import { ResolverProcessType } from '../types';
-import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public';
-
-type ResolverColorNames =
- | 'descriptionText'
- | 'full'
- | 'graphControls'
- | 'graphControlsBackground'
- | 'resolverBackground'
- | 'resolverEdge'
- | 'resolverEdgeText'
- | 'resolverBreadcrumbBackground'
- | 'pillStroke';
-
-type ColorMap = Record;
-interface NodeStyleConfig {
- backingFill: string;
- cubeSymbol: string;
- descriptionFill: string;
- descriptionText: string;
- isLabelFilled: boolean;
- labelButtonFill: ButtonColor;
- strokeColor: string;
-}
-
-interface NodeStyleMap {
- runningProcessCube: NodeStyleConfig;
- runningTriggerCube: NodeStyleConfig;
- terminatedProcessCube: NodeStyleConfig;
- terminatedTriggerCube: NodeStyleConfig;
-}
-
-const idGenerator = htmlIdGenerator();
-
-/**
- * Ids of paint servers to be referenced by fill and stroke attributes
- */
-const PaintServerIds = {
- runningProcessCube: idGenerator('psRunningProcessCube'),
- runningTriggerCube: idGenerator('psRunningTriggerCube'),
- terminatedProcessCube: idGenerator('psTerminatedProcessCube'),
- terminatedTriggerCube: idGenerator('psTerminatedTriggerCube'),
-};
-
-/**
- * PaintServers: Where color palettes, grandients, patterns and other similar concerns
- * are exposed to the component
- */
-
-const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => (
- <>
-
-
-
-
-
-
-
-
- {isDarkMode ? (
- <>
-
-
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
-
-
-
-
- >
- )}
- >
-));
-
-/**
- * Ids of symbols to be linked by elements
- */
-export const SymbolIds = {
- processNodeLabel: idGenerator('nodeSymbol'),
- runningProcessCube: idGenerator('runningCube'),
- runningTriggerCube: idGenerator('runningTriggerCube'),
- terminatedProcessCube: idGenerator('terminatedCube'),
- terminatedTriggerCube: idGenerator('terminatedTriggerCube'),
- processCubeActiveBacking: idGenerator('activeBacking'),
-};
-
-/**
- * Defs entries that define shapes, masks and other spatial elements
- */
-const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => (
- <>
-
-
-
-
- {'Running Process'}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {'resolver_dark process running'}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {'Terminated Process'}
-
-
-
-
-
-
-
-
- {'Terminated Trigger Process'}
- {isDarkMode && (
-
- )}
-
- {!isDarkMode && (
-
- )}
-
-
-
-
-
-
- {'resolver active backing'}
-
-
- >
-));
-
-/**
- * This `` element is used to define the reusable assets for the Resolver
- * It confers several advantages, including but not limited to:
- * 1. Freedom of form for creative assets (beyond box-model constraints)
- * 2. Separation of concerns between creative assets and more functional areas of the app
- * 3. `` elements can be handled by compositor (faster)
- */
-const SymbolDefinitionsComponent = memo(({ className }: { className?: string }) => {
- const isDarkMode = useUiSetting('theme:darkMode');
- return (
-
-
-
-
-
-
- );
-});
-
-export const SymbolDefinitions = styled(SymbolDefinitionsComponent)`
- position: absolute;
- left: 100%;
- top: 100%;
- width: 0;
- height: 0;
-`;
-
-const processTypeToCube: Record = {
- processCreated: 'runningProcessCube',
- processRan: 'runningProcessCube',
- processTerminated: 'terminatedProcessCube',
- unknownProcessEvent: 'runningProcessCube',
- processCausedAlert: 'runningTriggerCube',
- unknownEvent: 'runningProcessCube',
-};
-
-/**
- * A hook to bring Resolver theming information into components.
- */
-export const useResolverTheme = (): {
- colorMap: ColorMap;
- nodeAssets: NodeStyleMap;
- cubeAssetsForNode: (isProcessTerimnated: boolean, isProcessTrigger: boolean) => NodeStyleConfig;
-} => {
- const isDarkMode = useUiSetting('theme:darkMode');
- const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
-
- const getThemedOption = (lightOption: string, darkOption: string): string => {
- return isDarkMode ? darkOption : lightOption;
- };
-
- const colorMap = {
- descriptionText: theme.euiTextColor,
- full: theme.euiColorFullShade,
- graphControls: theme.euiColorDarkestShade,
- graphControlsBackground: theme.euiColorEmptyShade,
- processBackingFill: `${theme.euiColorPrimary}${getThemedOption('0F', '1F')}`, // Add opacity 0F = 6% , 1F = 12%
- resolverBackground: theme.euiColorEmptyShade,
- resolverEdge: getThemedOption(theme.euiColorLightestShade, theme.euiColorLightShade),
- resolverBreadcrumbBackground: theme.euiColorLightestShade,
- resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade),
- triggerBackingFill: `${theme.euiColorDanger}${getThemedOption('0F', '1F')}`,
- pillStroke: theme.euiColorLightShade,
- };
-
- const nodeAssets: NodeStyleMap = {
- runningProcessCube: {
- backingFill: colorMap.processBackingFill,
- cubeSymbol: `#${SymbolIds.runningProcessCube}`,
- descriptionFill: colorMap.descriptionText,
- descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', {
- defaultMessage: 'Running Process',
- }),
- isLabelFilled: true,
- labelButtonFill: 'primary',
- strokeColor: theme.euiColorPrimary,
- },
- runningTriggerCube: {
- backingFill: colorMap.triggerBackingFill,
- cubeSymbol: `#${SymbolIds.runningTriggerCube}`,
- descriptionFill: colorMap.descriptionText,
- descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', {
- defaultMessage: 'Running Trigger',
- }),
- isLabelFilled: true,
- labelButtonFill: 'danger',
- strokeColor: theme.euiColorDanger,
- },
- terminatedProcessCube: {
- backingFill: colorMap.processBackingFill,
- cubeSymbol: `#${SymbolIds.terminatedProcessCube}`,
- descriptionFill: colorMap.descriptionText,
- descriptionText: i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.terminatedProcess',
- {
- defaultMessage: 'Terminated Process',
- }
- ),
- isLabelFilled: false,
- labelButtonFill: 'primary',
- strokeColor: theme.euiColorPrimary,
- },
- terminatedTriggerCube: {
- backingFill: colorMap.triggerBackingFill,
- cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`,
- descriptionFill: colorMap.descriptionText,
- descriptionText: i18n.translate(
- 'xpack.securitySolution.endpoint.resolver.terminatedTrigger',
- {
- defaultMessage: 'Terminated Trigger',
- }
- ),
- isLabelFilled: false,
- labelButtonFill: 'danger',
- strokeColor: theme.euiColorDanger,
- },
- };
-
- function cubeAssetsForNode(isProcessTerminated: boolean, isProcessTrigger: boolean) {
- if (isProcessTerminated) {
- if (isProcessTrigger) {
- return nodeAssets.terminatedTriggerCube;
- } else {
- return nodeAssets[processTypeToCube.processTerminated];
- }
- } else if (isProcessTrigger) {
- return nodeAssets[processTypeToCube.processCausedAlert];
- } else {
- return nodeAssets[processTypeToCube.processRan];
- }
- }
-
- return { colorMap, nodeAssets, cubeAssetsForNode };
-};
-
-export const calculateResolverFontSize = (
- magFactorX: number,
- minFontSize: number,
- slopeOfFontScale: number
-): number => {
- const fontSizeAdjustmentForScale = magFactorX > 1 ? slopeOfFontScale * (magFactorX - 1) : 0;
- return minFontSize + fontSizeAdjustmentForScale;
-};
diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
index 935e565be039e6..dba1136193ee1a 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx
@@ -276,48 +276,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or
);
expect(edgesThatTerminateUnderneathSecondChild).toHaveLength(1);
});
-
- it('should render a related events button', async () => {
+ it('should show exactly one option with the correct count', async () => {
await expect(
- simulator.map(() => ({
- relatedEventButtons: simulator.processNodeSubmenuButton(entityIDs.origin).length,
- }))
- ).toYieldEqualTo({
- relatedEventButtons: 1,
- });
- });
- describe('when the related events button is clicked', () => {
- beforeEach(async () => {
- const button = await simulator.resolveWrapper(() =>
- simulator.processNodeSubmenuButton(entityIDs.origin)
- );
- if (button) {
- button.simulate('click', { button: 0 });
- }
- });
- it('should open the submenu and display exactly one option with the correct count', async () => {
- await expect(
- simulator.map(() =>
- simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text())
- )
- ).toYieldEqualTo(['2 registry']);
- });
- });
- describe('and when the related events button is clicked again', () => {
- beforeEach(async () => {
- const button = await simulator.resolveWrapper(() =>
- simulator.processNodeSubmenuButton(entityIDs.origin)
- );
- if (button) {
- button.simulate('click', { button: 0 });
- button.simulate('click', { button: 0 }); // The first click opened the menu, this second click closes it
- }
- });
- it('should close the submenu', async () => {
- await expect(
- simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').length)
- ).toYieldEqualTo(0);
- });
+ simulator.map(() =>
+ simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text())
+ )
+ ).toYieldEqualTo(['2 registry']);
});
});
});
diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
index fcc363a1560d5e..53b889004798f5 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx
@@ -9,7 +9,8 @@ import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { applyMatrix3, distance, angle } from '../models/vector2';
import { Vector2, Matrix3, EdgeLineMetadata } from '../types';
-import { useResolverTheme, calculateResolverFontSize } from './assets';
+import { fontSize } from './font_size';
+import { useColors } from './use_colors';
interface StyledEdgeLine {
readonly resolverEdgeColor: string;
@@ -19,7 +20,7 @@ interface StyledEdgeLine {
const StyledEdgeLine = styled.div`
position: absolute;
height: ${(props) => {
- return `${calculateResolverFontSize(props.magFactorX, 12, 8.5)}px`;
+ return `${fontSize(props.magFactorX, 12, 8.5)}px`;
}};
background-color: ${(props) => props.resolverEdgeColor};
`;
@@ -87,8 +88,8 @@ const EdgeLineComponent = React.memo(
*/
const screenStart = applyMatrix3(startPosition, projectionMatrix);
const screenEnd = applyMatrix3(endPosition, projectionMatrix);
- const [magFactorX] = projectionMatrix;
- const { colorMap } = useResolverTheme();
+ const [xScale] = projectionMatrix;
+ const colorMap = useColors();
const elapsedTime = edgeLineMetadata?.elapsedTime;
/**
@@ -96,7 +97,7 @@ const EdgeLineComponent = React.memo(
* should be the same as the distance between the start and end points.
*/
const length = distance(screenStart, screenEnd);
- const scaledTypeSize = calculateResolverFontSize(magFactorX, 10, 7.5);
+ const scaledTypeSize = fontSize(xScale, 10, 7.5);
const style = {
left: `${screenStart[0]}px`,
@@ -120,8 +121,8 @@ const EdgeLineComponent = React.memo(
/**
* Calculates a fractional offset from 0 -> 5% as magFactorX decreases from 1 to a min of .5
*/
- if (magFactorX < 1) {
- const fractionalOffset = (1 / magFactorX) * ((1 - magFactorX) * 10);
+ if (xScale < 1) {
+ const fractionalOffset = (1 / xScale) * ((1 - xScale) * 10);
elapsedTimeLeftPosPct += fractionalOffset;
}
@@ -130,7 +131,7 @@ const EdgeLineComponent = React.memo(
className={className}
style={style}
resolverEdgeColor={colorMap.resolverEdge}
- magFactorX={magFactorX}
+ magFactorX={xScale}
data-test-subj="resolver:graph:edgeline"
>
{elapsedTime && (
diff --git a/x-pack/plugins/security_solution/public/resolver/view/font_size.ts b/x-pack/plugins/security_solution/public/resolver/view/font_size.ts
new file mode 100644
index 00000000000000..d0340160eb5393
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/font_size.ts
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+/**
+ * Return a font-size based on a scale, minimum size, and a coefficient.
+ */
+export function fontSize(scale: number, minimum: number, slope: number): number {
+ return minimum + (scale > 1 ? slope * (scale - 1) : 0);
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
index 610deef07775bc..75aecf6747cca2 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx
@@ -13,8 +13,8 @@ import { useSelector, useDispatch } from 'react-redux';
import { SideEffectContext } from './side_effect_context';
import { Vector2 } from '../types';
import * as selectors from '../store/selectors';
-import { useResolverTheme } from './assets';
import { ResolverAction } from '../store/actions';
+import { useColors } from './use_colors';
interface StyledGraphControls {
graphControlsBackground: string;
@@ -66,7 +66,7 @@ const GraphControlsComponent = React.memo(
const dispatch: (action: ResolverAction) => unknown = useDispatch();
const scalingFactor = useSelector(selectors.scalingFactor);
const { timestamp } = useContext(SideEffectContext);
- const { colorMap } = useResolverTheme();
+ const colorMap = useColors();
const handleZoomAmountChange = useCallback(
(event: React.ChangeEvent | React.MouseEvent) => {
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
index deddd171982298..4e9d64f5a76a45 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx
@@ -11,11 +11,12 @@ import { i18n } from '@kbn/i18n';
/* eslint-disable react/display-name */
import React, { memo } from 'react';
-import { useResolverTheme, SymbolIds } from '../assets';
interface StyledSVGCube {
readonly isOrigin?: boolean;
}
+import { useCubeAssets } from '../use_cube_assets';
+import { useSymbolIDs } from '../use_symbol_ids';
/**
* Icon representing a process node.
@@ -34,8 +35,8 @@ export const CubeForProcess = memo(function ({
isOrigin?: boolean;
className?: string;
}) {
- const { cubeAssetsForNode } = useResolverTheme();
- const { cubeSymbol, strokeColor } = cubeAssetsForNode(!running, false);
+ const { cubeSymbol, strokeColor } = useCubeAssets(!running, false);
+ const { processCubeActiveBacking } = useSymbolIDs();
return (
{isOrigin && (
{
- if (!processEvent) {
- return { descriptionText: '' };
- }
- return cubeAssetsForNode(isProcessTerminated, false);
- }, [processEvent, cubeAssetsForNode, isProcessTerminated]);
+ const { descriptionText } = useCubeAssets(isProcessTerminated, false);
const nodeDetailHref = useSelector((state: ResolverState) =>
selectors.relativeHref(state)({
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
index fd564cde9d15cd..6113cea4c4edc1 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx
@@ -26,7 +26,7 @@ import { SafeResolverEvent } from '../../../../common/endpoint/types';
import { LimitWarning } from '../limit_warnings';
import { ResolverState } from '../../types';
import { useNavigateOrReplace } from '../use_navigate_or_replace';
-import { useResolverTheme } from '../assets';
+import { useColors } from '../use_colors';
const StyledLimitWarning = styled(LimitWarning)`
flex-flow: row wrap;
@@ -208,9 +208,7 @@ function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView }
const isTerminated = useSelector((state: ResolverState) =>
entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID)
);
- const {
- colorMap: { descriptionText },
- } = useResolverTheme();
+ const { descriptionText } = useColors();
return (
{name === '' ? (
diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
index 19f0aa3fe1d678..a7d76277c6ab1d 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/* eslint-disable react/display-name */
+
import { i18n } from '@kbn/i18n';
import { EuiBreadcrumbs, EuiCode, EuiBetaBadge } from '@elastic/eui';
import styled from 'styled-components';
import React, { memo } from 'react';
-import { useResolverTheme } from '../assets';
+import { useColors } from '../use_colors';
/**
* A bold version of EuiCode to display certain titles with
@@ -63,7 +65,7 @@ export const GeneratedText = React.memo(function ({ children }) {
valueSplitByWordBoundaries[0],
...valueSplitByWordBoundaries
.splice(1)
- .reduce(function (generatedTextMemo: Array, value, index) {
+ .reduce(function (generatedTextMemo: Array, value) {
return [...generatedTextMemo, value, ];
}, []),
];
@@ -73,7 +75,6 @@ export const GeneratedText = React.memo(function ({ children }) {
});
}
});
-GeneratedText.displayName = 'GeneratedText';
/**
* A component to keep time representations in blocks so they don't wrap
@@ -93,9 +94,7 @@ export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({
}: {
breadcrumbs: Breadcrumbs;
}) {
- const {
- colorMap: { resolverBreadcrumbBackground, resolverEdgeText },
- } = useResolverTheme();
+ const { resolverBreadcrumbBackground, resolverEdgeText } = useColors();
return (
<>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
index 3edfe36087e688..65ec395080f864 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
@@ -12,12 +12,15 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { NodeSubMenu } from './submenu';
import { applyMatrix3 } from '../models/vector2';
import { Vector2, Matrix3, ResolverState } from '../types';
-import { SymbolIds, useResolverTheme, calculateResolverFontSize } from './assets';
import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types';
import { useResolverDispatch } from './use_resolver_dispatch';
import * as eventModel from '../../../common/endpoint/models/event';
import * as selectors from '../store/selectors';
import { useNavigateOrReplace } from './use_navigate_or_replace';
+import { fontSize } from './font_size';
+import { useCubeAssets } from './use_cube_assets';
+import { useSymbolIDs } from './use_symbol_ids';
+import { useColors } from './use_colors';
interface StyledActionsContainer {
readonly color: string;
@@ -108,6 +111,8 @@ const UnstyledProcessEventDot = React.memo(
// This should be unique to each instance of Resolver
const htmlIDPrefix = `resolver:${resolverComponentInstanceID}`;
+ const symbolIDs = useSymbolIDs();
+
/**
* Convert the position, which is in 'world' coordinates, to screen coordinates.
*/
@@ -191,7 +196,7 @@ const UnstyledProcessEventDot = React.memo(
* 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this.
* 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise
*/
- const scaledTypeSize = calculateResolverFontSize(xScale, 18.75, 12.5);
+ const scaledTypeSize = fontSize(xScale, 18.75, 12.5);
const markerBaseSize = 15;
const markerSize = markerBaseSize;
@@ -212,7 +217,7 @@ const UnstyledProcessEventDot = React.memo(
})
| null;
} = React.createRef();
- const { colorMap, cubeAssetsForNode } = useResolverTheme();
+ const colorMap = useColors();
const {
backingFill,
cubeSymbol,
@@ -220,7 +225,7 @@ const UnstyledProcessEventDot = React.memo(
isLabelFilled,
labelButtonFill,
strokeColor,
- } = cubeAssetsForNode(
+ } = useCubeAssets(
isProcessTerminated,
/**
* There is no definition for 'trigger process' yet. return false.
@@ -252,13 +257,6 @@ const UnstyledProcessEventDot = React.memo(
});
}, [dispatch, nodeID]);
- const handleRelatedEventRequest = useCallback(() => {
- dispatch({
- type: 'userRequestedRelatedEventData',
- payload: nodeID,
- });
- }, [dispatch, nodeID]);
-
const handleClick = useCallback(
(clickEvent) => {
if (animationTarget.current?.beginElement) {
@@ -323,7 +321,7 @@ const UnstyledProcessEventDot = React.memo(
>
{isOrigin && (
{grandTotal !== null && grandTotal > 0 && (
diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
index f96d89b893cebe..13dcfcabe50cb7 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx
@@ -16,13 +16,14 @@ import { EdgeLine } from './edge_line';
import { GraphControls } from './graph_controls';
import { ProcessEventDot } from './process_event_dot';
import { useCamera } from './use_camera';
-import { SymbolDefinitions, useResolverTheme } from './assets';
+import { SymbolDefinitions } from './symbol_definitions';
import { useStateSyncingActions } from './use_state_syncing_actions';
import { StyledMapContainer, GraphContainer } from './styles';
import { entityIDSafeVersion } from '../../../common/endpoint/models/event';
import { SideEffectContext } from './side_effect_context';
import { ResolverProps, ResolverState } from '../types';
import { PanelRouter } from './panels';
+import { useColors } from './use_colors';
/**
* The highest level connected Resolver component. Needs a `Provider` in its ancestry to work.
@@ -73,7 +74,7 @@ export const ResolverWithoutProviders = React.memo(
const isLoading = useSelector(selectors.isTreeLoading);
const hasError = useSelector(selectors.hadErrorLoadingTree);
const activeDescendantId = useSelector(selectors.ariaActiveDescendant);
- const { colorMap } = useResolverTheme();
+ const colorMap = useColors();
return (
diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
index dda90df0fff936..d40aa0b26a94b3 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
@@ -4,16 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable react/display-name */
-
import { i18n } from '@kbn/i18n';
-import React, { useState, useCallback, useRef, useLayoutEffect, useMemo } from 'react';
-import { EuiI18nNumber, EuiButton, EuiPopover, ButtonColor } from '@elastic/eui';
+import React, { useMemo } from 'react';
+import { EuiI18nNumber } from '@elastic/eui';
import styled from 'styled-components';
import { ResolverNodeStats } from '../../../common/endpoint/types';
import { useRelatedEventByCategoryNavigation } from './use_related_event_by_category_navigation';
-import { Matrix3 } from '../types';
-import { useResolverTheme } from './assets';
+import { useColors } from './use_colors';
/**
* i18n-translated titles for submenus and identifiers for display of states:
@@ -45,53 +42,6 @@ interface ResolverSubmenuOption {
export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string;
-const StyledActionButton = styled(EuiButton)`
- &.euiButton--small {
- height: fit-content;
- line-height: 1;
- padding: 0.25em;
- font-size: 0.85rem;
- }
-`;
-
-/**
- * This will be the "host button" that displays the "total number of related events" and opens
- * the sumbmenu (with counts by category) when clicked.
- */
-const SubButton = React.memo(
- ({
- hasMenu,
- menuIsOpen,
- action,
- count,
- nodeID,
- }: {
- hasMenu: boolean;
- menuIsOpen?: boolean;
- action: (evt: React.MouseEvent) => void;
- count?: number;
- nodeID: string;
- }) => {
- const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown';
- return (
-
- {count ? : ''} {subMenuAssets.relatedEvents.title}
-
- );
- }
-);
-
/**
* A Submenu to be displayed in one of two forms:
* 1) Provided a collection of `optionsWithActions`: it will call `menuAction` then - if and when menuData becomes available - display each item with an optional prefix and call the supplied action for the options when that option is clicked.
@@ -99,53 +49,20 @@ const SubButton = React.memo(
*/
const NodeSubMenuComponents = React.memo(
({
- count,
- buttonBorderColor,
- menuAction,
className,
- projectionMatrix,
nodeID,
relatedEventStats,
}: {
className?: string;
- menuAction?: () => unknown;
- buttonBorderColor: ButtonColor;
// eslint-disable-next-line react/no-unused-prop-types
buttonFill: string;
- count?: number;
/**
* Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself.
*/
- projectionMatrix: Matrix3;
nodeID: string;
relatedEventStats: ResolverNodeStats | undefined;
}) => {
- // keep a ref to the popover so we can call its reposition method
- const popoverRef = useRef(null);
-
- const [menuIsOpen, setMenuOpen] = useState(false);
- const handleMenuOpenClick = useCallback(
- (clickEvent: React.MouseEvent) => {
- // stopping propagation/default to prevent other node animations from triggering
- clickEvent.preventDefault();
- clickEvent.stopPropagation();
- setMenuOpen(!menuIsOpen);
- },
- [menuIsOpen]
- );
- const handleMenuActionClick = useCallback(
- (clickEvent: React.MouseEvent) => {
- // stopping propagation/default to prevent other node animations from triggering
- clickEvent.preventDefault();
- clickEvent.stopPropagation();
- if (typeof menuAction === 'function') menuAction();
- setMenuOpen(true);
- },
- [menuAction]
- );
-
// The last projection matrix that was used to position the popover
- const projectionMatrixAtLastRender = useRef();
const relatedEventCallbacks = useRelatedEventByCategoryNavigation({
nodeID,
categories: relatedEventStats?.events?.byCategory,
@@ -164,91 +81,39 @@ const NodeSubMenuComponents = React.memo(
}
}, [relatedEventStats, relatedEventCallbacks]);
- useLayoutEffect(() => {
- if (
- /**
- * If there is a popover component reference,
- * and this isn't the first render,
- * and the projectionMatrix has changed since last render,
- * then force the popover to reposition itself.
- */
- popoverRef.current &&
- projectionMatrixAtLastRender.current &&
- projectionMatrixAtLastRender.current !== projectionMatrix
- ) {
- popoverRef.current.positionPopoverFixed();
- }
-
- // no matter what, keep track of the last project matrix that was used to size the popover
- projectionMatrixAtLastRender.current = projectionMatrix;
- }, [projectionMatrixAtLastRender, projectionMatrix]);
- const {
- colorMap: { pillStroke: pillBorderStroke, resolverBackground: pillFill },
- } = useResolverTheme();
+ const { pillStroke: pillBorderStroke, resolverBackground: pillFill } = useColors();
const listStylesFromTheme = useMemo(() => {
return {
border: `1.5px solid ${pillBorderStroke}`,
backgroundColor: pillFill,
};
}, [pillBorderStroke, pillFill]);
- if (relatedEventStats === undefined) {
- /**
- * When called with a `menuAction`
- * Render without dropdown and call the supplied action when host button is clicked
- */
- return (
-
-
- {subMenuAssets.relatedEvents.title}
-
-
- );
- }
if (relatedEventOptions === undefined) {
return null;
}
return (
- <>
-
- {menuIsOpen ? (
-
- {relatedEventOptions
- .sort((opta, optb) => {
- return opta.optionTitle.localeCompare(optb.optionTitle);
- })
- .map((opt) => {
- return (
-
-
- {opt.prefix} {opt.optionTitle}
-
-
- );
- })}
-
- ) : null}
- >
+
+ {relatedEventOptions
+ .sort((opta, optb) => {
+ return opta.optionTitle.localeCompare(optb.optionTitle);
+ })
+ .map((opt) => {
+ return (
+
+
+ {opt.prefix} {opt.optionTitle}
+
+
+ );
+ })}
+
);
}
);
@@ -266,7 +131,7 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
flex-flow: row wrap;
background: transparent;
position: absolute;
- top: 6.5em;
+ top: 4.5em;
contain: content;
width: 12em;
z-index: 2;
@@ -301,17 +166,4 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)`
&.options .item button:active {
transform: scale(0.95);
}
-
- & .euiButton {
- background-color: ${(props) => props.buttonFill};
- border-color: ${(props) => props.buttonBorderColor};
- border-style: solid;
- border-width: 1px;
-
- &:hover,
- &:active,
- &:focus {
- background-color: ${(props) => props.buttonFill};
- }
- }
`;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx
new file mode 100644
index 00000000000000..edf551c6cbeb9a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/symbol_definitions.tsx
@@ -0,0 +1,354 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable react/display-name */
+
+import React, { memo } from 'react';
+import styled from 'styled-components';
+import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public';
+import { useSymbolIDs } from './use_symbol_ids';
+import { usePaintServerIDs } from './use_paint_server_ids';
+
+/**
+ * PaintServers: Where color palettes, gradients, patterns and other similar concerns
+ * are exposed to the component
+ */
+const PaintServers = memo(({ isDarkMode }: { isDarkMode: boolean }) => {
+ const paintServerIDs = usePaintServerIDs();
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {isDarkMode ? (
+ <>
+
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+});
+
+/**
+ * Defs entries that define shapes, masks and other spatial elements
+ */
+const SymbolsAndShapes = memo(({ isDarkMode }: { isDarkMode: boolean }) => {
+ const symbolIDs = useSymbolIDs();
+ const paintServerIDs = usePaintServerIDs();
+ return (
+ <>
+
+
+
+
+ {'Running Process'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'resolver_dark process running'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'Terminated Process'}
+
+
+
+
+
+
+
+
+ {'Terminated Trigger Process'}
+ {isDarkMode && (
+
+ )}
+
+ {!isDarkMode && (
+
+ )}
+
+
+
+
+
+
+ {'resolver active backing'}
+
+
+ >
+ );
+});
+
+/**
+ * This `` element is used to define the reusable assets for the Resolver
+ * It confers several advantages, including but not limited to:
+ * 1. Freedom of form for creative assets (beyond box-model constraints)
+ * 2. Separation of concerns between creative assets and more functional areas of the app
+ * 3. `` elements can be handled by compositor (faster)
+ */
+export const SymbolDefinitions = memo(() => {
+ const isDarkMode = useUiSetting('theme:darkMode');
+ return (
+
+
+
+
+
+
+ );
+});
+
+const HiddenSVG = styled('svg')`
+ position: absolute;
+ left: 100%;
+ top: 100%;
+ width: 0;
+ height: 0;
+`;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts
new file mode 100644
index 00000000000000..8072266f1e8c8a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json';
+import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json';
+import { useMemo } from 'react';
+import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public';
+
+type ResolverColorNames =
+ | 'descriptionText'
+ | 'full'
+ | 'graphControls'
+ | 'graphControlsBackground'
+ | 'resolverBackground'
+ | 'resolverEdge'
+ | 'resolverEdgeText'
+ | 'resolverBreadcrumbBackground'
+ | 'pillStroke'
+ | 'triggerBackingFill'
+ | 'processBackingFill';
+type ColorMap = Record;
+
+/**
+ * Get access to Kibana-theme based colors.
+ */
+export function useColors(): ColorMap {
+ const isDarkMode = useUiSetting('theme:darkMode');
+ const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
+ return useMemo(() => {
+ return {
+ descriptionText: theme.euiTextColor,
+ full: theme.euiColorFullShade,
+ graphControls: theme.euiColorDarkestShade,
+ graphControlsBackground: theme.euiColorEmptyShade,
+ processBackingFill: `${theme.euiColorPrimary}${isDarkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12%
+ resolverBackground: theme.euiColorEmptyShade,
+ resolverEdge: isDarkMode ? theme.euiColorLightShade : theme.euiColorLightestShade,
+ resolverBreadcrumbBackground: theme.euiColorLightestShade,
+ resolverEdgeText: isDarkMode ? theme.euiColorFullShade : theme.euiColorDarkShade,
+ triggerBackingFill: `${theme.euiColorDanger}${isDarkMode ? '1F' : '0F'}`,
+ pillStroke: theme.euiColorLightShade,
+ };
+ }, [isDarkMode, theme]);
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts
new file mode 100644
index 00000000000000..c743ebc43f2bed
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts
@@ -0,0 +1,121 @@
+/*
+ * 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';
+
+import { ButtonColor } from '@elastic/eui';
+import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json';
+import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json';
+import { useMemo } from 'react';
+import { ResolverProcessType } from '../types';
+import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public';
+import { useSymbolIDs } from './use_symbol_ids';
+import { useColors } from './use_colors';
+
+/**
+ * Provides colors and HTML IDs used to render the 'cube' graphic that accompanies nodes.
+ */
+export function useCubeAssets(
+ isProcessTerminated: boolean,
+ isProcessTrigger: boolean
+): NodeStyleConfig {
+ const SymbolIds = useSymbolIDs();
+ const isDarkMode = useUiSetting('theme:darkMode');
+ const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight;
+ const colorMap = useColors();
+
+ const nodeAssets: NodeStyleMap = useMemo(
+ () => ({
+ runningProcessCube: {
+ backingFill: colorMap.processBackingFill,
+ cubeSymbol: `#${SymbolIds.runningProcessCube}`,
+ descriptionFill: colorMap.descriptionText,
+ descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningProcess', {
+ defaultMessage: 'Running Process',
+ }),
+ isLabelFilled: true,
+ labelButtonFill: 'primary',
+ strokeColor: theme.euiColorPrimary,
+ },
+ runningTriggerCube: {
+ backingFill: colorMap.triggerBackingFill,
+ cubeSymbol: `#${SymbolIds.runningTriggerCube}`,
+ descriptionFill: colorMap.descriptionText,
+ descriptionText: i18n.translate('xpack.securitySolution.endpoint.resolver.runningTrigger', {
+ defaultMessage: 'Running Trigger',
+ }),
+ isLabelFilled: true,
+ labelButtonFill: 'danger',
+ strokeColor: theme.euiColorDanger,
+ },
+ terminatedProcessCube: {
+ backingFill: colorMap.processBackingFill,
+ cubeSymbol: `#${SymbolIds.terminatedProcessCube}`,
+ descriptionFill: colorMap.descriptionText,
+ descriptionText: i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.terminatedProcess',
+ {
+ defaultMessage: 'Terminated Process',
+ }
+ ),
+ isLabelFilled: false,
+ labelButtonFill: 'primary',
+ strokeColor: theme.euiColorPrimary,
+ },
+ terminatedTriggerCube: {
+ backingFill: colorMap.triggerBackingFill,
+ cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`,
+ descriptionFill: colorMap.descriptionText,
+ descriptionText: i18n.translate(
+ 'xpack.securitySolution.endpoint.resolver.terminatedTrigger',
+ {
+ defaultMessage: 'Terminated Trigger',
+ }
+ ),
+ isLabelFilled: false,
+ labelButtonFill: 'danger',
+ strokeColor: theme.euiColorDanger,
+ },
+ }),
+ [SymbolIds, colorMap, theme]
+ );
+
+ if (isProcessTerminated) {
+ if (isProcessTrigger) {
+ return nodeAssets.terminatedTriggerCube;
+ } else {
+ return nodeAssets[processTypeToCube.processTerminated];
+ }
+ } else if (isProcessTrigger) {
+ return nodeAssets[processTypeToCube.processCausedAlert];
+ } else {
+ return nodeAssets[processTypeToCube.processRan];
+ }
+}
+
+const processTypeToCube: Record = {
+ processCreated: 'runningProcessCube',
+ processRan: 'runningProcessCube',
+ processTerminated: 'terminatedProcessCube',
+ unknownProcessEvent: 'runningProcessCube',
+ processCausedAlert: 'runningTriggerCube',
+ unknownEvent: 'runningProcessCube',
+};
+interface NodeStyleMap {
+ runningProcessCube: NodeStyleConfig;
+ runningTriggerCube: NodeStyleConfig;
+ terminatedProcessCube: NodeStyleConfig;
+ terminatedTriggerCube: NodeStyleConfig;
+}
+interface NodeStyleConfig {
+ backingFill: string;
+ cubeSymbol: string;
+ descriptionFill: string;
+ descriptionText: string;
+ isLabelFilled: boolean;
+ labelButtonFill: ButtonColor;
+ strokeColor: string;
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts
new file mode 100644
index 00000000000000..0336a29bb0721f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_paint_server_ids.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useMemo } from 'react';
+
+import { useSelector } from 'react-redux';
+
+import * as selectors from '../store/selectors';
+
+/**
+ * Access the HTML IDs for this Resolver's reusable SVG 'paint servers'.
+ * In the future these IDs may come from an outside provider (and may be shared by multiple Resolver instances.)
+ */
+export function usePaintServerIDs() {
+ const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID);
+ return useMemo(() => {
+ const prefix = `${resolverComponentInstanceID}-symbols`;
+ return {
+ runningProcessCube: `${prefix}-psRunningProcessCube`,
+ runningTriggerCube: `${prefix}-psRunningTriggerCube`,
+ terminatedProcessCube: `${prefix}-psTerminatedProcessCube`,
+ terminatedTriggerCube: `${prefix}-psTerminatedTriggerCube`,
+ };
+ }, [resolverComponentInstanceID]);
+}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.ts
new file mode 100644
index 00000000000000..0e1fd5737a3ce5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/resolver/view/use_symbol_ids.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 { useMemo } from 'react';
+
+import { useSelector } from 'react-redux';
+
+import * as selectors from '../store/selectors';
+
+/**
+ * Access the HTML IDs for this Resolver's reusable SVG symbols.
+ * In the future these IDs may come from an outside provider (and may be shared by multiple Resolver instances.)
+ */
+export function useSymbolIDs() {
+ const resolverComponentInstanceID = useSelector(selectors.resolverComponentInstanceID);
+ return useMemo(() => {
+ const prefix = `${resolverComponentInstanceID}-symbols`;
+ return {
+ processNodeLabel: `${prefix}-nodeSymbol`,
+ runningProcessCube: `${prefix}-runningCube`,
+ runningTriggerCube: `${prefix}-runningTriggerCube`,
+ terminatedProcessCube: `${prefix}-terminatedCube`,
+ terminatedTriggerCube: `${prefix}-terminatedTriggerCube`,
+ processCubeActiveBacking: `${prefix}-activeBacking`,
+ };
+ }, [resolverComponentInstanceID]);
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index 29c56e8ed80b1e..fb01f922555168 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -396,6 +396,10 @@ export const getResult = (): RuleAlertType => ({
],
threshold: undefined,
timestampOverride: undefined,
+ threatFilters: undefined,
+ threatMapping: undefined,
+ threatIndex: undefined,
+ threatQuery: undefined,
references: ['http://www.example.com', 'https://ww.example.com'],
note: '# Investigative notes',
version: 1,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
index 959bf3186f1365..dd887233c36a31 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
@@ -91,6 +91,10 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
severity_mapping: severityMapping,
tags,
threat,
+ threat_filters: threatFilters,
+ threat_index: threatIndex,
+ threat_mapping: threatMapping,
+ threat_query: threatQuery,
threshold,
throttle,
timestamp_override: timestampOverride,
@@ -176,6 +180,10 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
to,
type,
threat,
+ threatFilters,
+ threatMapping,
+ threatQuery,
+ threatIndex,
threshold,
timestampOverride,
references,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
index 701e5b5e706ed3..26ab89ad8ea7cd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -78,6 +78,10 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
tags,
threat,
threshold,
+ threat_filters: threatFilters,
+ threat_index: threatIndex,
+ threat_query: threatQuery,
+ threat_mapping: threatMapping,
throttle,
timestamp_override: timestampOverride,
to,
@@ -162,6 +166,10 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
type,
threat,
threshold,
+ threatFilters,
+ threatIndex,
+ threatQuery,
+ threatMapping,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 0f44b50d4bc747..0f5d0304f5ca02 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -158,6 +158,10 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
severity_mapping: severityMapping,
tags,
threat,
+ threat_filters: threatFilters,
+ threat_index: threatIndex,
+ threat_query: threatQuery,
+ threat_mapping: threatMapping,
threshold,
timestamp_override: timestampOverride,
to,
@@ -217,7 +221,11 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
to,
type,
threat,
+ threatFilters,
+ threatIndex,
+ threatQuery,
threshold,
+ threatMapping,
timestampOverride,
references,
note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts
index 11f74c264ae0cf..2159245f2f7350 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts
@@ -19,7 +19,7 @@ import {
} from './utils';
import { getResult } from '../__mocks__/request_responses';
import { INTERNAL_IDENTIFIER } from '../../../../../common/constants';
-import { RuleTypeParams } from '../../types';
+import { PartialFilter, RuleTypeParams } from '../../types';
import { BulkError, ImportSuccessError } from '../utils';
import { getOutputRuleAlertForRest } from '../__mocks__/utils';
import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils';
@@ -30,6 +30,7 @@ import { RuleAlertType } from '../../rules/types';
import { CreateRulesBulkSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema';
import { ImportRulesSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock';
+import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping';
type PromiseFromStreams = ImportRulesSchemaDecoded | Error;
@@ -122,6 +123,55 @@ describe('utils', () => {
);
});
+ test('transforms threat_matching fields', () => {
+ const threatRule = getResult();
+ const threatFilters: PartialFilter[] = [
+ {
+ query: {
+ bool: {
+ must: [
+ {
+ query_string: {
+ query: 'host.name: linux',
+ analyze_wildcard: true,
+ time_zone: 'Zulu',
+ },
+ },
+ ],
+ filter: [],
+ should: [],
+ must_not: [],
+ },
+ },
+ },
+ ];
+ const threatMapping: ThreatMapping = [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ value: 'host.name',
+ type: 'mapping',
+ },
+ ],
+ },
+ ];
+ threatRule.params.threatIndex = 'index-123';
+ threatRule.params.threatFilters = threatFilters;
+ threatRule.params.threatMapping = threatMapping;
+ threatRule.params.threatQuery = '*:*';
+
+ const rule = transformAlertToRule(threatRule);
+ expect(rule).toEqual(
+ expect.objectContaining({
+ threat_index: 'index-123',
+ threat_filters: threatFilters,
+ threat_mapping: threatMapping,
+ threat_query: '*:*',
+ })
+ );
+ });
+
// This has to stay here until we do data migration of saved objects and lists is removed from:
// signal_params_schema.ts
test('does not leak a lists structure in the transform which would cause validation issues', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
index ee83ea91578c5b..556ea209152e6c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
@@ -145,6 +145,10 @@ export const transformAlertToRule = (
type: alert.params.type,
threat: alert.params.threat ?? [],
threshold: alert.params.threshold,
+ threat_filters: alert.params.threatFilters,
+ threat_index: alert.params.threatIndex,
+ threat_query: alert.params.threatQuery,
+ threat_mapping: alert.params.threatMapping,
throttle: ruleActions?.ruleThrottle || 'no_actions',
timestamp_override: alert.params.timestampOverride,
note: alert.params.note,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
index 1117f34b6f8c5a..95067e57868d14 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
@@ -39,6 +39,10 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
severityMapping: [],
tags: [],
threat: [],
+ threatFilters: undefined,
+ threatMapping: undefined,
+ threatQuery: undefined,
+ threatIndex: undefined,
threshold: undefined,
timestampOverride: undefined,
to: 'now',
@@ -82,6 +86,10 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
severityMapping: [],
tags: [],
threat: [],
+ threatFilters: undefined,
+ threatIndex: undefined,
+ threatMapping: undefined,
+ threatQuery: undefined,
threshold: undefined,
timestampOverride: undefined,
to: 'now',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index 0c67d9ca77146f..9ed94cd7bff2e6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -42,6 +42,10 @@ export const createRules = async ({
severityMapping,
tags,
threat,
+ threatFilters,
+ threatIndex,
+ threatQuery,
+ threatMapping,
threshold,
timestampOverride,
to,
@@ -86,6 +90,10 @@ export const createRules = async ({
severityMapping,
threat,
threshold,
+ threatFilters,
+ threatIndex,
+ threatQuery,
+ threatMapping,
timestampOverride,
to,
type,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
index 3af0c3f55b485b..59e14dcffc3c01 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
@@ -47,6 +47,10 @@ export const installPrepackagedRules = (
to,
type,
threat,
+ threat_filters: threatFilters,
+ threat_mapping: threatMapping,
+ threat_query: threatQuery,
+ threat_index: threatIndex,
threshold,
timestamp_override: timestampOverride,
references,
@@ -93,6 +97,10 @@ export const installPrepackagedRules = (
to,
type,
threat,
+ threatFilters,
+ threatMapping,
+ threatQuery,
+ threatIndex,
threshold,
timestampOverride,
references,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index b845990fd94ef9..6b851351f27f25 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -85,6 +85,13 @@ import {
BuildingBlockTypeOrUndefined,
RuleNameOverrideOrUndefined,
} from '../../../../common/detection_engine/schemas/common/schemas';
+import {
+ ThreatIndexOrUndefined,
+ ThreatQueryOrUndefined,
+ ThreatMappingOrUndefined,
+ ThreatFiltersOrUndefined,
+} from '../../../../common/detection_engine/schemas/types/threat_mapping';
+
import { AlertsClient, PartialAlert } from '../../../../../alerts/server';
import { Alert, SanitizedAlert } from '../../../../../alerts/common';
import { SIGNALS_ID } from '../../../../common/constants';
@@ -206,6 +213,10 @@ export interface CreateRulesOptions {
tags: Tags;
threat: Threat;
threshold: ThresholdOrUndefined;
+ threatFilters: ThreatFiltersOrUndefined;
+ threatIndex: ThreatIndexOrUndefined;
+ threatQuery: ThreatQueryOrUndefined;
+ threatMapping: ThreatMappingOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: To;
type: Type;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh
new file mode 100755
index 00000000000000..23c1914387c44d
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_data.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+set -e
+./check_env_variables.sh
+
+
+# Adds port mock data to a threat list for testing.
+# Example: ./create_threat_data.sh
+# Example: ./create_threat_data.sh 1000 2000
+
+START=${1:-1}
+END=${2:-1000}
+
+for (( i=$START; i<=$END; i++ ))
+do {
+curl -s -k \
+ -H "Content-Type: application/json" \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X PUT ${ELASTICSEARCH_URL}/mock-threat-list/_doc/$i \
+ --data "
+{
+ \"@timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",
+ \"source\": { \"ip\": \"127.0.0.1\", \"port\": \"${i}\" },
+ \"destination\": { \"ip\": \"127.0.0.1\", \"port\": \"${i}\" }
+}"
+} > /dev/null
+done
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh
new file mode 100755
index 00000000000000..b0ec2973b2dd96
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/create_threat_mapping.sh
@@ -0,0 +1,61 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+set -e
+./check_env_variables.sh
+
+# Add a small partial ECS based mapping of just source.ip, source.port, destination.ip, destination.port
+# dnd then adds a large volume of threat lists to it
+
+# Example: .create_threat_mapping.sh
+
+curl -s -k \
+ -H "Content-Type: application/json" \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X PUT ${ELASTICSEARCH_URL}/mock-threat-list \
+ --data '
+{
+ "mappings": {
+ "dynamic": "strict",
+ "properties": {
+ "@timestamp": {
+ "type": "date"
+ },
+ "source": {
+ "properties": {
+ "ip": {
+ "type": "ip"
+ },
+ "port": {
+ "type": "long"
+ }
+ }
+ },
+ "destination": {
+ "properties": {
+ "ip": {
+ "type": "ip"
+ },
+ "port": {
+ "type": "long"
+ }
+ }
+ },
+ "host": {
+ "properties": {
+ "name": {
+ "type": "keyword"
+ },
+ "ip" : {
+ "type" : "ip"
+ }
+ }
+ }
+ }
+ }
+}' | jq .
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh
new file mode 100755
index 00000000000000..85eac94a2991f2
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/delete_threat_list.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+#
+# 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.
+#
+
+set -e
+./check_env_variables.sh
+
+# Deletes a mock threat list
+# Example: ./delete_threat_list.sh
+
+curl -s -k \
+ -H "Content-Type: application/json" \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X DELETE ${ELASTICSEARCH_URL}/mock-threat-list \
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json
new file mode 100644
index 00000000000000..c914e568048a17
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_threat_mapping.json
@@ -0,0 +1,60 @@
+{
+ "name": "Query with a threat mapping",
+ "description": "Query with a threat mapping",
+ "rule_id": "threat-mapping",
+ "risk_score": 1,
+ "severity": "high",
+ "type": "threat_match",
+ "query": "*:*",
+ "tags": ["tag_1", "tag_2"],
+ "threat_index": "mock-threat-list",
+ "threat_query": "*:*",
+ "threat_mapping": [
+ {
+ "entries": [
+ {
+ "field": "host.name",
+ "type": "mapping",
+ "value": "host.name"
+ },
+ {
+ "field": "host.ip",
+ "type": "mapping",
+ "value": "host.ip"
+ }
+ ]
+ },
+ {
+ "entries": [
+ {
+ "field": "destination.ip",
+ "type": "mapping",
+ "value": "destination.ip"
+ },
+ {
+ "field": "destination.port",
+ "type": "mapping",
+ "value": "destination.port"
+ }
+ ]
+ },
+ {
+ "entries": [
+ {
+ "field": "source.port",
+ "type": "mapping",
+ "value": "source.port"
+ }
+ ]
+ },
+ {
+ "entries": [
+ {
+ "field": "source.ip",
+ "type": "mapping",
+ "value": "source.ip"
+ }
+ ]
+ }
+ ]
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 9d3eb29be08dde..bbdb8ea0a36ed2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -47,6 +47,10 @@ export const sampleRuleAlertParams = (
filters: undefined,
savedId: undefined,
threshold: undefined,
+ threatFilters: undefined,
+ threatQuery: undefined,
+ threatMapping: undefined,
+ threatIndex: undefined,
timelineId: undefined,
timelineTitle: undefined,
timestampOverride: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts
index 6323938d6903bf..6ce0be54a9e7b6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts
@@ -18,6 +18,7 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
import { AlertServices } from '../../../../../alerts/server';
import { PartialFilter } from '../types';
import { BadRequestError } from '../errors/bad_request_error';
+import { QueryFilter } from './types';
interface GetFilterArgs {
type: Type;
@@ -48,7 +49,7 @@ export const getFilter = async ({
type,
query,
lists,
-}: GetFilterArgs): Promise => {
+}: GetFilterArgs): Promise => {
const queryFilter = () => {
if (query != null && language != null && index != null) {
return getQueryFilter(query, language, filters || [], index, lists);
@@ -90,6 +91,7 @@ export const getFilter = async ({
switch (type) {
case 'eql':
+ case 'threat_match':
case 'threshold': {
return savedId != null ? savedQueryFilter() : queryFilter();
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 0cf0c3880fc98f..68c6a51b4e6f66 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -22,6 +22,7 @@ import uuid from 'uuid';
import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock';
import { listMock } from '../../../../../lists/server/mocks';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
+import { BulkResponse } from './types';
const buildRuleMessage = buildRuleMessageFactory({
id: 'fake id',
@@ -759,7 +760,7 @@ describe('searchAfterAndBulkCreate', () => {
],
})
.mockImplementation(() => {
- throw Error('Fake Error');
+ throw Error('Fake Error'); // throws the exception we are testing
});
listClient.getListItemByValues = jest.fn(({ value }) =>
Promise.resolve(
@@ -811,4 +812,114 @@ describe('searchAfterAndBulkCreate', () => {
expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error
expect(lastLookBackDate).toEqual(null);
});
+
+ test('it returns error array when singleSearchAfter returns errors', async () => {
+ const sampleParams = sampleRuleAlertParams(30);
+ const bulkItem: BulkResponse = {
+ took: 100,
+ errors: true,
+ items: [
+ {
+ create: {
+ _version: 1,
+ _index: 'index-123',
+ _id: 'id-123',
+ status: 201,
+ error: {
+ type: 'network',
+ reason: 'error on creation',
+ shard: 'shard-123',
+ index: 'index-123',
+ },
+ },
+ },
+ ],
+ };
+ mockService.callCluster
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)))
+ .mockResolvedValueOnce(bulkItem) // adds the response with errors we are testing
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ })
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ })
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ })
+ .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits());
+
+ const {
+ success,
+ createdSignalsCount,
+ lastLookBackDate,
+ errors,
+ } = await searchAfterAndBulkCreate({
+ ruleParams: sampleParams,
+ gap: null,
+ previousStartedAt: new Date(),
+ listClient,
+ exceptionsList: [],
+ services: mockService,
+ logger: mockLogger,
+ id: sampleRuleGuid,
+ inputIndexPattern,
+ signalsIndex: DEFAULT_SIGNALS_INDEX,
+ name: 'rule-name',
+ actions: [],
+ createdAt: '2020-01-28T15:58:34.810Z',
+ updatedAt: '2020-01-28T15:59:14.004Z',
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+ interval: '5m',
+ enabled: true,
+ pageSize: 1,
+ filter: undefined,
+ refresh: false,
+ tags: ['some fake tag 1', 'some fake tag 2'],
+ throttle: 'no_actions',
+ buildRuleMessage,
+ });
+ expect(success).toEqual(false);
+ expect(errors).toEqual(['error on creation']);
+ expect(mockService.callCluster).toHaveBeenCalledTimes(9);
+ expect(createdSignalsCount).toEqual(4);
+ expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
index be1c44de593a49..756aedd5273d39 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
@@ -51,6 +51,7 @@ export interface SearchAfterAndBulkCreateReturnType {
bulkCreateTimes: string[];
lastLookBackDate: Date | null | undefined;
createdSignalsCount: number;
+ errors: string[];
}
// search_after through documents and re-index using bulk endpoint.
@@ -81,11 +82,12 @@ export const searchAfterAndBulkCreate = async ({
buildRuleMessage,
}: SearchAfterAndBulkCreateParams): Promise => {
const toReturn: SearchAfterAndBulkCreateReturnType = {
- success: false,
+ success: true,
searchAfterTimes: [],
bulkCreateTimes: [],
lastLookBackDate: null,
createdSignalsCount: 0,
+ errors: [],
};
// sortId tells us where to start our next consecutive search_after query
@@ -111,6 +113,7 @@ export const searchAfterAndBulkCreate = async ({
if (tuple == null || tuple.to == null || tuple.from == null) {
logger.error(buildRuleMessage(`[-] malformed date tuple`));
toReturn.success = false;
+ toReturn.errors = [...new Set([...toReturn.errors, 'malformed date tuple'])];
return toReturn;
}
signalsCreatedCount = 0;
@@ -163,7 +166,6 @@ export const searchAfterAndBulkCreate = async ({
} was 0, exiting and moving on to next tuple`
)
);
- toReturn.success = true;
break;
}
toReturn.lastLookBackDate =
@@ -199,6 +201,8 @@ export const searchAfterAndBulkCreate = async ({
const {
bulkCreateDuration: bulkDuration,
createdItemsCount: createdCount,
+ success: bulkSuccess,
+ errors: bulkErrors,
} = await singleBulkCreate({
filteredEvents,
ruleParams,
@@ -229,6 +233,8 @@ export const searchAfterAndBulkCreate = async ({
logger.debug(
buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`)
);
+ toReturn.success = toReturn.success && bulkSuccess;
+ toReturn.errors = [...new Set([...toReturn.errors, ...bulkErrors])];
}
// we are guaranteed to have searchResult hits at this point
@@ -239,17 +245,16 @@ export const searchAfterAndBulkCreate = async ({
sortId = lastSortId[0];
} else {
logger.debug(buildRuleMessage('sortIds was empty on searchResult'));
- toReturn.success = true;
break;
}
- } catch (exc) {
+ } catch (exc: unknown) {
logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`));
toReturn.success = false;
+ toReturn.errors = [...new Set([...toReturn.errors, `${exc}`])];
return toReturn;
}
}
}
logger.debug(buildRuleMessage(`[+] completed bulk index of ${toReturn.createdSignalsCount}`));
- toReturn.success = true;
return toReturn;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts
index 0c56ed300cb483..c8f8341392553e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts
@@ -42,6 +42,7 @@ export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({
savedId: null,
severity: 'high',
severityMapping: null,
+ threatFilters: null,
threat: null,
timelineId: null,
timelineTitle: null,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
index d08ca90f3e3534..dbb48d59d3a3f9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
@@ -48,6 +48,10 @@ const signalSchema = schema.object({
lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.7. Once we use a migration script please remove this.
exceptions_list: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), // For backwards compatibility with customers that had a data bug in 7.8. Once we use a migration script please remove this.
exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
+ threatFilters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
+ threatIndex: schema.maybe(schema.string()),
+ threatQuery: schema.maybe(schema.string()),
+ threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
});
/**
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index b8311182f3ca8c..3ff5d5d2a6e132 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -19,7 +19,10 @@ import {
} from './utils';
import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates';
import { RuleExecutorOptions } from './types';
-import { searchAfterAndBulkCreate } from './search_after_bulk_create';
+import {
+ searchAfterAndBulkCreate,
+ SearchAfterAndBulkCreateReturnType,
+} from './search_after_bulk_create';
import { scheduleNotificationActions } from '../notifications/schedule_notification_actions';
import { RuleAlertType } from '../rules/types';
import { findMlSignals } from './find_ml_signals';
@@ -37,7 +40,6 @@ jest.mock('./utils');
jest.mock('../notifications/schedule_notification_actions');
jest.mock('./find_ml_signals');
jest.mock('./bulk_create_ml_signals');
-jest.mock('./../../../../common/detection_engine/utils');
jest.mock('../../../../common/detection_engine/parse_schedule_dates');
const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({
@@ -477,21 +479,36 @@ describe('rules_notification_alert_type', () => {
);
});
});
+
+ describe('threat match', () => {
+ it('should throw an error if threatQuery or threatIndex or threatMapping was not null', async () => {
+ const result = getResult();
+ result.params.type = 'threat_match';
+ payload = getPayload(result, alertServices) as jest.Mocked;
+ await alert.executor(payload);
+ expect(logger.error).toHaveBeenCalled();
+ expect(logger.error.mock.calls[0][0]).toContain(
+ 'An error occurred during rule execution: message: "Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping: threatQuery: "undefined" threatIndex: "undefined" threatMapping: "undefined"" name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"'
+ );
+ });
+ });
});
describe('should catch error', () => {
it('when bulk indexing failed', async () => {
- (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({
+ const result: SearchAfterAndBulkCreateReturnType = {
success: false,
searchAfterTimes: [],
bulkCreateTimes: [],
lastLookBackDate: null,
createdSignalsCount: 0,
- });
+ errors: ['Error that bubbled up.'],
+ };
+ (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue(result);
await alert.executor(payload);
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toContain(
- 'Bulk Indexing of signals failed. Check logs for further details.'
+ 'Bulk Indexing of signals failed: Error that bubbled up. name: "Detect Root/Admin Users" id: "04128c15-0d1b-4716-a4c5-46997ac7f3bd" rule id: "rule-1" signals index: ".siem-signals"'
);
expect(ruleStatusService.error).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index d48b5b434c9c0b..196c17b42221b2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -14,7 +14,11 @@ import {
SERVER_APP_ID,
} from '../../../../common/constants';
import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers';
-import { isThresholdRule, isEqlRule } from '../../../../common/detection_engine/utils';
+import {
+ isThresholdRule,
+ isEqlRule,
+ isThreatMatchRule,
+} from '../../../../common/detection_engine/utils';
import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates';
import { SetupPlugins } from '../../../plugin';
import { getInputIndex } from './get_input_output_index';
@@ -45,6 +49,7 @@ import { ruleStatusServiceFactory } from './rule_status_service';
import { buildRuleMessageFactory } from './rule_messages';
import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client';
import { getNotificationResultsLink } from '../notifications/utils';
+import { createThreatSignals } from './threat_mapping/create_threat_signals';
export const signalRulesAlertType = ({
logger,
@@ -90,6 +95,10 @@ export const signalRulesAlertType = ({
query,
to,
threshold,
+ threatFilters,
+ threatQuery,
+ threatIndex,
+ threatMapping,
type,
exceptionsList,
} = params;
@@ -101,6 +110,7 @@ export const signalRulesAlertType = ({
searchAfterTimes: [],
lastLookBackDate: null,
createdSignalsCount: 0,
+ errors: [],
};
const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient);
const ruleStatusService = await ruleStatusServiceFactory({
@@ -221,7 +231,12 @@ export const signalRulesAlertType = ({
logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`));
}
- const { success, bulkCreateDuration, createdItemsCount } = await bulkCreateMlSignals({
+ const {
+ success,
+ errors,
+ bulkCreateDuration,
+ createdItemsCount,
+ } = await bulkCreateMlSignals({
actions,
throttle,
someResult: anomalyResults,
@@ -241,6 +256,7 @@ export const signalRulesAlertType = ({
tags,
});
result.success = success;
+ result.errors = errors;
result.createdSignalsCount = createdItemsCount;
if (bulkCreateDuration) {
result.bulkCreateTimes.push(bulkCreateDuration);
@@ -274,6 +290,7 @@ export const signalRulesAlertType = ({
success,
bulkCreateDuration,
createdItemsCount,
+ errors,
} = await bulkCreateThresholdSignals({
actions,
throttle,
@@ -297,10 +314,62 @@ export const signalRulesAlertType = ({
tags,
});
result.success = success;
+ result.errors = errors;
result.createdSignalsCount = createdItemsCount;
if (bulkCreateDuration) {
result.bulkCreateTimes.push(bulkCreateDuration);
}
+ } else if (isThreatMatchRule(type)) {
+ if (
+ threatQuery == null ||
+ threatIndex == null ||
+ threatMapping == null ||
+ query == null
+ ) {
+ throw new Error(
+ [
+ 'Threat Match rule is missing threatQuery and/or threatIndex and/or threatMapping:',
+ `threatQuery: "${threatQuery}"`,
+ `threatIndex: "${threatIndex}"`,
+ `threatMapping: "${threatMapping}"`,
+ ].join(' ')
+ );
+ }
+ const inputIndex = await getInputIndex(services, version, index);
+ result = await createThreatSignals({
+ threatMapping,
+ query,
+ inputIndex,
+ type,
+ filters: filters ?? [],
+ language,
+ name,
+ savedId,
+ services,
+ exceptionItems: exceptionItems ?? [],
+ gap,
+ previousStartedAt,
+ listClient,
+ logger,
+ alertId,
+ outputIndex,
+ params,
+ searchAfterSize,
+ actions,
+ createdBy,
+ createdAt,
+ updatedBy,
+ interval,
+ updatedAt,
+ enabled,
+ refresh,
+ tags,
+ throttle,
+ threatFilters: threatFilters ?? [],
+ threatQuery,
+ buildRuleMessage,
+ threatIndex,
+ });
} else {
const inputIndex = await getInputIndex(services, version, index);
const esFilter = await getFilter({
@@ -391,7 +460,8 @@ export const signalRulesAlertType = ({
}
} else {
const errorMessage = buildRuleMessage(
- 'Bulk Indexing of signals failed. Check logs for further details.'
+ 'Bulk Indexing of signals failed:',
+ result.errors.join()
);
logger.error(errorMessage);
await ruleStatusService.error(errorMessage, {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
index 41c825ea4d9787..374b967d1e77f0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
@@ -252,11 +252,11 @@ describe('singleBulkCreate', () => {
expect(createdItemsCount).toEqual(1);
});
- test('create successful bulk create when bulk create has multiple error statuses', async () => {
+ test('create failed bulk create when bulk create has multiple error statuses', async () => {
const sampleParams = sampleRuleAlertParams();
const sampleSearchResult = sampleDocSearchResultsNoSortId;
mockService.callCluster.mockResolvedValue(sampleBulkCreateErrorResult);
- const { success, createdItemsCount } = await singleBulkCreate({
+ const { success, createdItemsCount, errors } = await singleBulkCreate({
filteredEvents: sampleSearchResult(),
ruleParams: sampleParams,
services: mockService,
@@ -275,9 +275,9 @@ describe('singleBulkCreate', () => {
tags: ['some fake tag 1', 'some fake tag 2'],
throttle: 'no_actions',
});
-
expect(mockLogger.error).toHaveBeenCalled();
- expect(success).toEqual(true);
+ expect(errors).toEqual(['[4]: internal server error']);
+ expect(success).toEqual(false);
expect(createdItemsCount).toEqual(1);
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
index be71c67615a4c6..e8f254e6a8966b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
@@ -63,6 +63,7 @@ export interface SingleBulkCreateResponse {
success: boolean;
bulkCreateDuration?: string;
createdItemsCount: number;
+ errors: string[];
}
// Bulk Index documents.
@@ -89,7 +90,7 @@ export const singleBulkCreate = async ({
logger.debug(`about to bulk create ${filteredEvents.hits.hits.length} events`);
if (filteredEvents.hits.hits.length === 0) {
logger.debug(`all events were duplicates`);
- return { success: true, createdItemsCount: 0 };
+ return { success: true, createdItemsCount: 0, errors: [] };
}
// index documents after creating an ID based on the
// source documents' originating index, and the original
@@ -138,18 +139,31 @@ export const singleBulkCreate = async ({
logger.debug(`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`);
logger.debug(`took property says bulk took: ${response.took} milliseconds`);
- if (response.errors) {
- const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
+ const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0;
+ const duplicateSignalsCount = countBy(response.items, 'create.status')['409'];
+ const errorCountByMessage = errorAggregator(response, [409]);
+
+ logger.debug(`bulk created ${createdItemsCount} signals`);
+ if (duplicateSignalsCount > 0) {
logger.debug(`ignored ${duplicateSignalsCount} duplicate signals`);
- const errorCountByMessage = errorAggregator(response, [409]);
- if (!isEmpty(errorCountByMessage)) {
- logger.error(
- `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}`
- );
- }
}
- const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0;
- logger.debug(`bulk created ${createdItemsCount} signals`);
- return { success: true, bulkCreateDuration: makeFloatString(end - start), createdItemsCount };
+ if (!isEmpty(errorCountByMessage)) {
+ logger.error(
+ `[-] bulkResponse had errors with responses of: ${JSON.stringify(errorCountByMessage)}`
+ );
+ return {
+ errors: Object.keys(errorCountByMessage),
+ success: false,
+ bulkCreateDuration: makeFloatString(end - start),
+ createdItemsCount,
+ };
+ } else {
+ return {
+ errors: [],
+ success: true,
+ bulkCreateDuration: makeFloatString(end - start),
+ createdItemsCount,
+ };
+ }
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts
new file mode 100644
index 00000000000000..b1fab34d66ab8c
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts
@@ -0,0 +1,237 @@
+/*
+ * 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 { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping';
+import { Filter } from 'src/plugins/data/common';
+
+import { SearchResponse } from 'elasticsearch';
+import { ThreatListItem } from './types';
+
+export const getThreatMappingMock = (): ThreatMapping => {
+ return [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ type: 'mapping',
+ value: 'host.name',
+ },
+ {
+ field: 'host.ip',
+ type: 'mapping',
+ value: 'host.ip',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'destination.ip',
+ type: 'mapping',
+ value: 'destination.ip',
+ },
+ {
+ field: 'destination.port',
+ type: 'mapping',
+ value: 'destination.port',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'source.port',
+ type: 'mapping',
+ value: 'source.port',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'source.ip',
+ type: 'mapping',
+ value: 'source.ip',
+ },
+ ],
+ },
+ ];
+};
+
+export const getThreatListSearchResponseMock = (): SearchResponse => ({
+ took: 0,
+ timed_out: false,
+ _shards: {
+ total: 1,
+ successful: 1,
+ failed: 0,
+ skipped: 0,
+ },
+ hits: {
+ total: 1,
+ max_score: 0,
+ hits: [
+ {
+ _index: 'index',
+ _type: 'type',
+ _id: '123',
+ _score: 0,
+ _source: getThreatListItemMock(),
+ },
+ ],
+ },
+});
+
+export const getThreatListItemMock = (): ThreatListItem => ({
+ '@timestamp': '2020-09-09T21:59:13Z',
+ host: {
+ name: 'host-1',
+ ip: '192.168.0.0.1',
+ },
+ source: {
+ ip: '127.0.0.1',
+ port: 1,
+ },
+ destination: {
+ ip: '127.0.0.1',
+ port: 1,
+ },
+});
+
+export const getFilterThreatMapping = (): ThreatMapping => [
+ {
+ entries: [
+ {
+ field: 'host.name',
+ type: 'mapping',
+ value: 'host.name',
+ },
+ {
+ field: 'host.ip',
+ type: 'mapping',
+ value: 'host.ip',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'destination.ip',
+ type: 'mapping',
+ value: 'destination.ip',
+ },
+ {
+ field: 'destination.port',
+ type: 'mapping',
+ value: 'destination.port',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'source.port',
+ type: 'mapping',
+ value: 'source.port',
+ },
+ ],
+ },
+ {
+ entries: [
+ {
+ field: 'source.ip',
+ type: 'mapping',
+ value: 'source.ip',
+ },
+ ],
+ },
+];
+
+export const getThreatMappingFilterMock = (): Filter => ({
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ },
+ query: {
+ bool: {
+ should: getThreatMappingFiltersShouldMock(),
+ minimum_should_match: 1,
+ },
+ },
+});
+
+export const getThreatMappingFiltersShouldMock = (count = 1) => {
+ return new Array(count).fill(null).map((_, index) => getThreatMappingFilterShouldMock(index + 1));
+};
+
+export const getThreatMappingFilterShouldMock = (port = 1) => ({
+ bool: {
+ should: [
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [{ match: { 'host.name': 'host-1' } }],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [{ match: { 'host.ip': '192.168.0.0.1' } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [{ match: { 'destination.ip': '127.0.0.1' } }],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [{ match: { 'destination.port': port } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [{ match: { 'source.port': port } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ {
+ bool: {
+ filter: [
+ {
+ bool: {
+ should: [{ match: { 'source.ip': '127.0.0.1' } }],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
new file mode 100644
index 00000000000000..cf4a570248c99f
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
@@ -0,0 +1,457 @@
+/*
+ * 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 {
+ ThreatMapping,
+ ThreatMappingEntries,
+} from '../../../../../common/detection_engine/schemas/types/threat_mapping';
+
+import {
+ filterThreatMapping,
+ buildThreatMappingFilter,
+ splitShouldClauses,
+ createInnerAndClauses,
+ createAndOrClauses,
+ buildEntriesMappingFilter,
+} from './build_threat_mapping_filter';
+import {
+ getThreatMappingMock,
+ getThreatListSearchResponseMock,
+ getThreatListItemMock,
+ getThreatMappingFilterMock,
+ getFilterThreatMapping,
+ getThreatMappingFiltersShouldMock,
+ getThreatMappingFilterShouldMock,
+} from './build_threat_mapping_filter.mock';
+import { BooleanFilter } from './types';
+
+describe('build_threat_mapping_filter', () => {
+ describe('buildThreatMappingFilter', () => {
+ test('it should throw if given a chunk over 1024 in size', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ expect(() =>
+ buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1025 })
+ ).toThrow('chunk sizes cannot exceed 1024 in size');
+ });
+
+ test('it should NOT throw if given a chunk under 1024 in size', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ expect(() =>
+ buildThreatMappingFilter({ threatMapping, threatList, chunkSize: 1023 })
+ ).not.toThrow();
+ });
+
+ test('it should create the correct entries when using the default mocks', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ const filter = buildThreatMappingFilter({ threatMapping, threatList });
+ expect(filter).toEqual(getThreatMappingFilterMock());
+ });
+
+ test('it should not mutate the original threatMapping', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ buildThreatMappingFilter({ threatMapping, threatList });
+ expect(threatMapping).toEqual(getThreatMappingMock());
+ });
+
+ test('it should not mutate the original threatListItem', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ buildThreatMappingFilter({ threatMapping, threatList });
+ expect(threatList).toEqual(getThreatListSearchResponseMock());
+ });
+ });
+
+ describe('filterThreatMapping', () => {
+ test('it should not remove any entries when using the default mocks', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatListItem = getThreatListItemMock();
+
+ const item = filterThreatMapping({ threatMapping, threatListItem });
+ const expected = getFilterThreatMapping();
+ expect(item).toEqual(expected);
+ });
+
+ test('it should only give one filtered element if only 1 element is defined', () => {
+ const [firstElement] = getThreatMappingMock(); // get only the first element
+ const threatListItem = getThreatListItemMock();
+
+ const item = filterThreatMapping({ threatMapping: [firstElement], threatListItem });
+ const [firstElementFilter] = getFilterThreatMapping(); // get only the first element to compare
+ expect(item).toEqual([firstElementFilter]);
+ });
+
+ test('it should not mutate the original threatMapping', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatListItem = getThreatListItemMock();
+
+ filterThreatMapping({
+ threatMapping,
+ threatListItem,
+ });
+ expect(threatMapping).toEqual(getThreatMappingMock());
+ });
+
+ test('it should not mutate the original threatListItem', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatListItem = getThreatListItemMock();
+
+ filterThreatMapping({
+ threatMapping,
+ threatListItem,
+ });
+ expect(threatListItem).toEqual(getThreatListItemMock());
+ });
+ });
+
+ describe('createInnerAndClauses', () => {
+ test('it should return two clauses given a single entry', () => {
+ const [{ entries: threatMappingEntries }] = getThreatMappingMock(); // get the first element
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
+ const {
+ bool: {
+ should: [
+ {
+ bool: { filter },
+ },
+ ],
+ },
+ } = getThreatMappingFilterShouldMock(); // get the first element
+ expect(innerClause).toEqual(filter);
+ });
+
+ test('it should return an empty array given an empty array', () => {
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createInnerAndClauses({ threatMappingEntries: [], threatListItem });
+ expect(innerClause).toEqual([]);
+ });
+
+ test('it should filter out a single unknown value', () => {
+ const [{ entries }] = getThreatMappingMock(); // get the first element
+ const threatMappingEntries: ThreatMappingEntries = [
+ ...entries,
+ {
+ field: 'host.name', // add second invalid entry which should be filtered away
+ value: 'invalid',
+ type: 'mapping',
+ },
+ ];
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
+ const {
+ bool: {
+ should: [
+ {
+ bool: { filter },
+ },
+ ],
+ },
+ } = getThreatMappingFilterShouldMock(); // get the first element
+ expect(innerClause).toEqual(filter);
+ });
+
+ test('it should filter out 2 unknown values', () => {
+ const [{ entries }] = getThreatMappingMock(); // get the first element
+ const threatMappingEntries: ThreatMappingEntries = [
+ ...entries,
+ {
+ field: 'host.name', // add second invalid entry which should be filtered away
+ value: 'invalid',
+ type: 'mapping',
+ },
+ {
+ field: 'host.ip', // add second invalid entry which should be filtered away
+ value: 'invalid',
+ type: 'mapping',
+ },
+ ];
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
+ const {
+ bool: {
+ should: [
+ {
+ bool: { filter },
+ },
+ ],
+ },
+ } = getThreatMappingFilterShouldMock(); // get the first element
+ expect(innerClause).toEqual(filter);
+ });
+
+ test('it should filter out all unknown values as an empty array', () => {
+ const threatMappingEntries: ThreatMappingEntries = [
+ {
+ field: 'host.name', // add second invalid entry which should be filtered away
+ value: 'invalid',
+ type: 'mapping',
+ },
+ {
+ field: 'host.ip', // add second invalid entry which should be filtered away
+ value: 'invalid',
+ type: 'mapping',
+ },
+ ];
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createInnerAndClauses({ threatMappingEntries, threatListItem });
+ expect(innerClause).toEqual([]);
+ });
+ });
+
+ describe('createAndOrClauses', () => {
+ test('it should return all clauses given the entries', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createAndOrClauses({ threatMapping, threatListItem });
+ expect(innerClause).toEqual(getThreatMappingFilterShouldMock());
+ });
+
+ test('it should filter out data from entries that do not have mappings', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatListItem = { ...getThreatListItemMock(), foo: 'bar' };
+ const innerClause = createAndOrClauses({ threatMapping, threatListItem });
+ expect(innerClause).toEqual(getThreatMappingFilterShouldMock());
+ });
+
+ test('it should return an empty boolean given an empty array', () => {
+ const threatListItem = getThreatListItemMock();
+ const innerClause = createAndOrClauses({ threatMapping: [], threatListItem });
+ expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } });
+ });
+
+ test('it should return an empty boolean clause given an empty object for a threat list item', () => {
+ const threatMapping = getThreatMappingMock();
+ const innerClause = createAndOrClauses({ threatMapping, threatListItem: {} });
+ expect(innerClause).toEqual({ bool: { minimum_should_match: 1, should: [] } });
+ });
+ });
+
+ describe('buildEntriesMappingFilter', () => {
+ test('it should return all clauses given the entries', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ const mapping = buildEntriesMappingFilter({
+ threatMapping,
+ threatList,
+ chunkSize: 1024,
+ });
+ const expected: BooleanFilter = {
+ bool: { should: [getThreatMappingFilterShouldMock()], minimum_should_match: 1 },
+ };
+ expect(mapping).toEqual(expected);
+ });
+
+ test('it should return empty "should" given an empty threat list', () => {
+ const threatMapping = getThreatMappingMock();
+ const threatList = getThreatListSearchResponseMock();
+ threatList.hits.hits = [];
+ const mapping = buildEntriesMappingFilter({
+ threatMapping,
+ threatList,
+ chunkSize: 1024,
+ });
+ const expected: BooleanFilter = {
+ bool: { should: [], minimum_should_match: 1 },
+ };
+ expect(mapping).toEqual(expected);
+ });
+
+ test('it should return empty "should" given an empty threat mapping', () => {
+ const threatList = getThreatListSearchResponseMock();
+ const mapping = buildEntriesMappingFilter({
+ threatMapping: [],
+ threatList,
+ chunkSize: 1024,
+ });
+ const expected: BooleanFilter = {
+ bool: { should: [], minimum_should_match: 1 },
+ };
+ expect(mapping).toEqual(expected);
+ });
+
+ test('it should ignore entries that are invalid', () => {
+ const entries: ThreatMappingEntries = [
+ {
+ field: 'host.name',
+ type: 'mapping',
+ value: 'invalid',
+ },
+ {
+ field: 'host.ip',
+ type: 'mapping',
+ value: 'invalid',
+ },
+ ];
+
+ const threatMapping: ThreatMapping = [
+ ...getThreatMappingMock(),
+ ...[
+ {
+ entries,
+ },
+ ],
+ ];
+ const threatList = getThreatListSearchResponseMock();
+ const mapping = buildEntriesMappingFilter({
+ threatMapping,
+ threatList,
+ chunkSize: 1024,
+ });
+ const expected: BooleanFilter = {
+ bool: { should: [getThreatMappingFilterShouldMock()], minimum_should_match: 1 },
+ };
+ expect(mapping).toEqual(expected);
+ });
+ });
+
+ describe('splitShouldClauses', () => {
+ test('it should NOT split a single should clause as there is nothing to split on with chunkSize 1', () => {
+ const should = getThreatMappingFiltersShouldMock();
+ const clauses = splitShouldClauses({ should, chunkSize: 1 });
+ expect(clauses).toEqual(getThreatMappingFiltersShouldMock());
+ });
+
+ test('it should NOT mutate the original should clause passed in', () => {
+ const should = getThreatMappingFiltersShouldMock();
+ expect(should).toEqual(getThreatMappingFiltersShouldMock());
+ });
+
+ test('it should NOT split a single should clause as there is nothing to split on with chunkSize 2', () => {
+ const should = getThreatMappingFiltersShouldMock();
+ const clauses = splitShouldClauses({ should, chunkSize: 2 });
+ expect(clauses).toEqual(getThreatMappingFiltersShouldMock());
+ });
+
+ test('it should return an empty array given an empty array', () => {
+ const clauses = splitShouldClauses({ should: [], chunkSize: 2 });
+ expect(clauses).toEqual([]);
+ });
+
+ test('it should split an array of size 2 into a length 2 array with chunks on "chunkSize: 1"', () => {
+ const should = getThreatMappingFiltersShouldMock(2);
+ const clauses = splitShouldClauses({ should, chunkSize: 1 });
+ expect(clauses.length).toEqual(2);
+ });
+
+ test('it should not mutate the original when splitting on chunks', () => {
+ const should = getThreatMappingFiltersShouldMock(2);
+ splitShouldClauses({ should, chunkSize: 1 });
+ expect(should).toEqual(getThreatMappingFiltersShouldMock(2));
+ });
+
+ test('it should split an array of size 2 into 2 different chunks on "chunkSize: 1"', () => {
+ const should = getThreatMappingFiltersShouldMock(2);
+ const clauses = splitShouldClauses({ should, chunkSize: 1 });
+ const expected: BooleanFilter[] = [
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(1)],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(2)],
+ minimum_should_match: 1,
+ },
+ },
+ ];
+ expect(clauses).toEqual(expected);
+ });
+
+ test('it should split an array of size 4 into 4 groups of 4 chunks on "chunkSize: 1"', () => {
+ const should = getThreatMappingFiltersShouldMock(4);
+ const clauses = splitShouldClauses({ should, chunkSize: 1 });
+ const expected: BooleanFilter[] = [
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(1)],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(2)],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(3)],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(4)],
+ minimum_should_match: 1,
+ },
+ },
+ ];
+ expect(clauses).toEqual(expected);
+ });
+
+ test('it should split an array of size 4 into 2 groups of 2 chunks on "chunkSize: 2"', () => {
+ const should = getThreatMappingFiltersShouldMock(4);
+ const clauses = splitShouldClauses({ should, chunkSize: 2 });
+ const expected: BooleanFilter[] = [
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(1), getThreatMappingFilterShouldMock(2)],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(3), getThreatMappingFilterShouldMock(4)],
+ minimum_should_match: 1,
+ },
+ },
+ ];
+ expect(clauses).toEqual(expected);
+ });
+
+ test('it should NOT split an array of size 4 into any groups on "chunkSize: 5"', () => {
+ const should = getThreatMappingFiltersShouldMock(4);
+ const clauses = splitShouldClauses({ should, chunkSize: 5 });
+ const expected: BooleanFilter[] = [
+ getThreatMappingFilterShouldMock(1),
+ getThreatMappingFilterShouldMock(2),
+ getThreatMappingFilterShouldMock(3),
+ getThreatMappingFilterShouldMock(4),
+ ];
+ expect(clauses).toEqual(expected);
+ });
+
+ test('it should split an array of size 4 into 2 groups on "chunkSize: 3"', () => {
+ const should = getThreatMappingFiltersShouldMock(4);
+ const clauses = splitShouldClauses({ should, chunkSize: 3 });
+ const expected: BooleanFilter[] = [
+ {
+ bool: {
+ should: [
+ getThreatMappingFilterShouldMock(1),
+ getThreatMappingFilterShouldMock(2),
+ getThreatMappingFilterShouldMock(3),
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ {
+ bool: {
+ should: [getThreatMappingFilterShouldMock(4)],
+ minimum_should_match: 1,
+ },
+ },
+ ];
+ expect(clauses).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
new file mode 100644
index 00000000000000..3299b6ae34e4d5
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
@@ -0,0 +1,152 @@
+/*
+ * 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 get from 'lodash/fp/get';
+import { Filter } from 'src/plugins/data/common';
+import { ThreatMapping } from '../../../../../common/detection_engine/schemas/types/threat_mapping';
+import {
+ BooleanFilter,
+ BuildEntriesMappingFilterOptions,
+ BuildThreatMappingFilterOptions,
+ CreateAndOrClausesOptions,
+ CreateInnerAndClausesOptions,
+ FilterThreatMappingOptions,
+ SplitShouldClausesOptions,
+} from './types';
+
+export const MAX_CHUNK_SIZE = 1024;
+
+export const buildThreatMappingFilter = ({
+ threatMapping,
+ threatList,
+ chunkSize,
+}: BuildThreatMappingFilterOptions): Filter => {
+ const computedChunkSize = chunkSize ?? MAX_CHUNK_SIZE;
+ if (computedChunkSize > 1024) {
+ throw new TypeError('chunk sizes cannot exceed 1024 in size');
+ }
+ const query = buildEntriesMappingFilter({
+ threatMapping,
+ threatList,
+ chunkSize: computedChunkSize,
+ });
+ const filterChunk: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ },
+ query,
+ };
+ return filterChunk;
+};
+
+/**
+ * Filters out any entries which do not include the threat list item.
+ */
+export const filterThreatMapping = ({
+ threatMapping,
+ threatListItem,
+}: FilterThreatMappingOptions): ThreatMapping =>
+ threatMapping
+ .map((threatMap) => {
+ const entries = threatMap.entries.filter((entry) => get(entry.value, threatListItem) != null);
+ return { ...threatMap, entries };
+ })
+ .filter((threatMap) => threatMap.entries.length !== 0);
+
+export const createInnerAndClauses = ({
+ threatMappingEntries,
+ threatListItem,
+}: CreateInnerAndClausesOptions): BooleanFilter[] => {
+ return threatMappingEntries.reduce((accum, threatMappingEntry) => {
+ const value = get(threatMappingEntry.value, threatListItem);
+ if (value != null) {
+ // These values could be potentially 10k+ large so mutating the array intentionally
+ accum.push({
+ bool: {
+ should: [
+ {
+ match: {
+ [threatMappingEntry.field]: value,
+ },
+ },
+ ],
+ minimum_should_match: 1,
+ },
+ });
+ }
+ return accum;
+ }, []);
+};
+
+export const createAndOrClauses = ({
+ threatMapping,
+ threatListItem,
+}: CreateAndOrClausesOptions): BooleanFilter => {
+ const should = threatMapping.reduce((accum, threatMap) => {
+ const innerAndClauses = createInnerAndClauses({
+ threatMappingEntries: threatMap.entries,
+ threatListItem,
+ });
+ if (innerAndClauses.length !== 0) {
+ // These values could be potentially 10k+ large so mutating the array intentionally
+ accum.push({
+ bool: { filter: innerAndClauses },
+ });
+ }
+ return accum;
+ }, []);
+ return { bool: { should, minimum_should_match: 1 } };
+};
+
+export const buildEntriesMappingFilter = ({
+ threatMapping,
+ threatList,
+ chunkSize,
+}: BuildEntriesMappingFilterOptions): BooleanFilter => {
+ const combinedShould = threatList.hits.hits.reduce(
+ (accum, threatListSearchItem) => {
+ const filteredEntries = filterThreatMapping({
+ threatMapping,
+ threatListItem: threatListSearchItem._source,
+ });
+ const queryWithAndOrClause = createAndOrClauses({
+ threatMapping: filteredEntries,
+ threatListItem: threatListSearchItem._source,
+ });
+ if (queryWithAndOrClause.bool.should.length !== 0) {
+ // These values can be 10k+ large, so using a push here for performance
+ accum.push(queryWithAndOrClause);
+ }
+ return accum;
+ },
+ []
+ );
+ const should = splitShouldClauses({ should: combinedShould, chunkSize });
+ return { bool: { should, minimum_should_match: 1 } };
+};
+
+export const splitShouldClauses = ({
+ should,
+ chunkSize,
+}: SplitShouldClausesOptions): BooleanFilter[] => {
+ if (should.length <= chunkSize) {
+ return should;
+ } else {
+ return should.reduce((accum, item, index) => {
+ const chunkIndex = Math.floor(index / chunkSize);
+ const currentChunk = accum[chunkIndex];
+ if (!currentChunk) {
+ // create a new element in the array at the correct spot
+ accum[chunkIndex] = { bool: { should: [], minimum_should_match: 1 } };
+ }
+ // Add to the existing array element. Using mutatious push here since these arrays can get very large such as 10k+ and this is going to be a hot code spot.
+ accum[chunkIndex].bool.should.push(item);
+ return accum;
+ }, []);
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
new file mode 100644
index 00000000000000..7542128d837698
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { getThreatList } from './get_threat_list';
+import { buildThreatMappingFilter } from './build_threat_mapping_filter';
+
+import { getFilter } from '../get_filter';
+import {
+ searchAfterAndBulkCreate,
+ SearchAfterAndBulkCreateReturnType,
+} from '../search_after_bulk_create';
+import { CreateThreatSignalOptions, ThreatListItem } from './types';
+import { combineResults } from './utils';
+
+export const createThreatSignal = async ({
+ threatMapping,
+ query,
+ inputIndex,
+ type,
+ filters,
+ language,
+ savedId,
+ services,
+ exceptionItems,
+ gap,
+ previousStartedAt,
+ listClient,
+ logger,
+ alertId,
+ outputIndex,
+ params,
+ searchAfterSize,
+ actions,
+ createdBy,
+ createdAt,
+ updatedBy,
+ interval,
+ updatedAt,
+ enabled,
+ refresh,
+ tags,
+ throttle,
+ threatFilters,
+ threatQuery,
+ buildRuleMessage,
+ threatIndex,
+ name,
+ currentThreatList,
+ currentResult,
+}: CreateThreatSignalOptions): Promise<{
+ threatList: SearchResponse;
+ results: SearchAfterAndBulkCreateReturnType;
+}> => {
+ const threatFilter = buildThreatMappingFilter({
+ threatMapping,
+ threatList: currentThreatList,
+ });
+
+ const esFilter = await getFilter({
+ type,
+ filters: [...filters, threatFilter],
+ language,
+ query,
+ savedId,
+ services,
+ index: inputIndex,
+ lists: exceptionItems,
+ });
+
+ const newResult = await searchAfterAndBulkCreate({
+ gap,
+ previousStartedAt,
+ listClient,
+ exceptionsList: exceptionItems,
+ ruleParams: params,
+ services,
+ logger,
+ id: alertId,
+ inputIndexPattern: inputIndex,
+ signalsIndex: outputIndex,
+ filter: esFilter,
+ actions,
+ name,
+ createdBy,
+ createdAt,
+ updatedBy,
+ updatedAt,
+ interval,
+ enabled,
+ pageSize: searchAfterSize,
+ refresh,
+ tags,
+ throttle,
+ buildRuleMessage,
+ });
+
+ const results = combineResults(currentResult, newResult);
+ const searchAfter = currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort;
+
+ const threatList = await getThreatList({
+ callCluster: services.callCluster,
+ exceptionItems,
+ query: threatQuery,
+ threatFilters,
+ index: [threatIndex],
+ searchAfter,
+ sortField: undefined,
+ sortOrder: undefined,
+ });
+
+ return { threatList, results };
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
new file mode 100644
index 00000000000000..9027475d71c4a4
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 { getThreatList } from './get_threat_list';
+
+import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create';
+import { CreateThreatSignalsOptions } from './types';
+import { createThreatSignal } from './create_threat_signal';
+
+export const createThreatSignals = async ({
+ threatMapping,
+ query,
+ inputIndex,
+ type,
+ filters,
+ language,
+ savedId,
+ services,
+ exceptionItems,
+ gap,
+ previousStartedAt,
+ listClient,
+ logger,
+ alertId,
+ outputIndex,
+ params,
+ searchAfterSize,
+ actions,
+ createdBy,
+ createdAt,
+ updatedBy,
+ interval,
+ updatedAt,
+ enabled,
+ refresh,
+ tags,
+ throttle,
+ threatFilters,
+ threatQuery,
+ buildRuleMessage,
+ threatIndex,
+ name,
+}: CreateThreatSignalsOptions): Promise => {
+ let results: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ bulkCreateTimes: [],
+ searchAfterTimes: [],
+ lastLookBackDate: null,
+ createdSignalsCount: 0,
+ errors: [],
+ };
+
+ let threatList = await getThreatList({
+ callCluster: services.callCluster,
+ exceptionItems,
+ threatFilters,
+ query: threatQuery,
+ index: [threatIndex],
+ searchAfter: undefined,
+ sortField: undefined,
+ sortOrder: undefined,
+ });
+
+ while (threatList.hits.hits.length !== 0 && results.createdSignalsCount <= params.maxSignals) {
+ ({ threatList, results } = await createThreatSignal({
+ threatMapping,
+ query,
+ inputIndex,
+ type,
+ filters,
+ language,
+ savedId,
+ services,
+ exceptionItems,
+ gap,
+ previousStartedAt,
+ listClient,
+ logger,
+ alertId,
+ outputIndex,
+ params,
+ searchAfterSize,
+ actions,
+ createdBy,
+ createdAt,
+ updatedBy,
+ updatedAt,
+ interval,
+ enabled,
+ tags,
+ refresh,
+ throttle,
+ threatFilters,
+ threatQuery,
+ buildRuleMessage,
+ threatIndex,
+ name,
+ currentThreatList: threatList,
+ currentResult: results,
+ }));
+ }
+ return results;
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts
new file mode 100644
index 00000000000000..f600463c213c21
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { getSortWithTieBreaker } from './get_threat_list';
+
+describe('get_threat_signals', () => {
+ describe('getSortWithTieBreaker', () => {
+ test('it should return sort field of just timestamp if given no sort order', () => {
+ const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: undefined });
+ expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]);
+ });
+
+ test('it should return sort field of timestamp with asc even if sortOrder is changed as it is hard wired in', () => {
+ const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: 'desc' });
+ expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]);
+ });
+
+ test('it should return sort field of an extra field if given one', () => {
+ const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: undefined });
+ expect(sortOrder).toEqual([{ 'some-field': 'asc', '@timestamp': 'asc' }]);
+ });
+
+ test('it should return sort field of desc if given one', () => {
+ const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: 'desc' });
+ expect(sortOrder).toEqual([{ 'some-field': 'desc', '@timestamp': 'asc' }]);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
new file mode 100644
index 00000000000000..8b381ca0d96dcc
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter';
+import {
+ GetSortWithTieBreakerOptions,
+ GetThreatListOptions,
+ SortWithTieBreaker,
+ ThreatListItem,
+} from './types';
+
+/**
+ * This should not exceed 10000 (10k)
+ */
+export const MAX_PER_PAGE = 9000;
+
+export const getThreatList = async ({
+ callCluster,
+ query,
+ index,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ exceptionItems,
+ threatFilters,
+}: GetThreatListOptions): Promise> => {
+ const calculatedPerPage = perPage ?? MAX_PER_PAGE;
+ if (calculatedPerPage > 10000) {
+ throw new TypeError('perPage cannot exceed the size of 10000');
+ }
+ const queryFilter = getQueryFilter(query, 'kuery', threatFilters, index, exceptionItems);
+ const response: SearchResponse = await callCluster('search', {
+ body: {
+ query: queryFilter,
+ search_after: searchAfter,
+ sort: getSortWithTieBreaker({ sortField, sortOrder }),
+ },
+ ignoreUnavailable: true,
+ index,
+ size: calculatedPerPage,
+ });
+ return response;
+};
+
+export const getSortWithTieBreaker = ({
+ sortField,
+ sortOrder,
+}: GetSortWithTieBreakerOptions): SortWithTieBreaker[] => {
+ const ascOrDesc = sortOrder ?? 'asc';
+ if (sortField != null) {
+ return [{ [sortField]: ascOrDesc, '@timestamp': 'asc' }];
+ } else {
+ return [{ '@timestamp': 'asc' }];
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
new file mode 100644
index 00000000000000..4c3cd9943adb47
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
@@ -0,0 +1,163 @@
+/*
+ * 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 { Duration } from 'moment';
+import { SearchResponse } from 'elasticsearch';
+import { ListClient } from '../../../../../../lists/server';
+import {
+ Type,
+ LanguageOrUndefined,
+} from '../../../../../common/detection_engine/schemas/common/schemas';
+import {
+ ThreatQuery,
+ ThreatMapping,
+ ThreatMappingEntries,
+} from '../../../../../common/detection_engine/schemas/types/threat_mapping';
+import { PartialFilter, RuleTypeParams } from '../../types';
+import { AlertServices } from '../../../../../../alerts/server';
+import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas';
+import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create';
+import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server';
+import { RuleAlertAction } from '../../../../../common/detection_engine/types';
+import { BuildRuleMessage } from '../rule_messages';
+
+export interface CreateThreatSignalsOptions {
+ threatMapping: ThreatMapping;
+ query: string;
+ inputIndex: string[];
+ type: Type;
+ filters: PartialFilter[];
+ language: LanguageOrUndefined;
+ savedId: string | undefined;
+ services: AlertServices;
+ exceptionItems: ExceptionListItemSchema[];
+ gap: Duration | null;
+ previousStartedAt: Date | null;
+ listClient: ListClient;
+ logger: Logger;
+ alertId: string;
+ outputIndex: string;
+ params: RuleTypeParams;
+ searchAfterSize: number;
+ actions: RuleAlertAction[];
+ createdBy: string;
+ createdAt: string;
+ updatedBy: string;
+ updatedAt: string;
+ interval: string;
+ enabled: boolean;
+ tags: string[];
+ refresh: false | 'wait_for';
+ throttle: string;
+ threatFilters: PartialFilter[];
+ threatQuery: ThreatQuery;
+ buildRuleMessage: BuildRuleMessage;
+ threatIndex: string;
+ name: string;
+}
+
+export interface CreateThreatSignalOptions {
+ threatMapping: ThreatMapping;
+ query: string;
+ inputIndex: string[];
+ type: Type;
+ filters: PartialFilter[];
+ language: LanguageOrUndefined;
+ savedId: string | undefined;
+ services: AlertServices;
+ exceptionItems: ExceptionListItemSchema[];
+ gap: Duration | null;
+ previousStartedAt: Date | null;
+ listClient: ListClient;
+ logger: Logger;
+ alertId: string;
+ outputIndex: string;
+ params: RuleTypeParams;
+ searchAfterSize: number;
+ actions: RuleAlertAction[];
+ createdBy: string;
+ createdAt: string;
+ updatedBy: string;
+ updatedAt: string;
+ interval: string;
+ enabled: boolean;
+ tags: string[];
+ refresh: false | 'wait_for';
+ throttle: string;
+ threatFilters: PartialFilter[];
+ threatQuery: ThreatQuery;
+ buildRuleMessage: BuildRuleMessage;
+ threatIndex: string;
+ name: string;
+ currentThreatList: SearchResponse;
+ currentResult: SearchAfterAndBulkCreateReturnType;
+}
+
+export interface BuildThreatMappingFilterOptions {
+ threatMapping: ThreatMapping;
+ threatList: SearchResponse;
+ chunkSize?: number;
+}
+
+export interface FilterThreatMappingOptions {
+ threatMapping: ThreatMapping;
+ threatListItem: ThreatListItem;
+}
+
+export interface CreateInnerAndClausesOptions {
+ threatMappingEntries: ThreatMappingEntries;
+ threatListItem: ThreatListItem;
+}
+
+export interface CreateAndOrClausesOptions {
+ threatMapping: ThreatMapping;
+ threatListItem: ThreatListItem;
+}
+
+export interface BuildEntriesMappingFilterOptions {
+ threatMapping: ThreatMapping;
+ threatList: SearchResponse;
+ chunkSize: number;
+}
+
+export interface SplitShouldClausesOptions {
+ should: BooleanFilter[];
+ chunkSize: number;
+}
+
+export interface BooleanFilter {
+ bool: { should: unknown[]; minimum_should_match: number };
+}
+
+export interface GetThreatListOptions {
+ callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
+ query: string;
+ index: string[];
+ perPage?: number;
+ searchAfter: string[] | undefined;
+ sortField: string | undefined;
+ sortOrder: 'asc' | 'desc' | undefined;
+ threatFilters: PartialFilter[];
+ exceptionItems: ExceptionListItemSchema[];
+}
+
+export interface GetSortWithTieBreakerOptions {
+ sortField: string | undefined;
+ sortOrder: 'asc' | 'desc' | undefined;
+}
+
+/**
+ * This is an ECS document being returned, but the user could return or use non-ecs based
+ * documents potentially.
+ */
+export interface ThreatListItem {
+ [key: string]: unknown;
+}
+
+export interface SortWithTieBreaker {
+ '@timestamp': 'asc';
+ [key: string]: string;
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
new file mode 100644
index 00000000000000..48bdf430b940e7
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts
@@ -0,0 +1,159 @@
+/*
+ * 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 { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create';
+
+import { calculateAdditiveMax, combineResults } from './utils';
+
+describe('utils', () => {
+ describe('calculateAdditiveMax', () => {
+ test('it should return 0 for two empty arrays', () => {
+ const max = calculateAdditiveMax([], []);
+ expect(max).toEqual(['0']);
+ });
+
+ test('it should return 10 for two arrays with the numbers 5', () => {
+ const max = calculateAdditiveMax(['5'], ['5']);
+ expect(max).toEqual(['10']);
+ });
+
+ test('it should return 5 for two arrays with second array having just 5', () => {
+ const max = calculateAdditiveMax([], ['5']);
+ expect(max).toEqual(['5']);
+ });
+
+ test('it should return 5 for two arrays with first array having just 5', () => {
+ const max = calculateAdditiveMax(['5'], []);
+ expect(max).toEqual(['5']);
+ });
+
+ test('it should return 10 for the max of the two arrays added together when the max of each array is 5, "5 + 5 = 10"', () => {
+ const max = calculateAdditiveMax(['3', '5', '1'], ['3', '5', '1']);
+ expect(max).toEqual(['10']);
+ });
+ });
+
+ describe('combineResults', () => {
+ test('it should combine two results with success set to "true" if both are "true"', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineResults(existingResult, newResult);
+ expect(combinedResults.success).toEqual(true);
+ });
+
+ test('it should combine two results with success set to "false" if one of them is "false"', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineResults(existingResult, newResult);
+ expect(combinedResults.success).toEqual(false);
+ });
+
+ test('it should use the latest date if it is set in the new result', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineResults(existingResult, newResult);
+ expect(combinedResults.lastLookBackDate?.toISOString()).toEqual('2020-09-16T03:34:32.390Z');
+ });
+
+ test('it should combine the searchAfterTimes and the bulkCreateTimes', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: [],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: [],
+ };
+ const combinedResults = combineResults(existingResult, newResult);
+ expect(combinedResults).toEqual(
+ expect.objectContaining({
+ searchAfterTimes: ['60'],
+ bulkCreateTimes: ['50'],
+ })
+ );
+ });
+
+ test('it should combine errors together without duplicates', () => {
+ const existingResult: SearchAfterAndBulkCreateReturnType = {
+ success: false,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: undefined,
+ createdSignalsCount: 3,
+ errors: ['error 1', 'error 2', 'error 3'],
+ };
+
+ const newResult: SearchAfterAndBulkCreateReturnType = {
+ success: true,
+ searchAfterTimes: ['10', '20', '30'],
+ bulkCreateTimes: ['5', '15', '25'],
+ lastLookBackDate: new Date('2020-09-16T03:34:32.390Z'),
+ createdSignalsCount: 3,
+ errors: ['error 4', 'error 1', 'error 3', 'error 5'],
+ };
+ const combinedResults = combineResults(existingResult, newResult);
+ expect(combinedResults).toEqual(
+ expect.objectContaining({
+ errors: ['error 1', 'error 2', 'error 3', 'error 4', 'error 5'],
+ })
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
new file mode 100644
index 00000000000000..38bbb70b6c4ec4
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create';
+
+/**
+ * Given two timers this will take the max of each and add them to each other and return that addition.
+ * Max(timer_array_1) + Max(timer_array_2)
+ * @param existingTimers String array of existing timers
+ * @param newTimers String array of new timers.
+ * @returns String array of the new maximum between the two timers
+ */
+export const calculateAdditiveMax = (existingTimers: string[], newTimers: string[]): string[] => {
+ const numericNewTimerMax = Math.max(0, ...newTimers.map((time) => +time));
+ const numericExistingTimerMax = Math.max(0, ...existingTimers.map((time) => +time));
+ return [String(numericNewTimerMax + numericExistingTimerMax)];
+};
+
+/**
+ * Combines two results together and returns the results combined
+ * @param currentResult The current result to combine with a newResult
+ * @param newResult The new result to combine
+ */
+export const combineResults = (
+ currentResult: SearchAfterAndBulkCreateReturnType,
+ newResult: SearchAfterAndBulkCreateReturnType
+): SearchAfterAndBulkCreateReturnType => ({
+ success: currentResult.success === false ? false : newResult.success,
+ bulkCreateTimes: calculateAdditiveMax(currentResult.bulkCreateTimes, newResult.bulkCreateTimes),
+ searchAfterTimes: calculateAdditiveMax(
+ currentResult.searchAfterTimes,
+ newResult.searchAfterTimes
+ ),
+ lastLookBackDate: newResult.lastLookBackDate,
+ createdSignalsCount: currentResult.createdSignalsCount + newResult.createdSignalsCount,
+ errors: [...new Set([...currentResult.errors, ...newResult.errors])],
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
index 700a8fb5022d72..23aa786558a996 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { DslQuery, Filter } from 'src/plugins/data/common';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { AlertType, AlertTypeState, AlertExecutorOptions } from '../../../../../alerts/server';
@@ -58,7 +59,7 @@ export interface SignalSource {
}
export interface BulkItem {
- create: {
+ create?: {
_index: string;
_type?: string;
_id: string;
@@ -166,3 +167,15 @@ export interface RuleAlertAttributes extends AlertAttributes {
}
export type BulkResponseErrorAggregation = Record;
+
+/**
+ * TODO: Remove this if/when the return filter has its own type exposed
+ */
+export interface QueryFilter {
+ bool: {
+ must: DslQuery[];
+ filter: Filter[];
+ should: unknown[];
+ must_not: Filter[];
+ };
+}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
index 9d22ba9dcc02b3..123b9c9bdffa2a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
@@ -356,6 +356,14 @@ describe('utils', () => {
expect(aggregated).toEqual(expected);
});
+ test('it should aggregate with an empty create object', () => {
+ const empty = sampleBulkResponse();
+ empty.items = [{}];
+ const aggregated = errorAggregator(empty, []);
+ const expected: BulkResponseErrorAggregation = {};
+ expect(aggregated).toEqual(expected);
+ });
+
test('it should aggregate with an empty object when given a valid bulk response with no errors', () => {
const validResponse = sampleBulkResponse();
const aggregated = errorAggregator(validResponse, []);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
index 4a6ea96e1854bb..9f1e5d69804660 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
@@ -292,7 +292,7 @@ export const errorAggregator = (
ignoreStatusCodes: number[]
): BulkResponseErrorAggregation => {
return response.items.reduce((accum, item) => {
- if (item.create.error != null && !ignoreStatusCodes.includes(item.create.status)) {
+ if (item.create?.error != null && !ignoreStatusCodes.includes(item.create.status)) {
if (accum[item.create.error.reason] == null) {
accum[item.create.error.reason] = {
count: 1,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
index cbe756064b72b2..b0554adcc46b0f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
@@ -38,6 +38,12 @@ import {
TimestampOverrideOrUndefined,
Type,
} from '../../../common/detection_engine/schemas/common/schemas';
+import {
+ ThreatIndexOrUndefined,
+ ThreatQueryOrUndefined,
+ ThreatMappingOrUndefined,
+} from '../../../common/detection_engine/schemas/types/threat_mapping';
+
import { LegacyCallAPIOptions } from '../../../../../../src/core/server';
import { Filter } from '../../../../../../src/plugins/data/server';
import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types';
@@ -73,6 +79,10 @@ export interface RuleTypeParams {
severityMapping: SeverityMappingOrUndefined;
threat: ThreatOrUndefined;
threshold: ThresholdOrUndefined;
+ threatFilters: PartialFilter[] | undefined;
+ threatIndex: ThreatIndexOrUndefined;
+ threatQuery: ThreatQueryOrUndefined;
+ threatMapping: ThreatMappingOrUndefined;
timestampOverride: TimestampOverrideOrUndefined;
to: To;
type: Type;
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index f0e7372a208fbe..0571c4878956f3 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -16,6 +16,7 @@ import {
Plugin as IPlugin,
PluginInitializerContext,
SavedObjectsClient,
+ DEFAULT_APP_CATEGORIES,
} from '../../../../src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DataPluginSetup, DataPluginStart } from '../../../../src/plugins/data/server/plugin';
@@ -178,6 +179,7 @@ export class Plugin implements IPlugin
management: {
data: [PLUGIN.id],
},
+ catalogue: [PLUGIN.id],
privileges: [
{
requiredClusterPrivileges: [...APP_REQUIRED_CLUSTER_PRIVILEGES],
diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
index c9f8431fe1ab76..9500810a395f87 100644
--- a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
+++ b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx
@@ -22,7 +22,7 @@ export const SecureSpaceMessage = (props: SecureSpaceMessageProps) => {
return (
-
+
{
description={this.getPanelDescription()}
fullWidth
>
-
-
-
-
-
-
-
-
-
-
-
- }
- closePopover={this.closePopover}
- {...extraPopoverProps}
- ownFocus={true}
- isOpen={this.state.customizingAvatar}
- >
-
-
-
-
-
-
-
+
+
+
@@ -175,6 +134,37 @@ export class CustomizeSpace extends Component {
rows={2}
/>
+
+
+
+
+
+ }
+ closePopover={this.closePopover}
+ {...extraPopoverProps}
+ ownFocus={true}
+ isOpen={this.state.customizingAvatar}
+ >
+
+
+
+
+
);
diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap
index 3835fa085c26e6..ee1eb7c5e9aba7 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap
@@ -2,14 +2,14 @@
exports[`EnabledFeatures renders as expected 1`] = `
@@ -41,7 +41,7 @@ exports[`EnabledFeatures renders as expected 1`] = `
>
@@ -63,16 +63,16 @@ exports[`EnabledFeatures renders as expected 1`] = `
@@ -89,6 +89,12 @@ exports[`EnabledFeatures renders as expected 1`] = `
Array [
Object {
"app": Array [],
+ "category": Object {
+ "euiIconType": "logoKibana",
+ "id": "kibana",
+ "label": "Kibana",
+ "order": 1000,
+ },
"icon": "spacesApp",
"id": "feature-1",
"name": "Feature 1",
@@ -96,6 +102,12 @@ exports[`EnabledFeatures renders as expected 1`] = `
},
Object {
"app": Array [],
+ "category": Object {
+ "euiIconType": "logoKibana",
+ "id": "kibana",
+ "label": "Kibana",
+ "order": 1000,
+ },
"icon": "spacesApp",
"id": "feature-2",
"name": "Feature 2",
diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx
index 0eed6793ddbe00..4b22b92cfee16a 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiLink } from '@elastic/eui';
import React from 'react';
-import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
-import { Space } from '../../../../common/model/space';
-import { SectionPanel } from '../section_panel';
+import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { EnabledFeatures } from './enabled_features';
import { KibanaFeatureConfig } from '../../../../../features/public';
+import { DEFAULT_APP_CATEGORIES } from '../../../../../../../src/core/public';
+import { findTestSubject } from 'test_utils/find_test_subject';
+import { EuiCheckboxProps } from '@elastic/eui';
const features: KibanaFeatureConfig[] = [
{
@@ -18,6 +18,7 @@ const features: KibanaFeatureConfig[] = [
name: 'Feature 1',
icon: 'spacesApp',
app: [],
+ category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
},
{
@@ -25,16 +26,11 @@ const features: KibanaFeatureConfig[] = [
name: 'Feature 2',
icon: 'spacesApp',
app: [],
+ category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
},
];
-const space: Space = {
- id: 'my-space',
- name: 'my space',
- disabledFeatures: ['feature-1', 'feature-2'],
-};
-
describe('EnabledFeatures', () => {
const getUrlForApp = (appId: string) => appId;
@@ -43,7 +39,11 @@ describe('EnabledFeatures', () => {
shallowWithIntl(
{
).toMatchSnapshot();
});
- it('allows all features to be toggled on', () => {
+ it('allows all features in a category to be toggled on', () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
);
- // expand section panel
- wrapper.find(SectionPanel).find(EuiLink).simulate('click');
-
- // Click the "Change all" link
- wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click');
+ // Click category-level toggle
+ const {
+ onChange = () => {
+ throw new Error('expected onChange to be defined');
+ },
+ } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
+ onChange({ target: { checked: true } } as any);
// Ask to show all features
- wrapper.find('button[data-test-subj="spc-toggle-all-features-show"]').simulate('click');
+ findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
expect(changeHandler).toBeCalledTimes(1);
@@ -81,27 +87,67 @@ describe('EnabledFeatures', () => {
expect(updatedSpace.disabledFeatures).toEqual([]);
});
- it('allows all features to be toggled off', () => {
+ it('allows all features in a category to be toggled off', async () => {
const changeHandler = jest.fn();
const wrapper = mountWithIntl(
);
- // expand section panel
- wrapper.find(SectionPanel).find(EuiLink).simulate('click');
+ // Click category-level toggle
+ const {
+ onChange = () => {
+ throw new Error('expected onChange to be defined');
+ },
+ } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
+ onChange({ target: { checked: false } } as any);
+
+ // Ask to show all features
+ findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
+
+ await nextTick();
+ wrapper.update();
+
+ expect(changeHandler).toBeCalledTimes(1);
+
+ const updatedSpace = changeHandler.mock.calls[0][0];
+
+ expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']);
+ });
+
+ it('allows all features to be toggled off', async () => {
+ const changeHandler = jest.fn();
+
+ const wrapper = mountWithIntl(
+
+ );
- // Click the "Change all" link
- wrapper.find('.spcToggleAllFeatures__changeAllLink').first().simulate('click');
+ // show should not be visible when all features are already visible
+ expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(0);
+ findTestSubject(wrapper, 'hideAllFeaturesLink').simulate('click');
- // Ask to hide all features
- wrapper.find('button[data-test-subj="spc-toggle-all-features-hide"]').simulate('click');
+ await nextTick();
+ wrapper.update();
expect(changeHandler).toBeCalledTimes(1);
@@ -109,4 +155,109 @@ describe('EnabledFeatures', () => {
expect(updatedSpace.disabledFeatures).toEqual(['feature-1', 'feature-2']);
});
+
+ it('allows all features to be toggled on', async () => {
+ const changeHandler = jest.fn();
+
+ const wrapper = mountWithIntl(
+
+ );
+
+ // hide should not be visible when all features are already hidden
+ expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(0);
+ findTestSubject(wrapper, 'showAllFeaturesLink').simulate('click');
+
+ await nextTick();
+ wrapper.update();
+
+ expect(changeHandler).toBeCalledTimes(1);
+
+ const updatedSpace = changeHandler.mock.calls[0][0];
+
+ expect(updatedSpace.disabledFeatures).toEqual([]);
+ });
+
+ it('displays both show and hide options when a non-zero subset of features are toggled on', async () => {
+ const wrapper = mountWithIntl(
+
+ );
+ expect(findTestSubject(wrapper, 'hideAllFeaturesLink')).toHaveLength(1);
+ expect(findTestSubject(wrapper, 'showAllFeaturesLink')).toHaveLength(1);
+ });
+
+ describe('feature category button', () => {
+ it(`does not toggle visibility when it contains more than one item`, () => {
+ const changeHandler = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+
+ findTestSubject(wrapper, `featureCategoryButton_kibana`).simulate('click');
+ expect(changeHandler).not.toHaveBeenCalled();
+ });
+
+ it('toggles item visibility when the category contains a single item', () => {
+ const changeHandler = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+
+ findTestSubject(wrapper, `featureCategoryButton_management`).simulate('click');
+ expect(changeHandler).toBeCalledTimes(1);
+
+ const updatedSpace = changeHandler.mock.calls[0][0];
+
+ expect(updatedSpace.disabledFeatures).toEqual(['feature-3']);
+ });
+ });
});
diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx
index 689bb610d5f38c..5e7629c29bbdd0 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx
@@ -34,8 +34,8 @@ export class EnabledFeatures extends Component {
return (
{
@@ -114,7 +114,7 @@ export class EnabledFeatures extends Component {
{' '}
{details}
@@ -135,16 +135,16 @@ export class EnabledFeatures extends Component {
),
diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss
new file mode 100644
index 00000000000000..4f73349edac205
--- /dev/null
+++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.scss
@@ -0,0 +1,4 @@
+.spcFeatureTableAccordionContent {
+ // Align accordion content with the feature category logo in the accordion's buttonContent
+ padding-left: $euiSizeXL;
+}
\ No newline at end of file
diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx
index 9265ca46e3a3a4..95ff475ef4e30c 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx
@@ -4,14 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } from '@elastic/eui';
+import { EuiCallOut } from '@elastic/eui';
+
+import {
+ EuiAccordion,
+ EuiCheckbox,
+ EuiCheckboxProps,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiIcon,
+ EuiLink,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
+import { AppCategory } from 'kibana/public';
import _ from 'lodash';
-import React, { ChangeEvent, Component } from 'react';
+import React, { ChangeEvent, Component, ReactElement } from 'react';
import { KibanaFeatureConfig } from '../../../../../../plugins/features/public';
import { Space } from '../../../../common/model/space';
-import { ToggleAllFeatures } from './toggle_all_features';
+import { getEnabledFeatures } from '../../lib/feature_utils';
+import './feature_table.scss';
interface Props {
space: Partial;
@@ -20,15 +35,201 @@ interface Props {
}
export class FeatureTable extends Component {
+ private featureCategories: Map = new Map();
+
+ constructor(props: Props) {
+ super(props);
+ // features are static for the lifetime of the page, so this is safe to do here in a non-reactive manner
+ props.features.forEach((feature) => {
+ if (!this.featureCategories.has(feature.category.id)) {
+ this.featureCategories.set(feature.category.id, []);
+ }
+ this.featureCategories.get(feature.category.id)!.push(feature);
+ });
+ }
+
public render() {
- const { space, features } = this.props;
+ const { space } = this.props;
+
+ const accordions: Array<{ order: number; element: ReactElement }> = [];
+ this.featureCategories.forEach((featuresInCategory) => {
+ const { category } = featuresInCategory[0];
+
+ const featureCount = featuresInCategory.length;
+ const enabledCount = getEnabledFeatures(featuresInCategory, space).length;
+
+ const canExpandCategory = featuresInCategory.length > 1;
+
+ const checkboxProps: EuiCheckboxProps = {
+ id: `featureCategoryCheckbox_${category.id}`,
+ indeterminate: enabledCount > 0 && enabledCount < featureCount,
+ checked: featureCount === enabledCount,
+ ['aria-label']: i18n.translate(
+ 'xpack.spaces.management.enabledFeatures.featureCategoryButtonLabel',
+ { defaultMessage: 'Category toggle' }
+ ),
+ onClick: (e) => {
+ // Clicking the checkbox should not cause the accordion to expand.
+ // Stopping event propagation ensures this.
+ e.stopPropagation();
+ },
+ onChange: (e) => {
+ this.setFeaturesVisibility(
+ featuresInCategory.map((f) => f.id),
+ e.target.checked
+ );
+ },
+ };
+
+ const buttonContent = (
+ {
+ if (!canExpandCategory) {
+ const isChecked = enabledCount > 0;
+ this.setFeaturesVisibility(
+ featuresInCategory.map((f) => f.id),
+ !isChecked
+ );
+ }
+ }}
+ >
+
+
+
+ {category.euiIconType ? (
+
+
+
+ ) : null}
+
+
+ {category.label}
+
+
+
+ );
+
+ const label: string = i18n.translate('xpack.spaces.management.featureAccordionSwitchLabel', {
+ defaultMessage: '{enabledCount} / {featureCount} features visible',
+ values: {
+ enabledCount,
+ featureCount,
+ },
+ });
+ const extraAction = (
+
+ {label}
+
+ );
+
+ const helpText = this.getCategoryHelpText(category);
+
+ const accordion = (
+
+
+
+ {helpText && (
+ <>
+
+ {helpText}
+
+
+ >
+ )}
+ {featuresInCategory.map((feature) => {
+ const featureChecked = !(
+ space.disabledFeatures && space.disabledFeatures.includes(feature.id)
+ );
+
+ return (
+
+
+
+
+
+ );
+ })}
+
+
+ );
+
+ accordions.push({
+ order: category.order ?? Number.MAX_SAFE_INTEGER,
+ element: accordion,
+ });
+ });
- const items = features.map((feature) => ({
- feature,
- space,
- }));
+ accordions.sort((a1, a2) => a1.order - a2.order);
- return ;
+ const featureCount = this.props.features.length;
+ const enabledCount = getEnabledFeatures(this.props.features, this.props.space).length;
+ const controls = [];
+ if (enabledCount < featureCount) {
+ controls.push(
+ this.showAll()} data-test-subj="showAllFeaturesLink">
+
+ {i18n.translate('xpack.spaces.management.selectAllFeaturesLink', {
+ defaultMessage: 'Select all',
+ })}
+
+
+ );
+ }
+ if (enabledCount > 0) {
+ controls.push(
+ this.hideAll()} data-test-subj="hideAllFeaturesLink">
+
+ {i18n.translate('xpack.spaces.management.deselectAllFeaturesLink', {
+ defaultMessage: 'Deselect all',
+ })}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {i18n.translate('xpack.spaces.management.featureVisibilityTitle', {
+ defaultMessage: 'Feature visibility',
+ })}
+
+
+
+ {controls.map((control, idx) => (
+
+ {control}
+
+ ))}
+
+
+ {accordions.flatMap((a, idx) => [
+ a.element,
+ ,
+ ])}
+
+ );
}
public onChange = (featureId: string) => (e: ChangeEvent) => {
@@ -49,67 +250,41 @@ export class FeatureTable extends Component {
this.props.onChange(updatedSpace);
};
- private onChangeAll = (visible: boolean) => {
+ private getAllFeatureIds = () =>
+ [...this.featureCategories.values()].flat().map((feature) => feature.id);
+
+ private hideAll = () => {
+ this.setFeaturesVisibility(this.getAllFeatureIds(), false);
+ };
+
+ private showAll = () => {
+ this.setFeaturesVisibility(this.getAllFeatureIds(), true);
+ };
+
+ private setFeaturesVisibility = (features: string[], visible: boolean) => {
const updatedSpace: Partial = {
...this.props.space,
};
if (visible) {
- updatedSpace.disabledFeatures = [];
+ updatedSpace.disabledFeatures = (updatedSpace.disabledFeatures ?? []).filter(
+ (df) => !features.includes(df)
+ );
} else {
- updatedSpace.disabledFeatures = this.props.features.map((feature) => feature.id);
+ updatedSpace.disabledFeatures = Array.from(
+ new Set([...(updatedSpace.disabledFeatures ?? []), ...features])
+ );
}
this.props.onChange(updatedSpace);
};
- private getColumns = () => [
- {
- field: 'feature',
- name: i18n.translate('xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle', {
- defaultMessage: 'Feature',
- }),
- render: (
- feature: KibanaFeatureConfig,
- _item: { feature: KibanaFeatureConfig; space: Props['space'] }
- ) => {
- return (
-
-
- {feature.name}
-
- );
- },
- },
- {
- field: 'space',
- width: '150',
- name: (
-
-
-
-
- ),
-
- render: (spaceEntry: Space, record: Record) => {
- const checked = !(
- spaceEntry.disabledFeatures && spaceEntry.disabledFeatures.includes(record.feature.id)
- );
-
- return (
-
- );
- },
- },
- ];
+ private getCategoryHelpText = (category: AppCategory) => {
+ if (category.id === 'management') {
+ return i18n.translate('xpack.spaces.management.managementCategoryHelpText', {
+ defaultMessage:
+ 'Access to Stack Management is determined by your privileges, and cannot be hidden by Spaces.',
+ });
+ }
+ };
}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
index f5807208488753..66f5ea87551d3f 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx
@@ -4,19 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiLink, EuiSwitch } from '@elastic/eui';
+import { EuiButton, EuiCheckboxProps } from '@elastic/eui';
import { ReactWrapper } from 'enzyme';
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal';
import { ManageSpacePage } from './manage_space_page';
-import { SectionPanel } from './section_panel';
import { spacesManagerMock } from '../../spaces_manager/mocks';
import { SpacesManager } from '../../spaces_manager';
import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mocks';
import { featuresPluginMock } from '../../../../features/public/mocks';
import { KibanaFeature } from '../../../../features/public';
+import { DEFAULT_APP_CATEGORIES } from '../../../../../../src/core/public';
// To be resolved by EUI team.
// https://github.com/elastic/eui/issues/3712
@@ -39,6 +39,7 @@ featuresStart.getFeatures.mockResolvedValue([
name: 'feature 1',
icon: 'spacesApp',
app: [],
+ category: DEFAULT_APP_CATEGORIES.kibana,
privileges: null,
}),
]);
@@ -309,16 +310,12 @@ function updateSpace(wrapper: ReactWrapper, updateFeature = true) {
}
function toggleFeature(wrapper: ReactWrapper) {
- const featureSectionButton = wrapper
- .find(SectionPanel)
- .filter('[data-test-subj="enabled-features-panel"]')
- .find(EuiLink);
-
- featureSectionButton.simulate('click');
-
- wrapper.update();
-
- wrapper.find(EuiSwitch).find('button').simulate('click');
+ const {
+ onChange = () => {
+ throw new Error('expected onChange to be defined');
+ },
+ } = wrapper.find('input#featureCategoryCheckbox_kibana').props() as EuiCheckboxProps;
+ onChange({ target: { checked: false } } as any);
wrapper.update();
}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx
index 5338710b7c8a44..6943e275015547 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx
@@ -177,11 +177,16 @@ export class ManageSpacePage extends Component {
};
public getFormHeading = () => (
-
-
- {this.getTitle()}
-
-
+
+
+
+ {this.getTitle()}
+
+
+
+
+
+
);
public getTitle = () => {
diff --git a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx
index 2d1ec727b3348b..d9ad63c30adde1 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.test.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIcon } from '@elastic/eui';
+import { EuiBadge } from '@elastic/eui';
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { ReservedSpaceBadge } from './reserved_space_badge';
@@ -24,7 +24,7 @@ const unreservedSpace = {
test('it renders without crashing', () => {
const wrapper = shallowWithIntl( );
- expect(wrapper.find(EuiIcon)).toHaveLength(1);
+ expect(wrapper.find(EuiBadge)).toHaveLength(1);
});
test('it renders nothing for an unreserved space', () => {
diff --git a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx
index 38bf3519020967..f3a2273d90e8c2 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx
@@ -6,7 +6,7 @@
import React from 'react';
-import { EuiIcon, EuiToolTip } from '@elastic/eui';
+import { EuiBadge, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { isReservedSpace } from '../../../common';
import { Space } from '../../../common/model/space';
@@ -28,7 +28,9 @@ export const ReservedSpaceBadge = (props: Props) => {
/>
}
>
-
+
+ Reserved space
+
);
}
diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
index fe4bdc865094f8..c1d19eb06c2e75 100644
--- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx
@@ -47,6 +47,7 @@ featuresStart.getFeatures.mockResolvedValue([
name: 'feature 1',
icon: 'spacesApp',
app: [],
+ category: { id: 'foo', label: 'foo' },
privileges: null,
}),
]);
diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx
index 1e8520a2617dd3..e345657a785c12 100644
--- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx
@@ -88,7 +88,11 @@ describe('spacesManagementApp', () => {
expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Spaces' }]);
expect(container).toMatchInlineSnapshot(`
- Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"securityEnabled":true}
+
+ Spaces Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}},"securityEnabled":true}
+
`);
@@ -107,7 +111,11 @@ describe('spacesManagementApp', () => {
]);
expect(container).toMatchInlineSnapshot(`
- Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"securityEnabled":true}
+
+ Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/create","search":"","hash":""}},"securityEnabled":true}
+
`);
@@ -128,7 +136,11 @@ describe('spacesManagementApp', () => {
]);
expect(container).toMatchInlineSnapshot(`
- Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"securityEnabled":true}
+
+ Spaces Edit Page: {"capabilities":{"catalogue":{},"management":{},"navLinks":{}},"notifications":{"toasts":{}},"spacesManager":{"onActiveSpaceChange$":{"_isScalar":false}},"spaceId":"some-space","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/some-space","search":"","hash":""}},"securityEnabled":true}
+
`);
diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx
index 5b8b993d96adc0..a328c50af4e7a6 100644
--- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx
+++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx
@@ -9,6 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom';
import { Router, Route, Switch, useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { StartServicesAccessor } from 'src/core/public';
+import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
import { SecurityLicense } from '../../../security/public';
import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public';
import { PluginsStart } from '../plugin';
@@ -32,6 +33,7 @@ export const spacesManagementApp = Object.freeze({
title: i18n.translate('xpack.spaces.displayName', {
defaultMessage: 'Spaces',
}),
+
async mount({ element, setBreadcrumbs, history }) {
const [
{ notifications, i18n: i18nStart, application },
@@ -114,19 +116,21 @@ export const spacesManagementApp = Object.freeze({
render(
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
element
);
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 06fbed44438ffd..11acd28c1795ec 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8677,8 +8677,7 @@
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング",
"xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。",
"xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)",
- "xpack.infra.metrics.alertFlyout.firedTime": "時間",
- "xpack.infra.metrics.alertFlyout.firedTimes": "回数",
+ "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 時間} other {# 回数}}",
"xpack.infra.metrics.alertFlyout.hourLabel": "時間",
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日",
"xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間",
@@ -13937,8 +13936,6 @@
"xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました。{encryptionKey}が設定されていることを確認してこのレポートを再生成してください。{err}",
"xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "ジョブヘッダーがありません",
"xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana の高度な設定「{dateFormatTimezone}」が「ブラウザー」に設定されていますあいまいさを避けるために日付は UTC 形式に変換されます。",
- "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "レポートジョブデータの解読に失敗しました{encryptionKey} が設定されていることを確認してこのレポートを再生成してください。{err}",
- "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "ジョブヘッダーがありません",
"xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSVには、値がエスケープされた式が含まれる場合があります",
"xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage": "次の Elasticsearch からの応答で期待される {hits}: {response}",
"xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage": "次の Elasticsearch からの応答で期待される {scrollId}: {response}",
@@ -17539,13 +17536,10 @@
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(表示されているすべての機能)",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能の表示をカスタマイズ",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースでどの機能が表示されるかを管理します。",
- "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "セキュアなアクセスをご希望の場合は、{rolesLink} にアクセスしてください。",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(表示されている機能がありません)",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。",
"xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "ロール",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({featureCount} 件中 {enabledCount} 件の機能を表示中)",
- "xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "表示しますか?",
- "xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "機能",
"xpack.spaces.management.hideAllFeaturesText": "すべて非表示",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "アバター",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "素晴らしいスペース",
@@ -17553,10 +17547,8 @@
"xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "クリックしてこのスペースのアバターをカスタマイズします",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "スペースを作成",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "スペースの作成",
- "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "スペースに名前を付けてアバターをカスタマイズします",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "URL 識別子に注意してください。スペースの作成後に変更することはできません。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 識別子は変更できません。",
- "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "スペースのカスタマイズ",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "表示される機能のカスタマイズ",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "スペースの読み込み中にエラーが発生: {message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "スペースの保存中にエラーが発生: {message}",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 80d4f550a10155..41075a0faa081e 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8683,8 +8683,7 @@
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当",
"xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。",
"xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)",
- "xpack.infra.metrics.alertFlyout.firedTime": "次",
- "xpack.infra.metrics.alertFlyout.firedTimes": "次",
+ "xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 次} other {# 次}}",
"xpack.infra.metrics.alertFlyout.hourLabel": "小时",
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天",
"xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时",
@@ -13946,8 +13945,6 @@
"xpack.reporting.exportTypes.common.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}",
"xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage": "作业标头缺失",
"xpack.reporting.exportTypes.csv.executeJob.dateFormateSetting": "Kibana 高级设置“{dateFormatTimezone}”已设置为“浏览器”。日期将格式化为 UTC 以避免混淆。",
- "xpack.reporting.exportTypes.csv.executeJob.failedToDecryptReportJobDataErrorMessage": "无法解密报告作业数据。请确保已设置 {encryptionKey},然后重新生成此报告。{err}",
- "xpack.reporting.exportTypes.csv.executeJob.missingJobHeadersErrorMessage": "作业标头缺失",
"xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues": "CSV 可能包含值已转义的公式",
"xpack.reporting.exportTypes.csv.hitIterator.expectedHitsErrorMessage": "在以下 Elasticsearch 响应中预期 {hits}:{response}",
"xpack.reporting.exportTypes.csv.hitIterator.expectedScrollIdErrorMessage": "在以下 Elasticsearch 响应中预期 {scrollId}:{response}",
@@ -17549,13 +17546,10 @@
"xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(所有可见功能)",
"xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "定制功能显示",
"xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "控制哪些功能在此工作区中可见。",
- "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "想保护访问?前往 {rolesLink}。",
"xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(没有可见功能)",
"xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。",
"xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "角色",
"xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({enabledCount} / {featureCount} 个功能可见)",
- "xpack.spaces.management.enabledSpaceFeaturesEnabledColumnTitle": "显示?",
- "xpack.spaces.management.enabledSpaceFeaturesFeatureColumnTitle": "功能",
"xpack.spaces.management.hideAllFeaturesText": "全部隐藏",
"xpack.spaces.management.manageSpacePage.avatarFormRowLabel": "头像",
"xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间",
@@ -17563,10 +17557,8 @@
"xpack.spaces.management.manageSpacePage.clickToCustomizeTooltip": "单击可定制此工作区头像",
"xpack.spaces.management.manageSpacePage.createSpaceButton": "创建工作区",
"xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间",
- "xpack.spaces.management.manageSpacePage.customizeSpacePanelDescription": "命名您的工作区并定制其头像。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierEditable": "记下 URL 标识符。创建工作区后,将不能更改它。",
"xpack.spaces.management.manageSpacePage.customizeSpacePanelUrlIdentifierNotEditable": "URL 标识符无法更改。",
- "xpack.spaces.management.manageSpacePage.customizeSpaceTitle": "定制您的工作区",
"xpack.spaces.management.manageSpacePage.customizeVisibleFeatures": "定制可见功能",
"xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}",
"xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
index 6177262557e076..c69c33c0fe22ee 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx
@@ -248,6 +248,7 @@ export const AlertForm = ({
{
alertParams: AlertParamsType;
alertInterval: string;
+ alertThrottle: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: IErrorObject;
diff --git a/x-pack/plugins/ui_actions_enhanced/.eslintrc.json b/x-pack/plugins/ui_actions_enhanced/.eslintrc.json
new file mode 100644
index 00000000000000..2aab6c2d9093b6
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "@typescript-eslint/consistent-type-definitions": 0
+ }
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/common/types.ts b/x-pack/plugins/ui_actions_enhanced/common/types.ts
new file mode 100644
index 00000000000000..1150f4f823e8e4
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/common/types.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
+
+export type BaseActionConfig = SerializableState;
+
+export type SerializedAction = {
+ readonly factoryId: string;
+ readonly name: string;
+ readonly config: Config;
+};
+
+/**
+ * Serialized representation of a triggers-action pair, used to persist in storage.
+ */
+export type SerializedEvent = {
+ eventId: string;
+ triggers: string[];
+ action: SerializedAction;
+};
+
+export type DynamicActionsState = {
+ events: SerializedEvent[];
+};
diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json
index 108c66505f25c6..5435019f216f2a 100644
--- a/x-pack/plugins/ui_actions_enhanced/kibana.json
+++ b/x-pack/plugins/ui_actions_enhanced/kibana.json
@@ -7,7 +7,7 @@
"uiActions",
"licensing"
],
- "server": false,
+ "server": true,
"ui": true,
"requiredBundles": [
"kibanaUtils",
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
index a3f6cac3ba1b48..ca7f6af4f7a374 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
@@ -31,7 +31,7 @@ import {
txtTriggerPickerHelpTooltip,
} from './i18n';
import './action_wizard.scss';
-import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions';
+import { ActionFactory, BaseActionConfig, BaseActionFactoryContext } from '../../dynamic_actions';
import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public';
export interface ActionWizardProps<
@@ -57,12 +57,12 @@ export interface ActionWizardProps<
/**
* current config for currently selected action factory
*/
- config?: object;
+ config?: BaseActionConfig;
/**
* config changed
*/
- onConfigChange: (config: object) => void;
+ onConfigChange: (config: BaseActionConfig) => void;
/**
* Context will be passed into ActionFactory's methods
@@ -219,9 +219,9 @@ interface SelectedActionFactoryProps<
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> {
actionFactory: ActionFactory;
- config: object;
+ config: BaseActionConfig;
context: ActionFactoryContext;
- onConfigChange: (config: object) => void;
+ onConfigChange: (config: BaseActionConfig) => void;
showDeselect: boolean;
onDeselect: () => void;
allTriggers: TriggerId[];
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
index 71286e9a59c06e..af930bfba6b8b6 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
@@ -8,7 +8,7 @@ import React, { useState } from 'react';
import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { ActionWizard } from './action_wizard';
-import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions';
+import { ActionFactory, ActionFactoryDefinition, BaseActionConfig } from '../../dynamic_actions';
import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
import { licensingMock } from '../../../../licensing/public/mocks';
import {
@@ -19,18 +19,16 @@ import {
VALUE_CLICK_TRIGGER,
} from '../../../../../../src/plugins/ui_actions/public';
-type ActionBaseConfig = object;
-
export const dashboards = [
{ id: 'dashboard1', title: 'Dashboard 1' },
{ id: 'dashboard2', title: 'Dashboard 2' },
];
-interface DashboardDrilldownConfig {
+type DashboardDrilldownConfig = {
dashboardId?: string;
useCurrentFilters: boolean;
useCurrentDateRange: boolean;
-}
+};
function DashboardDrilldownCollectConfig(props: CollectConfigProps) {
const config = props.config ?? {
@@ -121,10 +119,11 @@ export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactor
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
});
-interface UrlDrilldownConfig {
+type UrlDrilldownConfig = {
url: string;
openInNewTab: boolean;
-}
+};
+
function UrlDrilldownCollectConfig(props: CollectConfigProps) {
const config = props.config ?? {
url: '',
@@ -182,6 +181,10 @@ export const urlFactory = new ActionFactory(urlDrilldownActionFactory, {
getFeatureUsageStart: () => licensingMock.createStart().featureUsage,
});
+export const mockActionFactories: ActionFactory[] = ([dashboardFactory, urlFactory] as Array<
+ ActionFactory
+>) as ActionFactory[];
+
export const mockSupportedTriggers: TriggerId[] = [
VALUE_CLICK_TRIGGER,
SELECT_RANGE_TRIGGER,
@@ -210,7 +213,7 @@ export const mockGetTriggerInfo = (triggerId: TriggerId): Trigger => {
export function Demo({ actionFactories }: { actionFactories: Array> }) {
const [state, setState] = useState<{
currentActionFactory?: ActionFactory;
- config?: ActionBaseConfig;
+ config?: BaseActionConfig;
selectedTriggers?: TriggerId[];
}>({});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx
index f7284539ab2fee..daa56354289cf7 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx
@@ -8,14 +8,13 @@ import * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
-import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data';
+import { mockActionFactories } from '../../../components/action_wizard/test_data';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage';
import { mockDynamicActionManager } from './test_data';
-import { ActionFactory } from '../../../dynamic_actions';
const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
- actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory],
+ actionFactories: mockActionFactories,
storage: new Storage(new StubBrowserStorage()),
toastService: {
addError: (...args: any[]) => {
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx
index 2412cdd51748c0..c4b07fa05c3c18 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx
@@ -8,10 +8,9 @@ import React from 'react';
import { cleanup, fireEvent, render, wait } from '@testing-library/react/pure';
import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns';
import {
- dashboardFactory,
mockGetTriggerInfo,
mockSupportedTriggers,
- urlFactory,
+ mockActionFactories,
} from '../../../components/action_wizard/test_data';
import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
@@ -21,12 +20,11 @@ import { WELCOME_MESSAGE_TEST_SUBJ } from '../drilldown_hello_bar';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { NotificationsStart } from 'kibana/public';
import { toastDrilldownsCRUDError } from './i18n';
-import { ActionFactory } from '../../../dynamic_actions';
const storage = new Storage(new StubBrowserStorage());
const toasts = coreMock.createStart().notifications.toasts;
const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({
- actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory],
+ actionFactories: mockActionFactories,
storage: new Storage(new StubBrowserStorage()),
toastService: toasts,
getTrigger: mockGetTriggerInfo,
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
index 6f9eccde8bdb07..28a0990cf75262 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
@@ -25,6 +25,7 @@ import {
} from './i18n';
import {
ActionFactory,
+ BaseActionConfig,
BaseActionFactoryContext,
DynamicActionManager,
SerializedAction,
@@ -127,7 +128,7 @@ export function createFlyoutManageDrilldowns({
return {
actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId],
- actionConfig: drilldownToEdit.action.config as object,
+ actionConfig: drilldownToEdit.action.config as BaseActionConfig,
name: drilldownToEdit.action.name,
selectedTriggers: (drilldownToEdit.triggers ?? []) as TriggerId[],
};
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts
index 58c36e36481b88..78eec05eb2d0bd 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/test_data.ts
@@ -60,7 +60,7 @@ class MockDynamicActionManager implements PublicMethodsOf
async updateEvent(
eventId: string,
- action: UiActionsEnhancedSerializedAction,
+ action: UiActionsEnhancedSerializedAction,
triggers: Array
) {
const state = this.state.get();
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx
index 8f73c2b3b3cc97..2f5f7760d40bdf 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx
@@ -8,8 +8,7 @@ import * as React from 'react';
import { EuiFlyout } from '@elastic/eui';
import { storiesOf } from '@storybook/react';
import { FlyoutDrilldownWizard } from './index';
-import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data';
-import { ActionFactory } from '../../../dynamic_actions';
+import { mockActionFactories } from '../../../components/action_wizard/test_data';
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
const otherProps = {
@@ -24,23 +23,12 @@ const otherProps = {
storiesOf('components/FlyoutDrilldownWizard', module)
.add('default', () => {
- return (
-
- );
+ return ;
})
.add('open in flyout - create', () => {
return (
{}}>
-
+
);
})
@@ -48,13 +36,10 @@ storiesOf('components/FlyoutDrilldownWizard', module)
return (
{}}>
{}}>
{
+export interface DrilldownWizardConfig {
name: string;
actionFactory?: ActionFactory;
actionConfig?: ActionConfig;
@@ -28,7 +32,7 @@ export interface DrilldownWizardConfig {
}
export interface FlyoutDrilldownWizardProps<
- CurrentActionConfig extends object = object,
+ CurrentActionConfig extends BaseActionConfig = BaseActionConfig,
ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext
> {
drilldownActionFactories: ActionFactory[];
@@ -71,7 +75,7 @@ function useWizardConfigState(
DrilldownWizardConfig,
{
setName: (name: string) => void;
- setActionConfig: (actionConfig: object) => void;
+ setActionConfig: (actionConfig: BaseActionConfig) => void;
setActionFactory: (actionFactory?: ActionFactory) => void;
setSelectedTriggers: (triggers?: TriggerId[]) => void;
}
@@ -100,7 +104,7 @@ function useWizardConfigState(
name,
});
},
- setActionConfig: (actionConfig: object) => {
+ setActionConfig: (actionConfig: BaseActionConfig) => {
setWizardConfig({
...wizardConfig,
actionConfig,
@@ -108,12 +112,12 @@ function useWizardConfigState(
},
setActionFactory: (actionFactory?: ActionFactory) => {
if (actionFactory) {
+ const actionConfig = (actionConfigCache[actionFactory.id] ??
+ actionFactory.createConfig(actionFactoryContext)) as BaseActionConfig;
setWizardConfig({
...wizardConfig,
actionFactory,
- actionConfig:
- actionConfigCache[actionFactory.id] ??
- actionFactory.createConfig(actionFactoryContext),
+ actionConfig,
selectedTriggers: [],
});
} else {
@@ -141,7 +145,9 @@ function useWizardConfigState(
];
}
-export function FlyoutDrilldownWizard({
+export function FlyoutDrilldownWizard<
+ CurrentActionConfig extends BaseActionConfig = BaseActionConfig
+>({
onClose,
onBack,
onSubmit = () => {},
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
index d7f94a52088b7f..45655c2634fe74 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
@@ -8,7 +8,11 @@ import React from 'react';
import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
-import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions';
+import {
+ ActionFactory,
+ BaseActionConfig,
+ BaseActionFactoryContext,
+} from '../../../dynamic_actions';
import { ActionWizard } from '../../../components/action_wizard';
import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public';
@@ -26,8 +30,8 @@ export interface FormDrilldownWizardProps<
onActionFactoryChange?: (actionFactory?: ActionFactory) => void;
actionFactoryContext: ActionFactoryContext;
- actionConfig?: object;
- onActionConfigChange?: (config: object) => void;
+ actionConfig?: BaseActionConfig;
+ onActionConfigChange?: (config: BaseActionConfig) => void;
actionFactories?: ActionFactory[];
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
index f5e565d4090ff0..b55b4b87ebccd2 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
@@ -4,10 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ActionFactoryDefinition, BaseActionFactoryContext } from '../dynamic_actions';
+import {
+ ActionFactoryDefinition,
+ BaseActionConfig,
+ BaseActionFactoryContext,
+ SerializedEvent,
+} from '../dynamic_actions';
import { LicenseType } from '../../../licensing/public';
import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public';
import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public';
+import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common';
/**
* This is a convenience interface to register a drilldown. Drilldown has
@@ -24,13 +30,13 @@ import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/pu
*/
export interface DrilldownDefinition<
- Config extends object = object,
+ Config extends BaseActionConfig = BaseActionConfig,
SupportedTriggers extends TriggerId = TriggerId,
FactoryContext extends BaseActionFactoryContext = {
triggers: SupportedTriggers[];
},
ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
-> {
+> extends PersistableStateDefinition {
/**
* Globally unique identifier for this drilldown.
*/
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
index 31c7481c9d63eb..fb7d96aaf8325f 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts
@@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export interface UrlDrilldownConfig {
+export type UrlDrilldownConfig = {
url: { format?: 'handlebars_v1'; template: string };
openInNewTab: boolean;
-}
+};
/**
* URL drilldown has 3 sources for variables: global, context and event variables
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
index 3ad6d4ee397493..57c8733ed44fcc 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
@@ -12,9 +12,16 @@ import {
} from '../../../../../src/plugins/ui_actions/public';
import { ActionFactoryDefinition } from './action_factory_definition';
import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
-import { BaseActionFactoryContext, SerializedAction } from './types';
+import {
+ BaseActionConfig,
+ BaseActionFactoryContext,
+ SerializedAction,
+ SerializedEvent,
+} from './types';
import { ILicense, LicensingPluginStart } from '../../../licensing/public';
import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
+import { SavedObjectReference } from '../../../../../src/core/types';
+import { PersistableState } from '../../../../../src/plugins/kibana_utils/common';
export interface ActionFactoryDeps {
readonly getLicense: () => ILicense;
@@ -22,13 +29,16 @@ export interface ActionFactoryDeps {
}
export class ActionFactory<
- Config extends object = object,
+ Config extends BaseActionConfig = BaseActionConfig,
SupportedTriggers extends TriggerId = TriggerId,
FactoryContext extends BaseActionFactoryContext = {
triggers: SupportedTriggers[];
},
ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
-> implements Omit, 'getHref'>, Configurable {
+> implements
+ Omit, 'getHref'>,
+ Configurable,
+ PersistableState {
constructor(
protected readonly def: ActionFactoryDefinition<
Config,
@@ -121,4 +131,16 @@ export class ActionFactory<
);
});
}
+
+ public telemetry(state: SerializedEvent, telemetryData: Record) {
+ return this.def.telemetry ? this.def.telemetry(state, telemetryData) : {};
+ }
+
+ public extract(state: SerializedEvent) {
+ return this.def.extract ? this.def.extract(state) : { state, references: [] };
+ }
+
+ public inject(state: SerializedEvent, references: SavedObjectReference[]) {
+ return this.def.inject ? this.def.inject(state, references) : state;
+ }
}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
index 7ec6b21485747b..b4df1f827a2a3f 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
@@ -5,7 +5,12 @@
*/
import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
-import { BaseActionFactoryContext, SerializedAction } from './types';
+import {
+ BaseActionConfig,
+ BaseActionFactoryContext,
+ SerializedAction,
+ SerializedEvent,
+} from './types';
import { LicenseType } from '../../../licensing/public';
import {
TriggerContextMapping,
@@ -13,19 +18,21 @@ import {
UiActionsActionDefinition as ActionDefinition,
UiActionsPresentable as Presentable,
} from '../../../../../src/plugins/ui_actions/public';
+import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common';
/**
* This is a convenience interface for registering new action factories.
*/
export interface ActionFactoryDefinition<
- Config extends object = object,
+ Config extends BaseActionConfig = BaseActionConfig,
SupportedTriggers extends TriggerId = TriggerId,
FactoryContext extends BaseActionFactoryContext = {
triggers: SupportedTriggers[];
},
ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers]
> extends Partial, 'getHref'>>,
- Configurable {
+ Configurable,
+ PersistableStateDefinition {
/**
* Unique ID of the action factory. This ID is used to identify this action
* factory in the registry as well as to construct actions of this type and
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts
new file mode 100644
index 00000000000000..7cac49624bfddc
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EnhancementRegistryDefinition } from '../../../../../src/plugins/embeddable/public';
+import { SavedObjectReference } from '../../../../../src/core/types';
+import { SerializableState } from '../../../../../src/plugins/kibana_utils/common';
+import { DynamicActionsState } from '../../../ui_actions_enhanced/public';
+import { UiActionsServiceEnhancements } from '../services';
+
+export const dynamicActionEnhancement = (
+ uiActionsEnhanced: UiActionsServiceEnhancements
+): EnhancementRegistryDefinition => {
+ return {
+ id: 'dynamicActions',
+ telemetry: (state: SerializableState, telemetryData: Record) => {
+ return uiActionsEnhanced.telemetry(state as DynamicActionsState, telemetryData);
+ },
+ extract: (state: SerializableState) => {
+ return uiActionsEnhanced.extract(state as DynamicActionsState);
+ },
+ inject: (state: SerializableState, references: SavedObjectReference[]) => {
+ return uiActionsEnhanced.inject(state as DynamicActionsState, references);
+ },
+ } as EnhancementRegistryDefinition;
+};
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
index 39d9dfeca2fd6c..83232bbce1ba7b 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
@@ -250,7 +250,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition1);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -277,7 +277,7 @@ describe('DynamicActionManager', () => {
test('adds event to UI state', async () => {
const { manager, uiActions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -296,7 +296,7 @@ describe('DynamicActionManager', () => {
test('optimistically adds event to UI state', async () => {
const { manager, uiActions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -319,7 +319,7 @@ describe('DynamicActionManager', () => {
test('instantiates event in actions service', async () => {
const { manager, uiActions, actions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -348,7 +348,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition1);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -361,7 +361,7 @@ describe('DynamicActionManager', () => {
test('does not add even to UI state', async () => {
const { manager, storage, uiActions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -380,7 +380,7 @@ describe('DynamicActionManager', () => {
test('optimistically adds event to UI state and then removes it', async () => {
const { manager, storage, uiActions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -406,7 +406,7 @@ describe('DynamicActionManager', () => {
test('does not instantiate event in actions service', async () => {
const { manager, storage, uiActions, actions } = setup([]);
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -432,7 +432,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition1);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition1.id,
name: 'foo',
config: {},
@@ -457,7 +457,7 @@ describe('DynamicActionManager', () => {
expect(registeredAction1.getDisplayName()).toBe('Action 3');
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -479,7 +479,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition2);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -505,7 +505,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition2);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -524,7 +524,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition2);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -552,7 +552,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition2);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -580,7 +580,7 @@ describe('DynamicActionManager', () => {
expect(registeredAction1.getDisplayName()).toBe('Action 3');
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
@@ -604,7 +604,7 @@ describe('DynamicActionManager', () => {
uiActions.registerActionFactory(actionFactoryDefinition2);
await manager.start();
- const action: SerializedAction = {
+ const action: SerializedAction = {
factoryId: actionFactoryDefinition2.id,
name: 'foo',
config: {},
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
index 6ca388281ad769..471b929fdbc068 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
@@ -74,7 +74,7 @@ export class DynamicActionManager {
const actionId = this.generateActionId(eventId);
const factory = uiActions.getActionFactory(event.action.factoryId);
- const actionDefinition: ActionDefinition = factory.create(action as SerializedAction);
+ const actionDefinition: ActionDefinition = factory.create(action as SerializedAction);
uiActions.registerAction({
...actionDefinition,
id: actionId,
@@ -195,10 +195,7 @@ export class DynamicActionManager {
* @param action Dynamic action for which to create an event.
* @param triggers List of triggers to which action should react.
*/
- public async createEvent(
- action: SerializedAction,
- triggers: Array
- ) {
+ public async createEvent(action: SerializedAction, triggers: Array) {
const event: SerializedEvent = {
eventId: uuidv4(),
triggers,
@@ -231,7 +228,7 @@ export class DynamicActionManager {
*/
public async updateEvent(
eventId: string,
- action: SerializedAction,
+ action: SerializedAction,
triggers: Array
) {
const event: SerializedEvent = {
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts
index d00db0d9acb7a2..28d104093f64f7 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts
@@ -5,21 +5,9 @@
*/
import { TriggerId } from '../../../../../src/plugins/ui_actions/public';
+import { SerializedAction, SerializedEvent, BaseActionConfig } from '../../common/types';
-export interface SerializedAction {
- readonly factoryId: string;
- readonly name: string;
- readonly config: Config;
-}
-
-/**
- * Serialized representation of a triggers-action pair, used to persist in storage.
- */
-export interface SerializedEvent {
- eventId: string;
- triggers: string[];
- action: SerializedAction;
-}
+export { SerializedAction, SerializedEvent, BaseActionConfig };
/**
* Action factory context passed into ActionFactories' CollectConfig, getDisplayName, getIconType
diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts
index 4a899b24852a96..ae720598ec7599 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/index.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts
@@ -29,7 +29,10 @@ export {
DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState,
MemoryActionStorage as UiActionsEnhancedMemoryActionStorage,
BaseActionFactoryContext as UiActionsEnhancedBaseActionFactoryContext,
+ BaseActionConfig as UiActionsEnhancedBaseActionConfig,
} from './dynamic_actions';
+export { DynamicActionsState } from './services/ui_actions_service_enhancements';
+
export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns';
export * from './drilldowns/url_drilldown';
diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
index 17a6fc1b955dff..9eb0a06b6dbaf0 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
@@ -30,6 +30,9 @@ const createStartContract = (): Start => {
getActionFactories: jest.fn(),
getActionFactory: jest.fn(),
FlyoutManageDrilldowns: jest.fn(),
+ telemetry: jest.fn(),
+ extract: jest.fn(),
+ inject: jest.fn(),
};
return startContract;
diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
index b38bc44abe2b06..b05c08c4c77d02 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
@@ -39,6 +39,7 @@ import { UiActionsServiceEnhancements } from './services';
import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public';
import { createFlyoutManageDrilldowns } from './drilldowns';
import { createStartServicesGetter, Storage } from '../../../../src/plugins/kibana_utils/public';
+import { dynamicActionEnhancement } from './dynamic_actions/dynamic_action_enhancement';
interface SetupDependencies {
embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions.
@@ -58,7 +59,10 @@ export interface SetupContract
export interface StartContract
extends UiActionsStart,
- Pick {
+ Pick<
+ UiActionsServiceEnhancements,
+ 'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject'
+ > {
FlyoutManageDrilldowns: ReturnType;
}
@@ -87,7 +91,7 @@ export class AdvancedUiActionsPublicPlugin
public setup(
core: CoreSetup,
- { uiActions, licensing }: SetupDependencies
+ { embeddable, uiActions, licensing }: SetupDependencies
): SetupContract {
const startServices = createStartServicesGetter(core.getStartServices);
this.enhancements = new UiActionsServiceEnhancements({
@@ -95,6 +99,7 @@ export class AdvancedUiActionsPublicPlugin
featureUsageSetup: licensing.featureUsage,
getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage,
});
+ embeddable.registerEnhancement(dynamicActionEnhancement(this.enhancements));
return {
...uiActions,
...this.enhancements,
diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
index 3a0b65d2ed8446..6c71868222b242 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
@@ -96,6 +96,66 @@ describe('UiActionsService', () => {
).resolves.toBe(false);
});
+ test('action factory extract function gets called when calling uiactions extract', () => {
+ const service = new UiActionsServiceEnhancements(deps);
+ const actionState = {
+ events: [
+ {
+ eventId: 'test',
+ triggers: [],
+ action: { factoryId: factoryDefinition1.id, name: 'test', config: {} },
+ },
+ ],
+ };
+ const extract = jest.fn().mockImplementation((state) => ({ state, references: [] }));
+ service.registerActionFactory({
+ ...factoryDefinition1,
+ extract,
+ });
+ service.extract(actionState);
+ expect(extract).toBeCalledWith(actionState.events[0]);
+ });
+
+ test('action factory inject function gets called when calling uiactions inject', () => {
+ const service = new UiActionsServiceEnhancements(deps);
+ const actionState = {
+ events: [
+ {
+ eventId: 'test',
+ triggers: [],
+ action: { factoryId: factoryDefinition1.id, name: 'test', config: {} },
+ },
+ ],
+ };
+ const inject = jest.fn().mockImplementation((state) => state);
+ service.registerActionFactory({
+ ...factoryDefinition1,
+ inject,
+ });
+ service.inject(actionState, []);
+ expect(inject).toBeCalledWith(actionState.events[0], []);
+ });
+
+ test('action factory telemetry function gets called when calling uiactions telemetry', () => {
+ const service = new UiActionsServiceEnhancements(deps);
+ const actionState = {
+ events: [
+ {
+ eventId: 'test',
+ triggers: [],
+ action: { factoryId: factoryDefinition1.id, name: 'test', config: {} },
+ },
+ ],
+ };
+ const telemetry = jest.fn().mockImplementation((state) => ({}));
+ service.registerActionFactory({
+ ...factoryDefinition1,
+ telemetry,
+ });
+ service.telemetry(actionState);
+ expect(telemetry).toBeCalledWith(actionState.events[0], {});
+ });
+
describe('registerFeature for licensing', () => {
const spy = jest.spyOn(deps.featureUsageSetup, 'register');
beforeEach(() => {
diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
index ab0aa1200f5a78..5e40d803962de2 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
@@ -8,12 +8,20 @@ import { ActionFactoryRegistry } from '../types';
import {
ActionFactory,
ActionFactoryDefinition,
+ BaseActionConfig,
BaseActionFactoryContext,
+ SerializedEvent,
} from '../dynamic_actions';
import { DrilldownDefinition } from '../drilldowns';
import { ILicense } from '../../../licensing/common/types';
import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public';
import { LicensingPluginSetup, LicensingPluginStart } from '../../../licensing/public';
+import { SavedObjectReference } from '../../../../../src/core/types';
+import { PersistableStateDefinition } from '../../../../../src/plugins/kibana_utils/common';
+
+import { DynamicActionsState } from '../../common/types';
+
+export { DynamicActionsState };
export interface UiActionsServiceEnhancementsParams {
readonly actionFactories?: ActionFactoryRegistry;
@@ -22,7 +30,8 @@ export interface UiActionsServiceEnhancementsParams {
readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage'];
}
-export class UiActionsServiceEnhancements {
+export class UiActionsServiceEnhancements
+ implements PersistableStateDefinition {
protected readonly actionFactories: ActionFactoryRegistry;
protected readonly deps: Omit;
@@ -36,7 +45,7 @@ export class UiActionsServiceEnhancements {
* serialize/deserialize dynamic actions.
*/
public readonly registerActionFactory = <
- Config extends object = object,
+ Config extends BaseActionConfig = BaseActionConfig,
SupportedTriggers extends TriggerId = TriggerId,
FactoryContext extends BaseActionFactoryContext = {
triggers: SupportedTriggers[];
@@ -81,7 +90,7 @@ export class UiActionsServiceEnhancements {
* Convenience method to register a {@link DrilldownDefinition | drilldown}.
*/
public readonly registerDrilldown = <
- Config extends object = object,
+ Config extends BaseActionConfig = BaseActionConfig,
SupportedTriggers extends TriggerId = TriggerId,
FactoryContext extends BaseActionFactoryContext = {
triggers: SupportedTriggers[];
@@ -102,6 +111,9 @@ export class UiActionsServiceEnhancements {
licenseFeatureName,
supportedTriggers,
isCompatible,
+ telemetry,
+ extract,
+ inject,
}: DrilldownDefinition): void => {
const actionFactory: ActionFactoryDefinition<
Config,
@@ -119,6 +131,9 @@ export class UiActionsServiceEnhancements {
isConfigValid,
getDisplayName,
supportedTriggers,
+ telemetry,
+ extract,
+ inject,
getIconType: () => euiIcon,
isCompatible: async () => true,
create: (serializedAction) => ({
@@ -151,4 +166,43 @@ export class UiActionsServiceEnhancements {
);
});
};
+
+ public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => {
+ let telemetryData = telemetry;
+ state.events.forEach((event: SerializedEvent) => {
+ if (this.actionFactories.has(event.action.factoryId)) {
+ telemetryData = this.actionFactories
+ .get(event.action.factoryId)!
+ .telemetry(event, telemetryData);
+ }
+ });
+ return telemetryData;
+ };
+
+ public readonly extract = (state: DynamicActionsState) => {
+ const references: SavedObjectReference[] = [];
+ const newState = {
+ events: state.events.map((event: SerializedEvent) => {
+ const result = this.actionFactories.has(event.action.factoryId)
+ ? this.actionFactories.get(event.action.factoryId)!.extract(event)
+ : {
+ state: event,
+ references: [],
+ };
+ references.push(...result.references);
+ return result.state;
+ }),
+ };
+ return { state: newState, references };
+ };
+
+ public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => {
+ return {
+ events: state.events.map((event: SerializedEvent) => {
+ return this.actionFactories.has(event.action.factoryId)
+ ? this.actionFactories.get(event.action.factoryId)!.inject(event, references)
+ : event;
+ }),
+ };
+ };
}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts
index 3d143b0cacd063..9a529f192158d6 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts
@@ -17,7 +17,6 @@ import { TimeRange } from '../../../../../src/plugins/data/public';
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type InheritedChildrenInput = {
timeRange: TimeRange;
id?: string;
diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts
new file mode 100644
index 00000000000000..b3664362009149
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server';
+import { SavedObjectReference } from '../../../../src/core/types';
+import { DynamicActionsState, SerializedEvent } from './types';
+import { AdvancedUiActionsPublicPlugin } from './plugin';
+import { SerializableState } from '../../../../src/plugins/kibana_utils/common';
+
+export const dynamicActionEnhancement = (
+ uiActionsEnhanced: AdvancedUiActionsPublicPlugin
+): EnhancementRegistryDefinition => {
+ return {
+ id: 'dynamicActions',
+ telemetry: (state: SerializableState, telemetry: Record) => {
+ let telemetryData = telemetry;
+ (state as DynamicActionsState).events.forEach((event: SerializedEvent) => {
+ if (uiActionsEnhanced.getActionFactory(event.action.factoryId)) {
+ telemetryData = uiActionsEnhanced
+ .getActionFactory(event.action.factoryId)!
+ .telemetry(event, telemetryData);
+ }
+ });
+ return telemetryData;
+ },
+ extract: (state: SerializableState) => {
+ const references: SavedObjectReference[] = [];
+ const newState: DynamicActionsState = {
+ events: (state as DynamicActionsState).events.map((event: SerializedEvent) => {
+ const result = uiActionsEnhanced.getActionFactory(event.action.factoryId)
+ ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.extract(event)
+ : {
+ state: event,
+ references: [],
+ };
+ result.references.forEach((r) => references.push(r));
+ return result.state;
+ }),
+ };
+ return { state: newState, references };
+ },
+ inject: (state: SerializableState, references: SavedObjectReference[]) => {
+ return {
+ events: (state as DynamicActionsState).events.map((event: SerializedEvent) => {
+ return uiActionsEnhanced.getActionFactory(event.action.factoryId)
+ ? uiActionsEnhanced.getActionFactory(event.action.factoryId)!.inject(event, references)
+ : event;
+ }),
+ } as DynamicActionsState;
+ },
+ } as EnhancementRegistryDefinition;
+};
diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts
new file mode 100644
index 00000000000000..5419c4135796df
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AdvancedUiActionsPublicPlugin } from './plugin';
+
+export function plugin() {
+ return new AdvancedUiActionsPublicPlugin();
+}
+
+export { AdvancedUiActionsPublicPlugin as Plugin };
+export {
+ SetupContract as AdvancedUiActionsSetup,
+ StartContract as AdvancedUiActionsStart,
+} from './plugin';
+
+export {
+ ActionFactoryDefinition as UiActionsEnhancedActionFactoryDefinition,
+ ActionFactory as UiActionsEnhancedActionFactory,
+} from './types';
+
+export {
+ DynamicActionsState,
+ BaseActionConfig as UiActionsEnhancedBaseActionConfig,
+ SerializedAction as UiActionsEnhancedSerializedAction,
+ SerializedEvent as UiActionsEnhancedSerializedEvent,
+} from '../common/types';
diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts
new file mode 100644
index 00000000000000..0a61c917a2c5ca
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 { identity } from 'lodash';
+import { CoreSetup, Plugin, SavedObjectReference } from '../../../../src/core/server';
+import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server';
+import { dynamicActionEnhancement } from './dynamic_action_enhancement';
+import {
+ ActionFactoryRegistry,
+ SerializedEvent,
+ ActionFactoryDefinition,
+ DynamicActionsState,
+} from './types';
+
+export interface SetupContract {
+ registerActionFactory: any;
+}
+
+export type StartContract = void;
+
+interface SetupDependencies {
+ embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions.
+}
+
+export class AdvancedUiActionsPublicPlugin
+ implements Plugin {
+ protected readonly actionFactories: ActionFactoryRegistry = new Map();
+
+ constructor() {}
+
+ public setup(core: CoreSetup, { embeddable }: SetupDependencies) {
+ embeddable.registerEnhancement(dynamicActionEnhancement(this));
+
+ return {
+ registerActionFactory: this.registerActionFactory,
+ };
+ }
+
+ public start() {}
+
+ public stop() {}
+
+ /**
+ * Register an action factory. Action factories are used to configure and
+ * serialize/deserialize dynamic actions.
+ */
+ public readonly registerActionFactory = (definition: ActionFactoryDefinition) => {
+ if (this.actionFactories.has(definition.id)) {
+ throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`);
+ }
+
+ this.actionFactories.set(definition.id, {
+ id: definition.id,
+ telemetry: definition.telemetry || (() => ({})),
+ inject: definition.inject || identity,
+ extract:
+ definition.extract ||
+ ((state: SerializedEvent) => {
+ return { state, references: [] };
+ }),
+ });
+ };
+
+ public readonly getActionFactory = (actionFactoryId: string) => {
+ const actionFactory = this.actionFactories.get(actionFactoryId);
+ return actionFactory;
+ };
+
+ public readonly telemetry = (state: DynamicActionsState, telemetry: Record = {}) => {
+ state.events.forEach((event: SerializedEvent) => {
+ if (this.actionFactories.has(event.action.factoryId)) {
+ this.actionFactories.get(event.action.factoryId)!.telemetry(event, telemetry);
+ }
+ });
+ return telemetry;
+ };
+
+ public readonly extract = (state: DynamicActionsState) => {
+ const references: SavedObjectReference[] = [];
+ const newState = {
+ events: state.events.map((event: SerializedEvent) => {
+ const result = this.actionFactories.has(event.action.factoryId)
+ ? this.actionFactories.get(event.action.factoryId)!.extract(event)
+ : {
+ state: event,
+ references: [],
+ };
+ result.references.forEach((r) => references.push(r));
+ return result.state;
+ }),
+ };
+ return { state: newState, references };
+ };
+
+ public readonly inject = (state: DynamicActionsState, references: SavedObjectReference[]) => {
+ return {
+ events: state.events.map((event: SerializedEvent) => {
+ return this.actionFactories.has(event.action.factoryId)
+ ? this.actionFactories.get(event.action.factoryId)!.inject(event, references)
+ : event;
+ }),
+ };
+ };
+}
diff --git a/x-pack/plugins/ui_actions_enhanced/server/types.ts b/x-pack/plugins/ui_actions_enhanced/server/types.ts
new file mode 100644
index 00000000000000..4859be67283442
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/server/types.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 {
+ PersistableState,
+ PersistableStateDefinition,
+} from '../../../../src/plugins/kibana_utils/common';
+
+import { SerializedAction, SerializedEvent, DynamicActionsState } from '../common/types';
+
+export type ActionFactoryRegistry = Map;
+
+export interface ActionFactoryDefinition
+ extends PersistableStateDefinition
{
+ id: string;
+}
+
+export interface ActionFactory
+ extends PersistableState
{
+ id: string;
+}
+
+export { SerializedEvent, SerializedAction, DynamicActionsState };
diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts
index 5c3211eff3b4ed..cd2dc5018e110a 100644
--- a/x-pack/plugins/uptime/server/kibana.index.ts
+++ b/x-pack/plugins/uptime/server/kibana.index.ts
@@ -5,6 +5,7 @@
*/
import { Request, Server } from 'hapi';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
import { PLUGIN } from '../common/constants/plugin';
import { compose } from './lib/compose/kibana';
import { initUptimeServer } from './uptime_server';
@@ -31,6 +32,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
id: PLUGIN.ID,
name: PLUGIN.NAME,
order: 1000,
+ category: DEFAULT_APP_CATEGORIES.observability,
navLinkId: PLUGIN.ID,
icon: 'uptimeApp',
app: ['uptime', 'kibana'],
diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts
index e11de1376e4000..f4553e4c3a6fe7 100644
--- a/x-pack/test/accessibility/apps/spaces.ts
+++ b/x-pack/test/accessibility/apps/spaces.ts
@@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('a11y test for for customize space card', async () => {
await PageObjects.spaceSelector.clickEnterSpaceName();
await PageObjects.spaceSelector.addSpaceName('space_a');
- await PageObjects.spaceSelector.clickSpaceAcustomAvatar();
+ await PageObjects.spaceSelector.clickCustomizeSpaceAvatar('space_a');
await a11y.testAppSnapshot();
await browser.pressKeys(browser.keys.ESCAPE);
});
@@ -75,30 +75,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await a11y.testAppSnapshot();
});
- it('a11y test for click on "show" button to open customize feature display', async () => {
- await retry.waitFor(
- 'show button is visible',
- async () => await testSubjects.exists('show-hide-section-link')
- );
- await PageObjects.spaceSelector.clickShowFeatures();
- await a11y.testAppSnapshot();
- });
-
- it('a11y test for change all option for feature visibility popover', async () => {
- await PageObjects.spaceSelector.clickFeaturesVisibilityButton();
+ it('a11y test for toggling an entire feature category', async () => {
+ await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana');
await a11y.testAppSnapshot();
- });
- it('a11y test for hide all feature visibility popover option', async () => {
- await PageObjects.spaceSelector.clickHideAllFeatures();
+ await PageObjects.spaceSelector.openFeatureCategory('kibana');
await a11y.testAppSnapshot();
- });
- it('a11y test for toggle individual feature - using enterprise feature visibility', async () => {
- await PageObjects.spaceSelector.clickFeaturesVisibilityButton();
- await PageObjects.spaceSelector.clickShowAllFeatures();
- await PageObjects.spaceSelector.toggleFeatureVisibility('enterpriseSearch');
- await a11y.testAppSnapshot();
+ await PageObjects.spaceSelector.toggleFeatureCategoryVisibility('kibana');
});
it('a11y test for space listing page', async () => {
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index 68ff3dad9ae866..43e4f642bb943a 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -76,6 +76,7 @@ export class FixturePlugin implements Plugin {
+ // FLAKY: https://github.com/elastic/kibana/issues/77870
+ describe.skip('when data is loaded', () => {
before(() => esArchiver.load('metrics_8.0.0'));
after(() => esArchiver.unload('metrics_8.0.0'));
diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts
index 18beb76e5a3a0b..6364a79a12f045 100644
--- a/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts
+++ b/x-pack/test/apm_api_integration/trial/tests/csm/web_core_vitals.ts
@@ -71,7 +71,7 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext)
0,
0,
],
- "tbt": "0.00",
+ "tbt": 0,
}
`);
});
diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
index bd35374643e9b8..b74df717010263 100644
--- a/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
+++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
@@ -40,18 +40,21 @@ export default function ({ getPageObjects, getService }) {
operation: 'date_histogram',
field: '@timestamp',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'avg',
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'ip',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.save(title, saveAsNew, redirectToOrigin);
}
diff --git a/x-pack/test/functional/apps/home/feature_controls/home_security.ts b/x-pack/test/functional/apps/home/feature_controls/home_security.ts
new file mode 100644
index 00000000000000..6074bba372cb23
--- /dev/null
+++ b/x-pack/test/functional/apps/home/feature_controls/home_security.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 expect from '@kbn/expect';
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ getPageObjects, getService }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const security = getService('security');
+ const PageObjects = getPageObjects(['security', 'home']);
+ const testSubjects = getService('testSubjects');
+
+ describe('security', () => {
+ before(async () => {
+ await esArchiver.load('dashboard/feature_controls/security');
+ await esArchiver.loadIfNeeded('logstash_functional');
+
+ // ensure we're logged out so we can login as the appropriate users
+ await PageObjects.security.forceLogout();
+ });
+
+ after(async () => {
+ await esArchiver.unload('dashboard/feature_controls/security');
+
+ // logout, so the other tests don't accidentally run as the custom users we're testing below
+ await PageObjects.security.forceLogout();
+ });
+
+ describe('global all privileges', () => {
+ before(async () => {
+ await security.role.create('global_all_role', {
+ elasticsearch: {},
+ kibana: [
+ {
+ base: ['all'],
+ spaces: ['*'],
+ },
+ ],
+ });
+
+ await security.user.create('global_all_user', {
+ password: 'global_all_user-password',
+ roles: ['global_all_role'],
+ full_name: 'test user',
+ });
+
+ await PageObjects.security.login('global_all_user', 'global_all_user-password', {
+ expectSpaceSelector: false,
+ });
+ });
+
+ after(async () => {
+ await security.role.delete('global_all_role');
+ await security.user.delete('global_all_user');
+ });
+
+ it('shows all available solutions', async () => {
+ const solutions = await PageObjects.home.getVisibileSolutions();
+ expect(solutions).to.eql([
+ 'enterpriseSearch',
+ 'observability',
+ 'securitySolution',
+ 'kibana',
+ ]);
+ });
+
+ it('shows the management section', async () => {
+ await testSubjects.existOrFail('homDataManage', { timeout: 2000 });
+ });
+
+ it('shows the "Manage" action item', async () => {
+ await testSubjects.existOrFail('homManagementActionItem', {
+ timeout: 2000,
+ });
+ });
+ });
+
+ describe('global dashboard all privileges', () => {
+ before(async () => {
+ await security.role.create('global_dashboard_all_role', {
+ elasticsearch: {},
+ kibana: [
+ {
+ feature: {
+ dashboard: ['all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ });
+
+ await security.user.create('global_dashboard_all_user', {
+ password: 'global_dashboard_all_user-password',
+ roles: ['global_dashboard_all_role'],
+ full_name: 'test user',
+ });
+
+ await PageObjects.security.login(
+ 'global_dashboard_all_user',
+ 'global_dashboard_all_user-password',
+ {
+ expectSpaceSelector: false,
+ }
+ );
+ });
+
+ after(async () => {
+ await security.role.delete('global_dashboard_all_role');
+ await security.user.delete('global_dashboard_all_user');
+ });
+
+ it('shows only the kibana solution', async () => {
+ const solutions = await PageObjects.home.getVisibileSolutions();
+ expect(solutions).to.eql(['kibana']);
+ });
+
+ it('does not show the management section', async () => {
+ await testSubjects.missingOrFail('homDataManage', { timeout: 2000 });
+ });
+
+ it('does not show the "Manage" action item', async () => {
+ await testSubjects.missingOrFail('homManagementActionItem', {
+ timeout: 2000,
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/functional/apps/home/feature_controls/index.ts b/x-pack/test/functional/apps/home/feature_controls/index.ts
new file mode 100644
index 00000000000000..70185d511f2c19
--- /dev/null
+++ b/x-pack/test/functional/apps/home/feature_controls/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default ({ loadTestFile }: FtrProviderContext) => {
+ describe('feature controls', function () {
+ this.tags('skipFirefox');
+ loadTestFile(require.resolve('./home_security'));
+ });
+};
diff --git a/x-pack/test/functional/apps/home/index.ts b/x-pack/test/functional/apps/home/index.ts
new file mode 100644
index 00000000000000..1e14411ffc2b0f
--- /dev/null
+++ b/x-pack/test/functional/apps/home/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default ({ loadTestFile }: FtrProviderContext) => {
+ describe('Home page', function () {
+ this.tags('ciGroup7');
+ loadTestFile(require.resolve('./feature_controls'));
+ });
+};
diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts
index 4a68c9a8ff3f2a..fa13d013ea1157 100644
--- a/x-pack/test/functional/apps/lens/dashboard.ts
+++ b/x-pack/test/functional/apps/lens/dashboard.ts
@@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await find.clickByButtonText('lnsXYvis');
await dashboardAddPanel.closeAddPanel();
await PageObjects.lens.goToTimeRange();
- await clickInChart(5, 5); // hardcoded position of bar
+ await clickInChart(5, 5); // hardcoded position of bar, depends heavy on data and charts implementation
await retry.try(async () => {
await testSubjects.click('applyFiltersPopoverButton');
@@ -68,5 +68,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const hasIpFilter = await filterBar.hasFilter('ip', '97.220.3.248');
expect(hasIpFilter).to.be(true);
});
+ it('should be able to add filters by clicking in pie chart', async () => {
+ await PageObjects.common.navigateToApp('dashboard');
+ await PageObjects.dashboard.clickNewDashboard();
+ await dashboardAddPanel.clickOpenAddPanel();
+ await dashboardAddPanel.filterEmbeddableNames('lnsPieVis');
+ await find.clickByButtonText('lnsPieVis');
+ await dashboardAddPanel.closeAddPanel();
+
+ await PageObjects.lens.goToTimeRange();
+ await clickInChart(5, 5); // hardcoded position of the slice, depends heavy on data and charts implementation
+
+ await PageObjects.lens.assertExactText(
+ '[data-test-subj="embeddablePanelHeading-lnsPieVis"]',
+ 'lnsPieVis'
+ );
+ const hasGeoDestFilter = await filterBar.hasFilter('geo.dest', 'LS');
+ expect(hasGeoDestFilter).to.be(true);
+ });
});
}
diff --git a/x-pack/test/functional/apps/lens/lens_reporting.ts b/x-pack/test/functional/apps/lens/lens_reporting.ts
index 4974b63be6f72c..751fbbce13addd 100644
--- a/x-pack/test/functional/apps/lens/lens_reporting.ts
+++ b/x-pack/test/functional/apps/lens/lens_reporting.ts
@@ -12,10 +12,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
const listingTable = getService('listingTable');
+ const security = getService('security');
describe('lens reporting', () => {
before(async () => {
await esArchiver.loadIfNeeded('lens/reporting');
+ await security.testUser.setRoles(
+ ['test_logstash_reader', 'global_dashboard_read', 'reporting_user'],
+ false
+ );
});
after(async () => {
@@ -25,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
refresh: true,
body: { query: { match_all: {} } },
});
+ await security.testUser.restoreDefaults();
});
it('should not cause PDF reports to fail', async () => {
@@ -33,7 +39,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.reporting.openPdfReportingPanel();
await PageObjects.reporting.clickGenerateReportButton();
const url = await PageObjects.reporting.getReportURL(60000);
-
expect(url).to.be.ok();
});
});
diff --git a/x-pack/test/functional/apps/lens/rollup.ts b/x-pack/test/functional/apps/lens/rollup.ts
index f6882c8aed2144..8e1dc231b6b1a9 100644
--- a/x-pack/test/functional/apps/lens/rollup.ts
+++ b/x-pack/test/functional/apps/lens/rollup.ts
@@ -34,18 +34,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
operation: 'date_histogram',
field: '@timestamp',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'sum',
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'geo.src',
});
+ await PageObjects.lens.closeDimensionEditor();
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2);
await PageObjects.lens.save('Afancilenstest');
diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts
index 8c4321d77acf46..42807a23cb13ab 100644
--- a/x-pack/test/functional/apps/lens/smokescreen.ts
+++ b/x-pack/test/functional/apps/lens/smokescreen.ts
@@ -8,20 +8,12 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const PageObjects = getPageObjects(['visualize', 'lens']);
+ const PageObjects = getPageObjects(['visualize', 'lens', 'common']);
const find = getService('find');
const listingTable = getService('listingTable');
const testSubjects = getService('testSubjects');
describe('lens smokescreen tests', () => {
- it('should allow editing saved visualizations', async () => {
- await PageObjects.visualize.gotoVisualizationLandingPage();
- await listingTable.searchForItemWithName('Artistpreviouslyknownaslens');
- await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
- await PageObjects.lens.goToTimeRange();
- await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
- });
-
it('should allow creation of lens xy chart', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
@@ -32,18 +24,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
operation: 'date_histogram',
field: '@timestamp',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'avg',
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: '@message.raw',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.switchToVisualization('lnsDatatable');
await PageObjects.lens.removeDimension('lnsDatatable_column');
@@ -54,6 +49,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
operation: 'terms',
field: 'ip',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.save('Afancilenstest');
@@ -70,8 +66,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// legend item(s), so we're using a class selector here.
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3);
});
+ it('should create an xy visualization with filters aggregation', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsXYvis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_splitDimensionPanel > lns-dimensionTrigger',
+ operation: 'filters',
+ isPreviousIncompatible: true,
+ });
+ await PageObjects.lens.addFilterToAgg(`geo.src : CN`);
+
+ expect(await PageObjects.lens.getFiltersAggLabels()).to.eql([`ip : *`, `geo.src : CN`]);
+ expect(await find.allByCssSelector('.echLegendItem')).to.have.length(2);
+ });
- it('should allow seamless transition to and from table view', async () => {
+ it('should transition from metric to table to metric', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('Artistpreviouslyknownaslens');
await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens');
@@ -84,7 +95,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.lens.assertMetric('Maximum of bytes', '19,986');
});
- it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => {
+ it('should transition from a multi-layer stacked bar to a multi-layer line chart and correctly remove all layers', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
@@ -95,22 +106,75 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: '@timestamp',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'avg',
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.createLayer();
expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false);
await PageObjects.lens.switchToVisualization('line');
+ await PageObjects.lens.configureDimension(
+ {
+ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
+ operation: 'terms',
+ field: 'geo.src',
+ },
+ 1
+ );
+
+ await PageObjects.lens.closeDimensionEditor();
+ await PageObjects.lens.configureDimension(
+ {
+ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
+ operation: 'avg',
+ field: 'bytes',
+ },
+ 1
+ );
+ await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.getLayerCount()).to.eql(2);
+ await testSubjects.click('lnsLayerRemove');
+ await testSubjects.click('lnsLayerRemove');
+ await testSubjects.existOrFail('empty-workspace');
+ });
+
+ it('should edit settings of xy line chart', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsXYvis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
+ await PageObjects.lens.goToTimeRange();
+ await testSubjects.click('lnsXY_splitDimensionPanel > indexPattern-dimension-remove');
+ await PageObjects.lens.switchToVisualization('line');
+ await PageObjects.lens.configureDimension({
+ dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger',
+ operation: 'max',
+ field: 'memory',
+ });
+ await PageObjects.lens.editDimensionLabel('Test of label');
+ await PageObjects.lens.editDimensionFormat('Percent');
+ await PageObjects.lens.editDimensionColor('#ff0000');
+ await PageObjects.lens.editMissingValues('Linear');
+
+ await PageObjects.lens.assertMissingValues('Linear');
+ await PageObjects.lens.assertColor('#ff0000');
+
+ await testSubjects.existOrFail('indexPattern-dimension-formatDecimals');
+
+ await PageObjects.lens.closeDimensionEditor();
+
+ expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
+ 'Test of label'
+ );
});
- it('should switch from a multi-layer stacked bar to donut chart using suggestions', async () => {
+ it('should transition from a multi-layer stacked bar to donut chart using suggestions', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
@@ -121,12 +185,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'geo.dest',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'avg',
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.createLayer();
await PageObjects.lens.configureDimension(
@@ -138,6 +204,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
1
);
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension(
{
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
@@ -146,6 +213,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
},
1
);
+
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.save('twolayerchart');
await testSubjects.click('lnsSuggestion-asDonut > lnsSuggestion');
@@ -158,7 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
- it('should allow transition from line chart to donut chart and to bar chart', async () => {
+ it('should transition from line chart to donut chart and to bar chart', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('lnsXYvis');
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
@@ -185,7 +254,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
- it('should allow seamless transition from bar chart to line chart using layer chart switch', async () => {
+ it('should transition from bar chart to line chart using layer chart switch', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('lnsXYvis');
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
@@ -203,7 +272,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
- it('should allow seamless transition from pie chart to treemap chart', async () => {
+ it('should transition from pie chart to treemap chart', async () => {
await PageObjects.visualize.gotoVisualizationLandingPage();
await listingTable.searchForItemWithName('lnsPieVis');
await PageObjects.lens.clickVisualizeListItemTitle('lnsPieVis');
@@ -221,7 +290,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
});
- it('should allow creating a pie chart and switching to datatable', async () => {
+ it('should create a pie chart and switch to datatable', async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
@@ -231,6 +300,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
operation: 'date_histogram',
field: '@timestamp',
});
+ await PageObjects.lens.closeDimensionEditor();
await PageObjects.lens.configureDimension({
dimension: 'lnsPie_sizeByDimensionPanel > lns-empty-dimension',
@@ -238,6 +308,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
field: 'bytes',
});
+ await PageObjects.lens.closeDimensionEditor();
expect(await PageObjects.lens.hasChartSwitchWarning('lnsDatatable')).to.eql(false);
await PageObjects.lens.switchToVisualization('lnsDatatable');
diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz
index 58ac5616651d41..7736287bc9a37e 100644
Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce/data.json.gz differ
diff --git a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz
index 06e83f8c267d66..0cec8a44dea8d9 100644
Binary files a/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz and b/x-pack/test/functional/es_archives/reporting/ecommerce_kibana_spaces/data.json.gz differ
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index e3c21085b92d31..a1e62afbe14c80 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -13,7 +13,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
const retry = getService('retry');
const find = getService('find');
const comboBox = getService('comboBox');
- const PageObjects = getPageObjects(['header', 'header', 'timePicker']);
+ const PageObjects = getPageObjects(['header', 'header', 'timePicker', 'common']);
return logWrapper('lensPage', log, {
/**
@@ -85,19 +85,32 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
* @param layerIndex - the index of the layer
*/
async configureDimension(
- opts: { dimension: string; operation: string; field: string },
+ opts: {
+ dimension: string;
+ operation: string;
+ field?: string;
+ isPreviousIncompatible?: boolean;
+ },
layerIndex = 0
) {
await retry.try(async () => {
await testSubjects.click(`lns-layerPanel-${layerIndex} > ${opts.dimension}`);
await testSubjects.exists(`lns-indexPatternDimension-${opts.operation}`);
});
+ const operationSelector = opts.isPreviousIncompatible
+ ? `lns-indexPatternDimension-${opts.operation} incompatible`
+ : `lns-indexPatternDimension-${opts.operation}`;
+ await testSubjects.click(operationSelector);
+
+ if (opts.field) {
+ const target = await testSubjects.find('indexPattern-dimension-field');
+ await comboBox.openOptionsList(target);
+ await comboBox.setElement(target, opts.field);
+ }
+ },
- await testSubjects.click(`lns-indexPatternDimension-${opts.operation}`);
-
- const target = await testSubjects.find('indexPattern-dimension-field');
- await comboBox.openOptionsList(target);
- await comboBox.setElement(target, opts.field);
+ // closes the dimension editor flyout
+ async closeDimensionEditor() {
await testSubjects.click('lns-indexPattern-dimensionContainerTitle');
},
@@ -107,7 +120,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
async removeDimension(dimensionTestSubj: string) {
await testSubjects.click(`${dimensionTestSubj} > indexPattern-dimension-remove`);
},
-
+ /**
+ * adds new filter to filters agg
+ */
+ async addFilterToAgg(queryString: string) {
+ await testSubjects.click('lns-newBucket-add');
+ const queryInput = await testSubjects.find('indexPattern-filters-queryStringInput');
+ await queryInput.type(queryString);
+ await PageObjects.common.pressEnterKey();
+ await PageObjects.common.pressEnterKey();
+ await PageObjects.common.sleep(1000); // give time for debounced components to rerender
+ },
/**
* Save the current Lens visualization.
*/
@@ -141,10 +164,43 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await testSubjects.click('lnsApp_saveAndReturnButton');
},
+ async editDimensionLabel(label: string) {
+ await testSubjects.setValue('indexPattern-label-edit', label);
+ },
+ async editDimensionFormat(format: string) {
+ const formatInput = await testSubjects.find('indexPattern-dimension-format');
+ await comboBox.openOptionsList(formatInput);
+ await comboBox.setElement(formatInput, format);
+ },
+ async editDimensionColor(color: string) {
+ const colorPickerInput = await testSubjects.find('colorPickerAnchor');
+ await colorPickerInput.type(color);
+ await PageObjects.common.sleep(1000); // give time for debounced components to rerender
+ },
+ async editMissingValues(option: string) {
+ await retry.try(async () => {
+ await testSubjects.click('lnsMissingValuesButton');
+ await testSubjects.exists('lnsMissingValuesSelect');
+ });
+ await testSubjects.click('lnsMissingValuesSelect');
+ const optionSelector = await find.byCssSelector(`#${option}`);
+ await optionSelector.click();
+ },
+
getTitle() {
return testSubjects.getVisibleText('lns_ChartTitle');
},
+ async getFiltersAggLabels() {
+ const labels = [];
+ const filters = await testSubjects.findAll('indexPattern-filters-existingFilterContainer');
+ for (let i = 0; i < filters.length; i++) {
+ labels.push(await filters[i].getVisibleText());
+ }
+ log.debug(`Found ${labels.length} filters on current page`);
+ return labels;
+ },
+
/**
* Uses the Lens visualization switcher to switch visualizations.
*
@@ -275,5 +331,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
await this.assertExactText('[data-test-subj="lns_metric_title"]', title);
await this.assertExactText('[data-test-subj="lns_metric_value"]', count);
},
+
+ async assertMissingValues(option: string) {
+ await this.assertExactText('[data-test-subj="lnsMissingValuesSelect"]', option);
+ },
+ async assertColor(color: string) {
+ // TODO: target dimensionTrigger color element after merging https://github.com/elastic/kibana/pull/76871
+ await testSubjects.getAttribute('colorPickerAnchor', color);
+ },
});
}
diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts
index acf8a65362f01b..426f8e520815e0 100644
--- a/x-pack/test/functional/page_objects/space_selector_page.ts
+++ b/x-pack/test/functional/page_objects/space_selector_page.ts
@@ -66,8 +66,8 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.setValue('addSpaceName', spaceName);
}
- async clickSpaceAcustomAvatar() {
- await testSubjects.click('space-avatar-space_a');
+ async clickCustomizeSpaceAvatar(spaceId: string) {
+ await testSubjects.click(`space-avatar-${spaceId}`);
}
async clickSpaceInitials() {
@@ -122,10 +122,6 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.setValue('spaceURLDisplay', spaceURL);
}
- async clickFeaturesVisibilityButton() {
- await testSubjects.click('changeAllFeatureVisibilityPopover');
- }
-
async clickHideAllFeatures() {
await testSubjects.click('spc-toggle-all-features-hide');
}
@@ -134,8 +130,28 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro
await testSubjects.click('spc-toggle-all-features-show');
}
- async toggleFeatureVisibility(featureName: string) {
- await testSubjects.click(`feature-${featureName}-toggle`);
+ async openFeatureCategory(categoryName: string) {
+ const category = await find.byCssSelector(
+ `button[aria-controls=featureCategory_${categoryName}]`
+ );
+ const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true';
+ if (!isCategoryExpanded) {
+ await category.click();
+ }
+ }
+
+ async closeFeatureCategory(categoryName: string) {
+ const category = await find.byCssSelector(
+ `button[aria-controls=featureCategory_${categoryName}]`
+ );
+ const isCategoryExpanded = (await category.getAttribute('aria-expanded')) === 'true';
+ if (isCategoryExpanded) {
+ await category.click();
+ }
+ }
+
+ async toggleFeatureCategoryVisibility(categoryName: string) {
+ await testSubjects.click(`featureCategoryButton_${categoryName}`);
}
async clickOnDescriptionOfSpace() {
diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
index 5c42c1978a0b5c..fd7869eac918ff 100644
--- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
+++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts
@@ -25,6 +25,7 @@ export class AlertingFixturePlugin implements Plugin {
- describe('Saved Search Features', () => {
- after(async () => {
- await reportingAPI.deleteAllReports();
- });
+ after(async () => {
+ await reportingAPI.deleteAllReports();
+ });
+ describe('Saved Search Features', () => {
it('With filters and timebased data, explicit UTC format', async () => {
// load test data that contains a saved search and documents
await esArchiver.load('reporting/logs');
@@ -350,8 +350,8 @@ export default function ({ getService }: FtrProviderContext) {
searchId: 'search:6091ead0-1c6d-11ea-a100-8589bb9d7c6b',
postPayload: {
timerange: {
- min: '2019-06-26T06:20:28Z',
- max: '2019-06-26T07:27:58Z',
+ min: '2019-05-28T00:00:00Z',
+ max: '2019-06-26T00:00:00Z',
timezone: 'UTC',
},
state: {
@@ -370,8 +370,8 @@ export default function ({ getService }: FtrProviderContext) {
{
range: {
order_date: {
- gte: '2019-06-26T06:20:28.066Z',
- lte: '2019-06-26T07:27:58.573Z',
+ gte: '2019-05-28T00:00:00.000Z',
+ lte: '2019-06-26T00:00:00.000Z',
format: 'strict_date_optional_time',
},
},
diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts
index 9eafd0c318383b..3f2b2e7116206e 100644
--- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts
+++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts
@@ -12,51 +12,106 @@ import { FtrProviderContext } from '../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
const reportingAPI = getService('reportingAPI');
const supertest = getService('supertest');
const log = getService('log');
+ const setSpaceConfig = async (spaceId: string, settings: object) => {
+ return await kibanaServer.request({
+ path: `/s/${spaceId}/api/kibana/settings`,
+ method: 'POST',
+ body: { changes: settings },
+ });
+ };
+
const getCompleted$ = (downloadPath: string) => {
return Rx.interval(2000).pipe(
tap(() => log.debug(`checking report status at ${downloadPath}...`)),
switchMap(() => supertest.get(downloadPath)),
filter(({ status: statusCode }) => statusCode === 200),
+ tap(() => log.debug(`report at ${downloadPath} is done`)),
map((response) => response.text),
first(),
timeout(15000)
);
};
- describe('Exports from Non-default Space', () => {
+ describe('Exports and Spaces', () => {
before(async () => {
await esArchiver.load('reporting/ecommerce');
- await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space
+ await esArchiver.load('reporting/ecommerce_kibana_spaces'); // multiple spaces with different config settings
});
after(async () => {
await esArchiver.unload('reporting/ecommerce');
await esArchiver.unload('reporting/ecommerce_kibana_spaces');
- });
-
- afterEach(async () => {
await reportingAPI.deleteAllReports();
});
- it('should complete a job of CSV saved search export in non-default space', async () => {
- const downloadPath = await reportingAPI.postJob(
- `/s/non_default_space/api/reporting/generate/csv?jobParams=%28browserTimezone%3AUTC%2CconflictedTypesFields%3A%21%28%29%2Cfields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2CindexPatternId%3A%27067dec90-e7ee-11ea-a730-d58e9ea7581b%27%2CmetaFields%3A%21%28_source%2C_id%2C_type%2C_index%2C_score%29%2CobjectType%3Asearch%2CsearchRequest%3A%28body%3A%28_source%3A%28includes%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%29%2Cdocvalue_fields%3A%21%28%28field%3Aorder_date%2Cformat%3Adate_time%29%29%2Cquery%3A%28bool%3A%28filter%3A%21%28%28match_all%3A%28%29%29%2C%28range%3A%28order_date%3A%28format%3Astrict_date_optional_time%2Cgte%3A%272019-06-11T08%3A24%3A16.425Z%27%2Clte%3A%272019-07-13T09%3A31%3A07.520Z%27%29%29%29%29%2Cmust%3A%21%28%29%2Cmust_not%3A%21%28%29%2Cshould%3A%21%28%29%29%29%2Cscript_fields%3A%28%29%2Csort%3A%21%28%28order_date%3A%28order%3Adesc%2Cunmapped_type%3Aboolean%29%29%29%2Cstored_fields%3A%21%28order_date%2Ccategory%2Ccustomer_first_name%2Ccustomer_full_name%2Ctotal_quantity%2Ctotal_unique_products%2Ctaxless_total_price%2Ctaxful_total_price%2Ccurrency%29%2Cversion%3A%21t%29%2Cindex%3A%27ecommerce%2A%27%29%2Ctitle%3A%27Ecom%20Search%27%29`
- );
+ describe('CSV saved search export', () => {
+ it('should use formats from the default space', async () => {
+ kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'UTC' });
+ const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, {
+ jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`,
+ });
+ const csv = await getCompleted$(path).toPromise();
+ expect(csv).to.match(
+ /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/
+ );
+ });
- // Retry the download URL until a "completed" response status is returned
- const completed$ = getCompleted$(downloadPath);
- const reportCompleted = await completed$.toPromise();
- expect(reportCompleted).to.match(/^"order_date",/);
+ it('should use formats from non-default spaces', async () => {
+ setSpaceConfig('non_default_space', {
+ 'csv:separator': ';',
+ 'csv:quoteValues': false,
+ 'dateFormat:tz': 'US/Alaska',
+ });
+ const path = await reportingAPI.postJobJSON(
+ `/s/non_default_space/api/reporting/generate/csv`,
+ {
+ jobParams: `(conflictedTypesFields:!(),fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),indexPatternId:'067dec90-e7ee-11ea-a730-d58e9ea7581b',metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T08:24:16.425Z',lte:'2019-07-13T09:31:07.520Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_first_name,customer_full_name,total_quantity,total_unique_products,taxless_total_price,taxful_total_price,currency),version:!t),index:'ecommerce*'),title:'Ecom Search')`,
+ }
+ );
+ const csv = await getCompleted$(path).toPromise();
+ expect(csv).to.match(
+ /^order_date;category;customer_first_name;customer_full_name;total_quantity;total_unique_products;taxless_total_price;taxful_total_price;currency\nJul 11, 2019 @ 16:00:00.000;/
+ );
+ });
+
+ it(`should use browserTimezone in jobParams for date formatting`, async () => {
+ const tzParam = 'America/Phoenix';
+ const tzSettings = 'Browser';
+ setSpaceConfig('non_default_space', { 'csv:separator': ';', 'dateFormat:tz': tzSettings });
+ const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, {
+ jobParams: `(browserTimezone:${tzParam},conflictedTypesFields:!(),fields:!(order_date,category,customer_full_name,taxful_total_price,currency),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,category,customer_full_name,taxful_total_price,currency)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-05-30T05:09:59.743Z',lte:'2019-07-26T08:47:09.682Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,category,customer_full_name,taxful_total_price,currency),version:!t),index:'ec*'),title:'EC SEARCH from DEFAULT')`,
+ });
+
+ const csv = await getCompleted$(path).toPromise();
+ expect(csv).to.match(
+ /^"order_date",category,"customer_full_name","taxful_total_price",currency\n"Jul 11, 2019 @ 17:00:00.000"/
+ );
+ });
+
+ it(`should default to UTC for date formatting when timezone is not known`, async () => {
+ kibanaServer.uiSettings.update({ 'csv:separator': ',', 'dateFormat:tz': 'Browser' });
+ const path = await reportingAPI.postJobJSON(`/api/reporting/generate/csv`, {
+ jobParams: `(conflictedTypesFields:!(),fields:!(order_date,order_date,customer_full_name,taxful_total_price),indexPatternId:aac3e500-f2c7-11ea-8250-fb138aa491e7,metaFields:!(_source,_id,_type,_index,_score),objectType:search,searchRequest:(body:(_source:(includes:!(order_date,customer_full_name,taxful_total_price)),docvalue_fields:!((field:order_date,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(order_date:(format:strict_date_optional_time,gte:'2019-06-11T04:49:43.495Z',lte:'2019-07-14T10:25:34.149Z')))),must:!(),must_not:!(),should:!())),script_fields:(),sort:!((order_date:(order:desc,unmapped_type:boolean))),stored_fields:!(order_date,customer_full_name,taxful_total_price),version:!t),index:'ec*'),title:'EC SEARCH')`,
+ });
+ const csv = await getCompleted$(path).toPromise();
+ expect(csv).to.match(
+ /^"order_date","order_date","customer_full_name","taxful_total_price"\n"Jul 12, 2019 @ 00:00:00.000","Jul 12, 2019 @ 00:00:00.000","Sultan Al Boone","173.96"/
+ );
+ });
});
// FLAKY: https://github.com/elastic/kibana/issues/76551
it.skip('should complete a job of PNG export of a dashboard in non-default space', async () => {
- const downloadPath = await reportingAPI.postJob(
- `/s/non_default_space/api/reporting/generate/png?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apng%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29`
+ const downloadPath = await reportingAPI.postJobJSON(
+ `/s/non_default_space/api/reporting/generate/png`,
+ {
+ jobParams: `(browserTimezone:UTC,layout:(dimensions:(height:512,width:2402),id:png),objectType:dashboard,relativeUrl:'/s/non_default_space/app/dashboards#/view/3c9ee360-e7ee-11ea-a730-d58e9ea7581b?_g=(filters:!!(),refreshInterval:(pause:!!t,value:0),time:(from:!'2019-06-10T03:17:28.800Z!',to:!'2019-07-14T19:25:06.385Z!'))&_a=(description:!'!',filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),query:(language:kuery,query:!'!'),timeRestore:!!t,title:!'Ecom%20Dashboard%20Non%20Default%20Space!',viewMode:view)',title:'Ecom Dashboard Non Default Space')`,
+ }
);
const completed$: Rx.Observable = getCompleted$(downloadPath);
@@ -66,8 +121,11 @@ export default function ({ getService }: FtrProviderContext) {
// FLAKY: https://github.com/elastic/kibana/issues/76551
it.skip('should complete a job of PDF export of a dashboard in non-default space', async () => {
- const downloadPath = await reportingAPI.postJob(
- `/s/non_default_space/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apreserve_layout%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29`
+ const downloadPath = await reportingAPI.postJobJSON(
+ `/s/non_default_space/api/reporting/generate/printablePdf`,
+ {
+ jobParams: `(browserTimezone:UTC,layout:(dimensions:(height:512,width:2402),id:preserve_layout),objectType:dashboard,relativeUrls:!('/s/non_default_space/app/dashboards#/view/3c9ee360-e7ee-11ea-a730-d58e9ea7581b?_g=(filters:!!(),refreshInterval:(pause:!!t,value:0),time:(from:!'2019-06-10T03:17:28.800Z!',to:!'2019-07-14T19:25:06.385Z!'))&_a=(description:!'!',filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),query:(language:kuery,query:!'!'),timeRestore:!!t,title:!'Ecom%20Dashboard%20Non%20Default%20Space!',viewMode:view)'),title:'Ecom Dashboard Non Default Space')`,
+ }
);
const completed$ = getCompleted$(downloadPath);
diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts
index aaf4dd39264114..99a46684d8a67a 100644
--- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts
+++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts
@@ -115,8 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
});
});
- // FAILING: https://github.com/elastic/kibana/issues/76581
- describe.skip('from new jobs posted', () => {
+ describe('from new jobs posted', () => {
it('should handle csv', async () => {
await reportingAPI.expectAllJobsToFinishSuccessfully(
await Promise.all([
@@ -133,7 +132,8 @@ export default function ({ getService }: FtrProviderContext) {
reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0);
});
- it('should handle preserve_layout pdf', async () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/76581
+ it.skip('should handle preserve_layout pdf', async () => {
await reportingAPI.expectAllJobsToFinishSuccessfully(
await Promise.all([
reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_DASHBOARD_FILTER_6_3),
@@ -150,7 +150,8 @@ export default function ({ getService }: FtrProviderContext) {
reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2);
});
- it('should handle print_layout pdf', async () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/76581
+ it.skip('should handle print_layout pdf', async () => {
await reportingAPI.expectAllJobsToFinishSuccessfully(
await Promise.all([
reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_3),
diff --git a/x-pack/test/reporting_api_integration/services.ts b/x-pack/test/reporting_api_integration/services.ts
index e61e6483855af8..2c0252fde76938 100644
--- a/x-pack/test/reporting_api_integration/services.ts
+++ b/x-pack/test/reporting_api_integration/services.ts
@@ -84,7 +84,7 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) {
);
},
- async postJob(apiPath: string) {
+ async postJob(apiPath: string): Promise {
log.debug(`ReportingAPI.postJob(${apiPath})`);
const { body } = await supertest
.post(removeWhitespace(apiPath))
@@ -93,6 +93,12 @@ export function ReportingAPIProvider({ getService }: FtrProviderContext) {
return body.path;
},
+ async postJobJSON(apiPath: string, jobJSON: object = {}): Promise {
+ log.debug(`ReportingAPI.postJobJSON((${apiPath}): ${JSON.stringify(jobJSON)})`);
+ const { body } = await supertest.post(apiPath).set('kbn-xsrf', 'xxx').send(jobJSON);
+ return body.path;
+ },
+
/**
*
* @return {Promise} A function to call to clean up the index alias that was added.
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
index e8fa18aa01b4cd..620eab37f9b46e 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts
@@ -13,8 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const queryBar = getService('queryBar');
- // FLAKY: https://github.com/elastic/kibana/issues/77835
- describe.skip('Endpoint Event Resolver', function () {
+ describe('Endpoint Event Resolver', function () {
before(async () => {
await esArchiver.load('endpoint/resolver_tree', { useCreate: true });
await pageObjects.hosts.navigateToSecurityHostsPage();
@@ -45,7 +44,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
const nodeData: string[] = [];
const TableData: string[] = [];
- const Table = await testSubjects.findAll('resolver:node-list:item');
+ const Table = await testSubjects.findAll('resolver:node-list:node-link:title');
for (const value of Table) {
const text = await value._webElement.getText();
TableData.push(text.split('\n')[0]);
diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
index a950b4fc3d70ab..c5d188c4139bf9 100644
--- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
+++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts
@@ -18,6 +18,7 @@ class FooPlugin implements Plugin {
id: 'foo',
name: 'Foo',
icon: 'upArrow',
+ category: { id: 'foo', label: 'foo' },
navLinkId: 'foo_plugin',
app: ['foo_plugin', 'kibana'],
catalogue: ['foo'],
diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
index dde99e7409dee3..a3bacffffd54d9 100644
--- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
+++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts
@@ -13,7 +13,15 @@ import { UserAtSpaceScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
- const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
+ const esFeatureExceptions = [
+ 'security',
+ 'index_lifecycle_management',
+ 'snapshot_restore',
+ 'rollup_jobs',
+ 'reporting',
+ 'transform',
+ 'watcher',
+ ];
describe('catalogue', () => {
UserAtSpaceScenarios.forEach((scenario) => {
diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts
index 1f19228b2d9582..ad54efca912723 100644
--- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts
+++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts
@@ -13,7 +13,15 @@ import { UserScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
- const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
+ const esFeatureExceptions = [
+ 'security',
+ 'index_lifecycle_management',
+ 'snapshot_restore',
+ 'rollup_jobs',
+ 'reporting',
+ 'transform',
+ 'watcher',
+ ];
describe('catalogue', () => {
UserScenarios.forEach((scenario) => {
diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts
index baae3286ddb5d9..1ef520d179804f 100644
--- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts
+++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts
@@ -13,7 +13,15 @@ import { SpaceScenarios } from '../scenarios';
export default function catalogueTests({ getService }: FtrProviderContext) {
const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities');
- const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher'];
+ const esFeatureExceptions = [
+ 'security',
+ 'index_lifecycle_management',
+ 'snapshot_restore',
+ 'rollup_jobs',
+ 'reporting',
+ 'transform',
+ 'watcher',
+ ];
describe('catalogue', () => {
SpaceScenarios.forEach((scenario) => {
diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json
index 0f5a7526d98bcc..44c8449dc5dd0f 100644
--- a/x-pack/tsconfig.json
+++ b/x-pack/tsconfig.json
@@ -22,7 +22,6 @@
"kibana/public": ["src/core/public"],
"kibana/server": ["src/core/server"],
"plugins/xpack_main/*": ["x-pack/legacy/plugins/xpack_main/public/*"],
- "plugins/spaces/*": ["x-pack/legacy/plugins/spaces/public/*"],
"test_utils/*": ["x-pack/test_utils/*"],
"fixtures/*": ["src/fixtures/*"]
},
diff --git a/yarn.lock b/yarn.lock
index fa5f6ff1356701..cec2697f6c15cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1226,10 +1226,10 @@
tabbable "^1.1.0"
uuid "^3.1.0"
-"@elastic/eui@28.4.0":
- version "28.4.0"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-28.4.0.tgz#6b3b6b5e8b602d009410a97c26e22093846f708a"
- integrity sha512-4iXo5fNx4qqeI+Tj5EJ0qHRhyi8KTLaqeQJCWg9Vy7N83ap6kp6s7X6D6qYUHqdmOdJH9QZYuYIpRUi3TQEJNg==
+"@elastic/eui@29.0.0":
+ version "29.0.0"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-29.0.0.tgz#1c8d822c62ad5e29298a3a36f5b02fd9b32a5550"
+ integrity sha512-YsDjtN/nRA4vvWukg5FDN4iPQgHUVxDwn/JZ1mArCeMe34JwzYJlEkk6Z/+iNbJOZQNHngmV8I2TStcP8k82gg==
dependencies:
"@types/chroma-js" "^2.0.0"
"@types/lodash" "^4.14.160"
@@ -4388,10 +4388,10 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6"
integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==
-"@types/prop-types@^15.5.3":
- version "15.5.9"
- resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0"
- integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ==
+"@types/prop-types@^15.7.3":
+ version "15.7.3"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
+ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
"@types/proper-lockfile@^3.0.1":
version "3.0.1"
@@ -9590,7 +9590,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
-create-react-class@^15.5.1:
+create-react-class@^15.5.1, create-react-class@^15.5.2:
version "15.6.3"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
integrity sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==
@@ -9599,15 +9599,6 @@ create-react-class@^15.5.1:
loose-envify "^1.3.1"
object-assign "^4.1.1"
-create-react-class@^15.5.2:
- version "15.6.2"
- resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
- integrity sha1-zx7RXxKq1/FO9fLf4F5sQvke8Co=
- dependencies:
- fbjs "^0.8.9"
- loose-envify "^1.3.1"
- object-assign "^4.1.1"
-
create-react-context@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.1.6.tgz#0f425931d907741127acc6e31acb4f9015dd9fdc"
@@ -22793,15 +22784,6 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
-prop-types@15.6.0:
- version "15.6.0"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
- integrity sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=
- dependencies:
- fbjs "^0.8.16"
- loose-envify "^1.3.1"
- object-assign "^4.1.1"
-
prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -28338,9 +28320,9 @@ typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, types
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
ua-parser-js@^0.7.18:
- version "0.7.21"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
- integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
+ version "0.7.22"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
+ integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.5"