Merge branch 'main' into screenshotting/pdf-integration-tests

This commit is contained in:
Kibana Machine 2022-03-24 08:27:40 -04:00 committed by GitHub
commit d580993d28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
739 changed files with 20250 additions and 6308 deletions

View file

@ -31,6 +31,7 @@
"expressions": "src/plugins/expressions",
"expressionShape": "src/plugins/expression_shape",
"expressionTagcloud": "src/plugins/chart_expressions/expression_tagcloud",
"eventAnnotation": "src/plugins/event_annotation",
"fieldFormats": "src/plugins/field_formats",
"flot": "packages/kbn-flot-charts/lib",
"home": "src/plugins/home",
@ -50,7 +51,10 @@
"kibana-react": "src/plugins/kibana_react",
"kibanaOverview": "src/plugins/kibana_overview",
"lists": "packages/kbn-securitysolution-list-utils/src",
"management": ["src/legacy/core_plugins/management", "src/plugins/management"],
"management": [
"src/legacy/core_plugins/management",
"src/plugins/management"
],
"monaco": "packages/kbn-monaco/src",
"navigation": "src/plugins/navigation",
"newsfeed": "src/plugins/newsfeed",
@ -62,8 +66,13 @@
"sharedUX": "src/plugins/shared_ux",
"sharedUXComponents": "packages/kbn-shared-ux-components/src",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"],
"timelion": ["src/plugins/vis_types/timelion"],
"telemetry": [
"src/plugins/telemetry",
"src/plugins/telemetry_management_section"
],
"timelion": [
"src/plugins/vis_types/timelion"
],
"uiActions": "src/plugins/ui_actions",
"uiActionsExamples": "examples/ui_action_examples",
"usageCollection": "src/plugins/usage_collection",
@ -83,4 +92,4 @@
"visualizations": "src/plugins/visualizations"
},
"translations": []
}
}

View file

@ -69,7 +69,7 @@ Every team should be collecting telemetry metrics on its public API usage. Th
### APM
Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking serveral types of transactions by default, such as `page-load`, `request`, etc.
Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking several types of transactions by default, such as `page-load`, `request`, etc.
You may introduce custom transactions. Please refer to the [APM documentation](https://www.elastic.co/guide/en/apm/get-started/current/index.html) and follow these guidelines when doing so:
- Use dashed syntax for transaction types and names: `my-transaction-type` and `my-transaction-name`

View file

@ -153,7 +153,7 @@ plugins to customize the Kibana experience. Examples of extension points are:
- core.overlays.showModal
- embeddables.registerEmbeddableFactory
- uiActions.registerAction
- core.saedObjects.registerType
- core.savedObjects.registerType
## Follow up material

View file

@ -94,6 +94,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a
|This plugin contains reusable code in the form of self-contained modules (or libraries). Each of these modules exports a set of functionality relevant to the domain of the module.
|{kib-repo}blob/{branch}/src/plugins/event_annotation/README.md[eventAnnotation]
|The Event Annotation service contains expressions for event annotations
|{kib-repo}blob/{branch}/src/plugins/expression_error/README.md[expressionError]
|Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image.

View file

@ -42,22 +42,24 @@ image::maps/images/grid_to_docs.gif[]
[role="xpack"]
[[maps-grid-aggregation]]
=== Grid aggregation
=== Clusters
Grid aggregation layers use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[GeoTile grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell.
Clusters use {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] or {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into grids. You can calculate metrics for each gridded cell.
Symbolize grid aggregation metrics as:
Symbolize cluster metrics as:
*Clusters*:: Creates a <<vector-layer, vector layer>> with a cluster symbol for each gridded cell.
*Clusters*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<vector-layer, vector layer>> with a cluster symbol for each gridded cell.
The cluster location is the weighted centroid for all documents in the gridded cell.
*Grid rectangles*:: Creates a <<vector-layer, vector layer>> with a bounding box polygon for each gridded cell.
*Grids*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<vector-layer, vector layer>> with a bounding box polygon for each gridded cell.
*Heat map*:: Creates a <<heatmap-layer, heat map layer>> that clusters the weighted centroids for each gridded cell.
*Heat map*:: Uses {ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile grid aggregation] to group your documents into grids. Creates a <<heatmap-layer, heat map layer>> that clusters the weighted centroids for each gridded cell.
To enable a grid aggregation layer:
*Hexbins*:: Uses {ref}/search-aggregations-bucket-geohexgrid-aggregation.html[Geohex grid aggregation] to group your documents into H3 hexagon grids. Creates a <<vector-layer, vector layer>> with a hexagon polygon for each gridded cell.
. Click *Add layer*, then select the *Clusters and grids* or *Heat map* layer.
To enable a clusters layer:
. Click *Add layer*, then select the *Clusters* or *Heat map* layer.
To enable a blended layer that dynamically shows clusters or documents:

View file

@ -128,7 +128,7 @@ traffic. Larger circles will symbolize grids with
more total bytes transferred, and smaller circles will symbolize
grids with less bytes transferred.
. Click **Add layer**, and select **Clusters and grids**.
. Click **Add layer**, and select **Clusters**.
. Set **Data view** to **kibana_sample_data_logs**.
. Click **Add layer**.
. In **Layer settings**, set:

View file

@ -11,7 +11,7 @@ To add a vector layer to your map, click *Add layer*, then select one of the fol
*Choropleth*:: Shaded areas to compare statistics across boundaries.
*Clusters and grids*:: Geospatial data grouped in grids with metrics for each gridded cell.
*Clusters*:: Geospatial data grouped in grids with metrics for each gridded cell.
The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape].
*Create index*:: Draw shapes on the map and index in Elasticsearch.

View file

@ -40,6 +40,11 @@ These non-persisted action tasks have a risk that they won't be run at all if th
`xpack.task_manager.ephemeral_tasks.request_capacity`::
Sets the size of the ephemeral queue defined above. Defaults to 10.
`xpack.task_manager.event_loop_delay.monitor`::
Enables event loop delay monitoring, which will log a warning when a task causes an event loop delay which exceeds the `warn_threshold` setting. Defaults to true.
`xpack.task_manager.event_loop_delay.warn_threshold`::
Sets the amount of event loop delay during a task execution which will cause a warning to be logged. Defaults to 5000 milliseconds (5 seconds).
[float]
[[task-manager-health-settings]]

View file

@ -282,7 +282,7 @@ on the {kib} index at startup. {kib} users still need to authenticate with
{es}, which is proxied through the {kib} server.
|[[elasticsearch-service-account-token]] `elasticsearch.serviceAccountToken:`
| beta[]. If your {es} is protected with basic authentication, this token provides the credentials
| If your {es} is protected with basic authentication, this token provides the credentials
that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting
is an alternative to `elasticsearch.username` and `elasticsearch.password`.

View file

@ -85,7 +85,7 @@
"**/istanbul-lib-coverage": "^3.2.0",
"**/json-schema": "^0.4.0",
"**/minimatch": "^3.1.2",
"**/minimist": "^1.2.5",
"**/minimist": "^1.2.6",
"**/node-forge": "^1.3.0",
"**/pdfkit/crypto-js": "4.0.0",
"**/react-syntax-highlighter": "^15.3.1",
@ -241,7 +241,7 @@
"deep-freeze-strict": "^1.1.1",
"deepmerge": "^4.2.2",
"del": "^5.1.0",
"elastic-apm-node": "^3.30.0",
"elastic-apm-node": "^3.31.0",
"execa": "^4.0.2",
"exit-hook": "^2.2.0",
"expiry-js": "0.1.7",
@ -648,7 +648,7 @@
"@types/mime": "^2.0.1",
"@types/mime-types": "^2.1.0",
"@types/minimatch": "^2.0.29",
"@types/minimist": "^1.2.1",
"@types/minimist": "^1.2.2",
"@types/mocha": "^9.1.0",
"@types/mock-fs": "^4.13.1",
"@types/moment-timezone": "^0.5.12",
@ -841,7 +841,7 @@
"lmdb-store": "^1.6.11",
"marge": "^1.0.1",
"micromatch": "3.1.10",
"minimist": "^1.2.5",
"minimist": "^1.2.6",
"mkdirp": "0.5.1",
"mocha": "^9.1.0",
"mocha-junit-reporter": "^2.0.2",

View file

@ -124,3 +124,4 @@ pageLoadAssetSize:
sessionView: 77750
cloudSecurityPosture: 19109
visTypeGauge: 24113
eventAnnotation: 19334

View file

@ -16447,7 +16447,7 @@ module.exports = function (args, opts) {
var o = obj;
for (var i = 0; i < keys.length-1; i++) {
var key = keys[i];
if (key === '__proto__') return;
if (isConstructorOrProto(o, key)) return;
if (o[key] === undefined) o[key] = {};
if (o[key] === Object.prototype || o[key] === Number.prototype
|| o[key] === String.prototype) o[key] = {};
@ -16456,7 +16456,7 @@ module.exports = function (args, opts) {
}
var key = keys[keys.length - 1];
if (key === '__proto__') return;
if (isConstructorOrProto(o, key)) return;
if (o === Object.prototype || o === Number.prototype
|| o === String.prototype) o = {};
if (o === Array.prototype) o = [];
@ -16621,6 +16621,10 @@ function isNumber (x) {
}
function isConstructorOrProto (obj, key) {
return key === 'constructor' && typeof obj[key] === 'function' || key === '__proto__';
}
/***/ }),
/* 229 */

View file

@ -41,9 +41,7 @@ export interface UseExceptionListsProps {
namespaceTypes: NamespaceType[];
notifications: NotificationsStart;
initialPagination?: Pagination;
showTrustedApps: boolean;
showEventFilters: boolean;
showHostIsolationExceptions: boolean;
hideLists?: readonly string[];
}
export interface UseExceptionListProps {

View file

@ -39,9 +39,7 @@ const DEFAULT_PAGINATION = {
* @param filterOptions filter by certain fields
* @param namespaceTypes spaces to be searched
* @param notifications kibana service for displaying toasters
* @param showTrustedApps boolean - include/exclude trusted app lists
* @param showEventFilters boolean - include/exclude event filters lists
* @param showHostIsolationExceptions boolean - include/exclude host isolation exceptions lists
* @param hideLists a list of listIds we don't want to query
* @param initialPagination
*
*/
@ -52,9 +50,7 @@ export const useExceptionLists = ({
filterOptions = {},
namespaceTypes,
notifications,
showTrustedApps = false,
showEventFilters = false,
showHostIsolationExceptions = false,
hideLists = [],
}: UseExceptionListsProps): ReturnExceptionLists => {
const [exceptionLists, setExceptionLists] = useState<ExceptionListSchema[]>([]);
const [pagination, setPagination] = useState<Pagination>(initialPagination);
@ -67,11 +63,9 @@ export const useExceptionLists = ({
getFilters({
filters: filterOptions,
namespaceTypes,
showTrustedApps,
showEventFilters,
showHostIsolationExceptions,
hideLists,
}),
[namespaceTypes, filterOptions, showTrustedApps, showEventFilters, showHostIsolationExceptions]
[namespaceTypes, filterOptions, hideLists]
);
const fetchData = useCallback(async (): Promise<void> => {

View file

@ -1,39 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getEventFiltersFilter } from '.';
describe('getEventFiltersFilter', () => {
test('it returns filter to search for "exception-list" namespace trusted apps', () => {
const filter = getEventFiltersFilter(true, ['exception-list']);
expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_event_filters*)');
});
test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => {
const filter = getEventFiltersFilter(true, ['exception-list', 'exception-list-agnostic']);
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
test('it returns filter to exclude "exception-list" namespace trusted apps', () => {
const filter = getEventFiltersFilter(false, ['exception-list']);
expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_event_filters*)');
});
test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => {
const filter = getEventFiltersFilter(false, ['exception-list', 'exception-list-agnostic']);
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)'
);
});
});

View file

@ -1,27 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { SavedObjectType } from '../types';
export const getEventFiltersFilter = (
showEventFilter: boolean,
namespaceTypes: SavedObjectType[]
): string => {
if (showEventFilter) {
const filters = namespaceTypes.map((namespace) => {
return `${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
});
return `(${filters.join(' OR ')})`;
} else {
const filters = namespaceTypes.map((namespace) => {
return `not ${namespace}.attributes.list_id: ${ENDPOINT_EVENT_FILTERS_LIST_ID}*`;
});
return `(${filters.join(' AND ')})`;
}
};

View file

@ -10,423 +10,198 @@ import { getFilters } from '.';
describe('getFilters', () => {
describe('single', () => {
test('it properly formats when no filters passed "showTrustedApps", "showEventFilters", and "showHostIsolationExceptions" is false', () => {
test('it properly formats when no filters and hide lists contains few list ids', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(not exception-list.attributes.list_id: listId-1*) AND (not exception-list.attributes.list_id: listId-2*) AND (not exception-list.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed "showTrustedApps", "showEventFilters", and "showHostIsolationExceptions" is true', () => {
test('it properly formats when no filters and hide lists contains one list id', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
expect(filter).toEqual('(not exception-list.attributes.list_id: listId-1*)');
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
test('it properly formats when no filters and no hide lists', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
expect(filter).toEqual('');
});
test('it if filters passed and "showTrustedApps" is true', () => {
test('it properly formats when filters passed and hide lists contains few list ids', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: listId-1*) AND (not exception-list.attributes.list_id: listId-2*) AND (not exception-list.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed and "showEventFilters" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it if filters passed and "showEventFilters" is true', () => {
test('it properly formats when filters passed and hide lists contains one list id', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: listId-1*)'
);
});
test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it if filters passed and "showHostIsolationExceptions" is true', () => {
test('it properly formats when filters passed and no hide lists', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample)'
);
});
});
describe('agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => {
test('it properly formats when no filters and hide lists contains few list ids', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(not exception-list-agnostic.attributes.list_id: listId-1*) AND (not exception-list-agnostic.attributes.list_id: listId-2*) AND (not exception-list-agnostic.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => {
test('it properly formats when no filters and hide lists contains one list id', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['agnostic'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
expect(filter).toEqual('(not exception-list-agnostic.attributes.list_id: listId-1*)');
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['agnostic'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
test('it properly formats when no filters and no hide lists', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['agnostic'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
expect(filter).toEqual('');
});
test('it if filters passed and "showTrustedApps" is true', () => {
test('it properly formats when filters passed and hide lists contains few list ids', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['agnostic'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: listId-1*) AND (not exception-list-agnostic.attributes.list_id: listId-2*) AND (not exception-list-agnostic.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed and "showEventFilters" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it if filters passed and "showEventFilters" is true', () => {
test('it properly formats when filters passed and hide lists contains one list id', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: listId-1*)'
);
});
test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it if filters passed and "showHostIsolationExceptions" is true', () => {
test('it properly formats when filters passed and no hide lists', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample)'
);
});
});
describe('single, agnostic', () => {
test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => {
test('it properly formats when no filters and hide lists contains few list ids', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*) AND (not exception-list.attributes.list_id: listId-2* AND not exception-list-agnostic.attributes.list_id: listId-2*) AND (not exception-list.attributes.list_id: listId-3* AND not exception-list-agnostic.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => {
test('it properly formats when no filters and hide lists contains one list id', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*)'
);
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: true,
showEventFilters: true,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when no filters passed and "showTrustedApps" is true', () => {
test('it properly formats when no filters and no hide lists', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
expect(filter).toEqual('');
});
test('it properly formats when filters passed and "showTrustedApps" is true', () => {
test('it properly formats when filters passed and hide lists contains few list ids', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: true,
showEventFilters: false,
showHostIsolationExceptions: false,
hideLists: ['listId-1', 'listId-2', 'listId-3'],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*) AND (not exception-list.attributes.list_id: listId-2* AND not exception-list-agnostic.attributes.list_id: listId-2*) AND (not exception-list.attributes.list_id: listId-3* AND not exception-list-agnostic.attributes.list_id: listId-3*)'
);
});
test('it properly formats when no filters passed and "showEventFilters" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when filters passed and "showEventFilters" is true', () => {
test('it properly formats when filters passed and hide lists contains one list id', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: true,
showHostIsolationExceptions: false,
hideLists: ['listId-1'],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*)'
);
});
test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => {
const filter = getFilters({
filters: {},
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
});
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it properly formats when filters passed and "showHostIsolationExceptions" is true', () => {
test('it properly formats when filters passed and no hide lists', () => {
const filter = getFilters({
filters: { created_by: 'moi', name: 'Sample' },
namespaceTypes: ['single', 'agnostic'],
showTrustedApps: false,
showEventFilters: false,
showHostIsolationExceptions: true,
hideLists: [],
});
expect(filter).toEqual(
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
'(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample)'
);
});
});

View file

@ -9,34 +9,23 @@
import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts-list-types';
import { getGeneralFilters } from '../get_general_filters';
import { getSavedObjectTypes } from '../get_saved_object_types';
import { getTrustedAppsFilter } from '../get_trusted_apps_filter';
import { getEventFiltersFilter } from '../get_event_filters_filter';
import { getHostIsolationExceptionsFilter } from '../get_host_isolation_exceptions_filter';
export interface GetFiltersParams {
filters: ExceptionListFilter;
namespaceTypes: NamespaceType[];
showTrustedApps: boolean;
showEventFilters: boolean;
showHostIsolationExceptions: boolean;
hideLists: readonly string[];
}
export const getFilters = ({
filters,
namespaceTypes,
showTrustedApps,
showEventFilters,
showHostIsolationExceptions,
}: GetFiltersParams): string => {
export const getFilters = ({ filters, namespaceTypes, hideLists }: GetFiltersParams): string => {
const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes });
const generalFilters = getGeneralFilters(filters, namespaces);
const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces);
const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces);
const hostIsolationExceptionsFilter = getHostIsolationExceptionsFilter(
showHostIsolationExceptions,
namespaces
);
return [generalFilters, trustedAppsFilter, eventFiltersFilter, hostIsolationExceptionsFilter]
const hideListsFilters = hideLists.map((listId) => {
const filtersByNamespace = namespaces.map((namespace) => {
return `not ${namespace}.attributes.list_id: ${listId}*`;
});
return `(${filtersByNamespace.join(' AND ')})`;
});
return [generalFilters, ...hideListsFilters]
.filter((filter) => filter.trim() !== '')
.join(' AND ');
};

View file

@ -1,49 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getHostIsolationExceptionsFilter } from '.';
describe('getHostIsolationExceptionsFilter', () => {
test('it returns filter to search for "exception-list" namespace host isolation exceptions', () => {
const filter = getHostIsolationExceptionsFilter(true, ['exception-list']);
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it returns filter to search for "exception-list" and "agnostic" namespace host isolation exceptions', () => {
const filter = getHostIsolationExceptionsFilter(true, [
'exception-list',
'exception-list-agnostic',
]);
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it returns filter to exclude "exception-list" namespace host isolation exceptions', () => {
const filter = getHostIsolationExceptionsFilter(false, ['exception-list']);
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
test('it returns filter to exclude "exception-list" and "agnostic" namespace host isolation exceptions', () => {
const filter = getHostIsolationExceptionsFilter(false, [
'exception-list',
'exception-list-agnostic',
]);
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)'
);
});
});

View file

@ -1,27 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { SavedObjectType } from '../types';
export const getHostIsolationExceptionsFilter = (
showFilter: boolean,
namespaceTypes: SavedObjectType[]
): string => {
if (showFilter) {
const filters = namespaceTypes.map((namespace) => {
return `${namespace}.attributes.list_id: ${ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID}*`;
});
return `(${filters.join(' OR ')})`;
} else {
const filters = namespaceTypes.map((namespace) => {
return `not ${namespace}.attributes.list_id: ${ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID}*`;
});
return `(${filters.join(' AND ')})`;
}
};

View file

@ -1,39 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getTrustedAppsFilter } from '.';
describe('getTrustedAppsFilter', () => {
test('it returns filter to search for "exception-list" namespace trusted apps', () => {
const filter = getTrustedAppsFilter(true, ['exception-list']);
expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)');
});
test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => {
const filter = getTrustedAppsFilter(true, ['exception-list', 'exception-list-agnostic']);
expect(filter).toEqual(
'(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
);
});
test('it returns filter to exclude "exception-list" namespace trusted apps', () => {
const filter = getTrustedAppsFilter(false, ['exception-list']);
expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)');
});
test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => {
const filter = getTrustedAppsFilter(false, ['exception-list', 'exception-list-agnostic']);
expect(filter).toEqual(
'(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)'
);
});
});

View file

@ -1,27 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants';
import { SavedObjectType } from '../types';
export const getTrustedAppsFilter = (
showTrustedApps: boolean,
namespaceTypes: SavedObjectType[]
): string => {
if (showTrustedApps) {
const filters = namespaceTypes.map((namespace) => {
return `${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`;
});
return `(${filters.join(' OR ')})`;
} else {
const filters = namespaceTypes.map((namespace) => {
return `not ${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`;
});
return `(${filters.join(' AND ')})`;
}
};

View file

@ -13,7 +13,6 @@ export * from './get_general_filters';
export * from './get_ids_and_namespaces';
export * from './get_saved_object_type';
export * from './get_saved_object_types';
export * from './get_trusted_apps_filter';
export * from './has_large_value_list';
export * from './helpers';
export * from './types';

View file

@ -20,10 +20,31 @@ describe('validateFilePathInput', () => {
describe('windows', () => {
const os = OperatingSystem.WINDOWS;
it('does not warn on valid filenames', () => {
expect(
validateFilePathInput({
os,
value: 'C:\\Windows\\*\\FILENAME.EXE-1231205124.gz',
})
).not.toBeDefined();
expect(
validateFilePathInput({
os,
value: "C:\\Windows\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt",
})
).toEqual(undefined);
});
it('warns on wildcard in file name at the end of the path', () => {
expect(validateFilePathInput({ os, value: 'c:\\path*.exe' })).toEqual(
FILENAME_WILDCARD_WARNING
);
expect(
validateFilePathInput({
os,
value: 'C:\\Windows\\*\\FILENAME.EXE-*.gz',
})
).toEqual(FILENAME_WILDCARD_WARNING);
});
it('warns on unix paths or non-windows paths', () => {
@ -34,6 +55,7 @@ describe('validateFilePathInput', () => {
expect(validateFilePathInput({ os, value: 'c:\\path/opt' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: 'c:\\folder\\' })).toEqual(FILEPATH_WARNING);
});
});
describe('unix paths', () => {
@ -42,8 +64,22 @@ describe('validateFilePathInput', () => {
? OperatingSystem.MAC
: OperatingSystem.LINUX;
it('does not warn on valid filenames', () => {
expect(validateFilePathInput({ os, value: '/opt/*/FILENAME.EXE-1231205124.gz' })).not.toEqual(
FILENAME_WILDCARD_WARNING
);
expect(
validateFilePathInput({
os,
value: "/opt/*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt",
})
).not.toEqual(FILENAME_WILDCARD_WARNING);
});
it('warns on wildcard in file name at the end of the path', () => {
expect(validateFilePathInput({ os, value: '/opt/bin*' })).toEqual(FILENAME_WILDCARD_WARNING);
expect(validateFilePathInput({ os, value: '/opt/FILENAME.EXE-*.gz' })).toEqual(
FILENAME_WILDCARD_WARNING
);
});
it('warns on windows paths', () => {
@ -54,6 +90,7 @@ describe('validateFilePathInput', () => {
expect(validateFilePathInput({ os, value: 'opt/bin\\file.exe' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING);
expect(validateFilePathInput({ os, value: '/folder/' })).toEqual(FILEPATH_WARNING);
});
});
});
@ -577,22 +614,56 @@ describe('Unacceptable Mac/Linux exact paths', () => {
});
});
describe('Executable filenames with wildcard PATHS', () => {
describe('hasSimpleExecutableName', () => {
it('should return TRUE when MAC/LINUX wildcard paths have an executable name', () => {
const os =
parseInt((Math.random() * 2).toString(), 10) === 1
? OperatingSystem.MAC
: OperatingSystem.LINUX;
expect(
hasSimpleExecutableName({
os: OperatingSystem.LINUX,
os,
type: 'wildcard',
value: '/opt/*/app',
})
).toEqual(true);
expect(
hasSimpleExecutableName({
os: OperatingSystem.MAC,
os,
type: 'wildcard',
value: '/op*/**/app.dmg',
})
).toEqual(true);
expect(
hasSimpleExecutableName({
os,
type: 'wildcard',
value: "/sy*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt",
})
).toEqual(true);
});
it('should return FALSE when MAC/LINUX wildcard paths have a wildcard in executable name', () => {
const os =
parseInt((Math.random() * 2).toString(), 10) === 1
? OperatingSystem.MAC
: OperatingSystem.LINUX;
expect(
hasSimpleExecutableName({
os,
type: 'wildcard',
value: '/op/*/*pp',
})
).toEqual(false);
expect(
hasSimpleExecutableName({
os,
type: 'wildcard',
value: '/op*/b**/ap.m**',
})
).toEqual(false);
});
it('should return TRUE when WINDOWS wildcards paths have a executable name', () => {
@ -603,24 +674,22 @@ describe('Executable filenames with wildcard PATHS', () => {
value: 'c:\\**\\path.exe',
})
).toEqual(true);
expect(
hasSimpleExecutableName({
os: OperatingSystem.WINDOWS,
type: 'wildcard',
value: 'C:\\*\\file-name.path华语 1234.txt',
})
).toEqual(true);
expect(
hasSimpleExecutableName({
os: OperatingSystem.WINDOWS,
type: 'wildcard',
value: "C:\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt",
})
).toEqual(true);
});
it('should return FALSE when MAC/LINUX wildcard paths have a wildcard in executable name', () => {
expect(
hasSimpleExecutableName({
os: OperatingSystem.LINUX,
type: 'wildcard',
value: '/op/*/*pp',
})
).toEqual(false);
expect(
hasSimpleExecutableName({
os: OperatingSystem.MAC,
type: 'wildcard',
value: '/op*/b**/ap.m**',
})
).toEqual(false);
});
it('should return FALSE when WINDOWS wildcards paths have a wildcard in executable name', () => {
expect(
hasSimpleExecutableName({

View file

@ -22,6 +22,20 @@ export const enum ConditionEntryField {
SIGNER = 'process.Ext.code_signature',
}
export const enum EntryFieldType {
HASH = '.hash.',
EXECUTABLE = '.executable.caseless',
PATH = '.path',
SIGNER = '.Ext.code_signature',
}
export type TrustedAppConditionEntryField =
| 'process.hash.*'
| 'process.executable.caseless'
| 'process.Ext.code_signature';
export type BlocklistConditionEntryField = 'file.hash.*' | 'file.path' | 'file.Ext.code_signature';
export type AllConditionEntryFields = TrustedAppConditionEntryField | BlocklistConditionEntryField;
export const enum OperatingSystem {
LINUX = 'linux',
MAC = 'macos',
@ -31,20 +45,6 @@ export const enum OperatingSystem {
export type EntryTypes = 'match' | 'wildcard' | 'match_any';
export type TrustedAppEntryTypes = Extract<EntryTypes, 'match' | 'wildcard'>;
/*
* regex to match executable names
* starts matching from the eol of the path
* file names with a single or multiple spaces (for spaced names)
* and hyphens and combinations of these that produce complex names
* such as:
* c:\home\lib\dmp.dmp
* c:\home\lib\my-binary-app-+/ some/ x/ dmp.dmp
* /home/lib/dmp.dmp
* /home/lib/my-binary-app+-\ some\ x\ dmp.dmp
*/
export const WIN_EXEC_PATH = /(\\[-\w]+|\\[-\w]+[\.]+[\w]+)$/i;
export const UNIX_EXEC_PATH = /(\/[-\w]+|\/[-\w]+[\.]+[\w]+)$/i;
export const validateFilePathInput = ({
os,
value = '',
@ -70,7 +70,7 @@ export const validateFilePathInput = ({
}
if (isValidFilePath) {
if (!hasSimpleFileName) {
if (hasSimpleFileName !== undefined && !hasSimpleFileName) {
return FILENAME_WILDCARD_WARNING;
}
} else {
@ -86,9 +86,14 @@ export const hasSimpleExecutableName = ({
os: OperatingSystem;
type: EntryTypes;
value: string;
}): boolean => {
}): boolean | undefined => {
const separator = os === OperatingSystem.WINDOWS ? '\\' : '/';
const lastString = value.split(separator).pop();
if (!lastString) {
return;
}
if (type === 'wildcard') {
return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value);
return (lastString.split('*').length || lastString.split('?').length) === 1;
}
return true;
};
@ -100,7 +105,7 @@ export const isPathValid = ({
value,
}: {
os: OperatingSystem;
field: ConditionEntryField | 'file.path.text';
field: AllConditionEntryFields | 'file.path.text';
type: EntryTypes;
value: string;
}): boolean => {

View file

@ -36,6 +36,7 @@ it('build default and oss dist for current platform, without packages, by defaul
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": false,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -67,6 +68,7 @@ it('builds packages if --all-platforms is passed', () => {
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": true,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -98,6 +100,7 @@ it('limits packages if --rpm passed with --all-platforms', () => {
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": true,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -129,6 +132,7 @@ it('limits packages if --deb passed with --all-platforms', () => {
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": false,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -161,6 +165,7 @@ it('limits packages if --docker passed with --all-platforms', () => {
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": false,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -200,6 +205,7 @@ it('limits packages if --docker passed with --skip-docker-ubi and --all-platform
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": false,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,
@ -232,6 +238,7 @@ it('limits packages if --all-platforms passed with --skip-docker-ubuntu', () =>
"createGenericFolders": true,
"createPlatformFolders": true,
"createRpmPackage": true,
"dockerCrossCompile": false,
"dockerPush": false,
"dockerTagQualifier": null,
"downloadCloudDependencies": true,

View file

@ -22,6 +22,7 @@ export function readCliArgs(argv: string[]) {
'skip-os-packages',
'rpm',
'deb',
'docker-cross-compile',
'docker-images',
'docker-push',
'skip-docker-contexts',
@ -52,6 +53,7 @@ export function readCliArgs(argv: string[]) {
rpm: null,
deb: null,
'docker-images': null,
'docker-cross-compile': false,
'docker-push': false,
'docker-tag-qualifier': null,
'version-qualifier': '',
@ -112,6 +114,7 @@ export function readCliArgs(argv: string[]) {
const buildOptions: BuildOptions = {
isRelease: Boolean(flags.release),
versionQualifier: flags['version-qualifier'],
dockerCrossCompile: Boolean(flags['docker-cross-compile']),
dockerPush: Boolean(flags['docker-push']),
dockerTagQualifier: flags['docker-tag-qualifier'],
initialize: !Boolean(flags['skip-initialize']),

View file

@ -13,6 +13,7 @@ import * as Tasks from './tasks';
export interface BuildOptions {
isRelease: boolean;
dockerCrossCompile: boolean;
dockerPush: boolean;
dockerTagQualifier: string | null;
downloadFreshNode: boolean;

View file

@ -39,6 +39,7 @@ if (showHelp) {
--rpm {dim Only build the rpm packages}
--deb {dim Only build the deb packages}
--docker-images {dim Only build the Docker images}
--docker-cross-compile {dim Produce arm64 and amd64 Docker images}
--docker-contexts {dim Only build the Docker build contexts}
--skip-docker-ubi {dim Don't build the docker ubi image}
--skip-docker-ubuntu {dim Don't build the docker ubuntu image}

View file

@ -32,6 +32,7 @@ const config = new Config(
buildSha: 'abcd1234',
buildVersion: '8.0.0',
},
false,
'',
false,
true

View file

@ -29,6 +29,7 @@ const setup = async ({ targetAllPlatforms = true }: { targetAllPlatforms?: boole
return await Config.create({
isRelease: true,
targetAllPlatforms,
dockerCrossCompile: false,
dockerPush: false,
dockerTagQualifier: '',
});

View file

@ -17,6 +17,7 @@ interface Options {
isRelease: boolean;
targetAllPlatforms: boolean;
versionQualifier?: string;
dockerCrossCompile: boolean;
dockerTagQualifier: string | null;
dockerPush: boolean;
}
@ -35,6 +36,7 @@ export class Config {
isRelease,
targetAllPlatforms,
versionQualifier,
dockerCrossCompile,
dockerTagQualifier,
dockerPush,
}: Options) {
@ -51,6 +53,7 @@ export class Config {
versionQualifier,
pkg,
}),
dockerCrossCompile,
dockerTagQualifier,
dockerPush,
isRelease
@ -63,6 +66,7 @@ export class Config {
private readonly nodeVersion: string,
private readonly repoRoot: string,
private readonly versionInfo: VersionInfo,
private readonly dockerCrossCompile: boolean,
private readonly dockerTagQualifier: string | null,
private readonly dockerPush: boolean,
public readonly isRelease: boolean
@ -96,6 +100,13 @@ export class Config {
return this.dockerPush;
}
/**
* Get docker cross compile
*/
getDockerCrossCompile() {
return this.dockerCrossCompile;
}
/**
* Convert an absolute path to a relative path, based from the repo
*/

View file

@ -50,6 +50,7 @@ const setup = async () => {
isRelease: true,
targetAllPlatforms: true,
versionQualifier: '-SNAPSHOT',
dockerCrossCompile: false,
dockerPush: false,
dockerTagQualifier: '',
});

View file

@ -20,18 +20,24 @@ export const DownloadCloudDependencies: Task = {
const version = config.getBuildVersion();
const buildId = id.match(/[0-9]\.[0-9]\.[0-9]-[0-9a-z]{8}/);
const buildIdUrl = buildId ? `${buildId[0]}/` : '';
const architecture = process.arch === 'arm64' ? 'arm64' : 'x86_64';
const url = `https://${subdomain}-no-kpi.elastic.co/${buildIdUrl}downloads/beats/${beat}/${beat}-${version}-linux-${architecture}.tar.gz`;
const checksum = await downloadToString({ log, url: url + '.sha512', expectStatus: 200 });
const destination = config.resolveFromRepo('.beats', Path.basename(url));
return downloadToDisk({
log,
url,
destination,
shaChecksum: checksum.split(' ')[0],
shaAlgorithm: 'sha512',
maxAttempts: 3,
const localArchitecture = [process.arch === 'arm64' ? 'arm64' : 'x86_64'];
const allArchitectures = ['arm64', 'x86_64'];
const architectures = config.getDockerCrossCompile() ? allArchitectures : localArchitecture;
const downloads = architectures.map(async (arch) => {
const url = `https://${subdomain}-no-kpi.elastic.co/${buildIdUrl}downloads/beats/${beat}/${beat}-${version}-linux-${arch}.tar.gz`;
const checksum = await downloadToString({ log, url: url + '.sha512', expectStatus: 200 });
const destination = config.resolveFromRepo('.beats', Path.basename(url));
return downloadToDisk({
log,
url,
destination,
shaChecksum: checksum.split(' ')[0],
shaAlgorithm: 'sha512',
maxAttempts: 3,
});
});
return Promise.all(downloads);
};
let buildId = '';

View file

@ -39,6 +39,7 @@ async function setup({ failOnUrl }: { failOnUrl?: string } = {}) {
const config = await Config.create({
isRelease: true,
targetAllPlatforms: true,
dockerCrossCompile: false,
dockerPush: false,
dockerTagQualifier: '',
});

View file

@ -43,6 +43,7 @@ async function setup() {
const config = await Config.create({
isRelease: true,
targetAllPlatforms: true,
dockerCrossCompile: false,
dockerPush: false,
dockerTagQualifier: '',
});

View file

@ -48,6 +48,7 @@ async function setup(actualShaSums?: Record<string, string>) {
const config = await Config.create({
isRelease: true,
targetAllPlatforms: true,
dockerCrossCompile: false,
dockerPush: false,
dockerTagQualifier: '',
});

View file

@ -379,6 +379,8 @@ kibana_vars=(
xpack.task_manager.poll_interval
xpack.task_manager.request_capacity
xpack.task_manager.version_conflict_threshold
xpack.task_manager.event_loop_delay.monitor
xpack.task_manager.event_loop_delay.warn_threshold
xpack.uptime.index
)

View file

@ -76,6 +76,7 @@ export async function runDockerGenerator(
const dockerPush = config.getDockerPush();
const dockerTagQualifier = config.getDockerTagQualfiier();
const dockerCrossCompile = config.getDockerCrossCompile();
const publicArtifactSubdomain = config.isRelease ? 'artifacts' : 'snapshots-no-kpi';
const scope: TemplateContext = {
@ -110,7 +111,7 @@ export async function runDockerGenerator(
arm64: 'aarch64',
};
const buildArchitectureSupported = hostTarget[process.arch] === flags.architecture;
if (flags.architecture && !buildArchitectureSupported) {
if (flags.architecture && !buildArchitectureSupported && !dockerCrossCompile) {
return;
}

View file

@ -23,6 +23,7 @@ function generator({
const dockerTargetName = `${imageTag}${imageFlavor}:${version}${
dockerTagQualifier ? '-' + dockerTagQualifier : ''
}`;
const dockerArchitecture = architecture === 'aarch64' ? 'linux/arm64' : 'linux/amd64';
return dedent(`
#!/usr/bin/env bash
#
@ -59,7 +60,7 @@ function generator({
retry_docker_pull ${baseOSImage}
echo "Building: kibana${imageFlavor}-docker"; \\
docker build -t ${dockerTargetName} -f Dockerfile . || exit 1;
docker buildx build --platform ${dockerArchitecture} -t ${dockerTargetName} -f Dockerfile . || exit 1;
docker save ${dockerTargetName} | gzip -c > ${dockerTargetFilename}

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ControlGroupInput } from '..';
import { ControlStyle, ControlWidth } from '../types';
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';
export const getDefaultControlGroupInput = (): Omit<ControlGroupInput, 'id'> => ({
panels: {},
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
controlStyle: DEFAULT_CONTROL_STYLE,
chainingSystem: 'HIERARCHICAL',
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
ignoreValidations: false,
},
});

View file

@ -17,11 +17,14 @@ export interface ControlPanelState<TEmbeddableInput extends ControlInput = Contr
width: ControlWidth;
}
export type ControlGroupChainingSystem = 'HIERARCHICAL' | 'NONE';
export interface ControlsPanels {
[panelId: string]: ControlPanelState;
}
export interface ControlGroupInput extends EmbeddableInput, ControlInput {
chainingSystem: ControlGroupChainingSystem;
defaultControlWidth?: ControlWidth;
controlStyle: ControlStyle;
panels: ControlsPanels;

View file

@ -12,3 +12,4 @@ export type { ControlWidth } from './types';
export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types';
export { CONTROL_GROUP_TYPE } from './control_group/types';
export { getDefaultControlGroupInput } from './control_group/control_group_constants';

View file

@ -89,6 +89,7 @@ const ControlGroupStoryComponent: FC<{
);
const controlGroupContainerEmbeddable = await factory.create({
controlStyle: 'oneLine',
chainingSystem: 'NONE', // a chaining system doesn't make sense in storybook since the controls aren't backed by elasticsearch
panels: panels ?? {},
id: uuid.v4(),
viewMode,

View file

@ -46,7 +46,7 @@ export const ControlGroupStrings = {
}),
getTitleInputTitle: () =>
i18n.translate('controls.controlGroup.manageControl.titleInputTitle', {
defaultMessage: 'Title',
defaultMessage: 'Label',
}),
getControlTypeTitle: () =>
i18n.translate('controls.controlGroup.manageControl.controlTypesTitle', {
@ -82,10 +82,6 @@ export const ControlGroupStrings = {
i18n.translate('controls.controlGroup.management.defaultWidthTitle', {
defaultMessage: 'Default size',
}),
getLayoutTitle: () =>
i18n.translate('controls.controlGroup.management.layoutTitle', {
defaultMessage: 'Layout',
}),
getDeleteButtonTitle: () =>
i18n.translate('controls.controlGroup.management.delete', {
defaultMessage: 'Delete control',
@ -120,18 +116,22 @@ export const ControlGroupStrings = {
defaultMessage: 'Large',
}),
},
controlStyle: {
getDesignSwitchLegend: () =>
i18n.translate('controls.controlGroup.management.layout.designSwitchLegend', {
defaultMessage: 'Switch control designs',
labelPosition: {
getLabelPositionTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.title', {
defaultMessage: 'Label position',
}),
getSingleLineTitle: () =>
i18n.translate('controls.controlGroup.management.layout.singleLine', {
defaultMessage: 'Single line',
getLabelPositionLegend: () =>
i18n.translate('controls.controlGroup.management.labelPosition.designSwitchLegend', {
defaultMessage: 'Switch label position between inline and above',
}),
getTwoLineTitle: () =>
i18n.translate('controls.controlGroup.management.layout.twoLine', {
defaultMessage: 'Double line',
getInlineTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.inline', {
defaultMessage: 'Inline',
}),
getAboveTitle: () =>
i18n.translate('controls.controlGroup.management.labelPosition.above', {
defaultMessage: 'Above',
}),
},
deleteControls: {
@ -192,6 +192,55 @@ export const ControlGroupStrings = {
defaultMessage: 'Cancel',
}),
},
validateSelections: {
getValidateSelectionsTitle: () =>
i18n.translate('controls.controlGroup.management.validate.title', {
defaultMessage: 'Validate user selections',
}),
getValidateSelectionsSubTitle: () =>
i18n.translate('controls.controlGroup.management.validate.subtitle', {
defaultMessage:
'Automatically ignore any control selection that would result in no data.',
}),
},
controlChaining: {
getHierarchyTitle: () =>
i18n.translate('controls.controlGroup.management.hierarchy.title', {
defaultMessage: 'Chain controls',
}),
getHierarchySubTitle: () =>
i18n.translate('controls.controlGroup.management.hierarchy.subtitle', {
defaultMessage:
'Selections in one control narrow down available options in the next. Controls are chained from left to right.',
}),
},
querySync: {
getQuerySettingsTitle: () =>
i18n.translate('controls.controlGroup.management.query.searchSettingsTitle', {
defaultMessage: 'Sync with query bar',
}),
getQuerySettingsSubtitle: () =>
i18n.translate('controls.controlGroup.management.query.useAllSearchSettingsTitle', {
defaultMessage:
'Keeps the control group in sync with the query bar by applying time range, filter pills, and queries from the query bar',
}),
getAdvancedSettingsTitle: () =>
i18n.translate('controls.controlGroup.management.query.advancedSettings', {
defaultMessage: 'Advanced',
}),
getIgnoreTimerangeTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreTimerange', {
defaultMessage: 'Ignore timerange',
}),
getIgnoreQueryTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreQuery', {
defaultMessage: 'Ignore query bar',
}),
getIgnoreFilterPillsTitle: () =>
i18n.translate('controls.controlGroup.management.query.ignoreFilterPills', {
defaultMessage: 'Ignore filter pills',
}),
},
},
floatingActions: {
getEditButtonTitle: () =>

View file

@ -14,7 +14,9 @@
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { omit } from 'lodash';
import fastIsEqual from 'fast-deep-equal';
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiFlyoutHeader,
EuiButtonGroup,
@ -28,38 +30,101 @@ import {
EuiButtonEmpty,
EuiSpacer,
EuiCheckbox,
EuiForm,
EuiAccordion,
useGeneratedHtmlId,
EuiSwitch,
EuiText,
EuiHorizontalRule,
} from '@elastic/eui';
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from './editor_constants';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlStyle, ControlWidth } from '../../types';
import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from './editor_constants';
import { ParentIgnoreSettings } from '../..';
import { ControlsPanels } from '../types';
import { ControlGroupInput } from '..';
import {
DEFAULT_CONTROL_WIDTH,
getDefaultControlGroupInput,
} from '../../../common/control_group/control_group_constants';
interface EditControlGroupProps {
width: ControlWidth;
controlStyle: ControlStyle;
setAllWidths: boolean;
initialInput: ControlGroupInput;
controlCount: number;
updateControlStyle: (controlStyle: ControlStyle) => void;
updateWidth: (newWidth: ControlWidth) => void;
updateAllControlWidths: (newWidth: ControlWidth) => void;
onCancel: () => void;
updateInput: (input: Partial<ControlGroupInput>) => void;
onDeleteAll: () => void;
onClose: () => void;
}
type EditorControlGroupInput = ControlGroupInput &
Required<Pick<ControlGroupInput, 'defaultControlWidth'>>;
const editorControlGroupInputIsEqual = (a: ControlGroupInput, b: ControlGroupInput) =>
fastIsEqual(a, b);
export const ControlGroupEditor = ({
width,
controlStyle,
setAllWidths,
controlCount,
updateControlStyle,
updateWidth,
updateAllControlWidths,
onCancel,
initialInput,
updateInput,
onDeleteAll,
onClose,
}: EditControlGroupProps) => {
const [currentControlStyle, setCurrentControlStyle] = useState(controlStyle);
const [currentWidth, setCurrentWidth] = useState(width);
const [applyToAll, setApplyToAll] = useState(setAllWidths);
const [resetAllWidths, setResetAllWidths] = useState(false);
const advancedSettingsAccordionId = useGeneratedHtmlId({ prefix: 'advancedSettingsAccordion' });
const [controlGroupEditorState, setControlGroupEditorState] = useState<EditorControlGroupInput>({
defaultControlWidth: DEFAULT_CONTROL_WIDTH,
...getDefaultControlGroupInput(),
...initialInput,
});
const updateControlGroupEditorSetting = useCallback(
(newSettings: Partial<ControlGroupInput>) => {
setControlGroupEditorState({
...controlGroupEditorState,
...newSettings,
});
},
[controlGroupEditorState]
);
const updateIgnoreSetting = useCallback(
(newSettings: Partial<ParentIgnoreSettings>) => {
setControlGroupEditorState({
...controlGroupEditorState,
ignoreParentSettings: {
...(controlGroupEditorState.ignoreParentSettings ?? {}),
...newSettings,
},
});
},
[controlGroupEditorState]
);
const fullQuerySyncActive = useMemo(
() =>
!Object.values(omit(controlGroupEditorState.ignoreParentSettings, 'ignoreValidations')).some(
Boolean
),
[controlGroupEditorState]
);
const applyChangesToInput = useCallback(() => {
const inputToApply = { ...controlGroupEditorState };
if (resetAllWidths) {
const newPanels = {} as ControlsPanels;
Object.entries(initialInput.panels).forEach(
([id, panel]) =>
(newPanels[id] = {
...panel,
width: inputToApply.defaultControlWidth,
})
);
inputToApply.panels = newPanels;
}
if (!editorControlGroupInputIsEqual(inputToApply, initialInput)) updateInput(inputToApply);
}, [controlGroupEditorState, resetAllWidths, initialInput, updateInput]);
return (
<>
@ -69,57 +134,183 @@ export const ControlGroupEditor = ({
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj="control-group-settings-flyout">
<EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}>
<EuiButtonGroup
color="primary"
idSelected={currentControlStyle}
legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()}
data-test-subj="control-group-layout-options"
options={CONTROL_LAYOUT_OPTIONS}
onChange={(newControlStyle: string) => {
setCurrentControlStyle(newControlStyle as ControlStyle);
}}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
<EuiButtonGroup
color="primary"
idSelected={currentWidth}
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
data-test-subj="control-group-default-size-options"
options={CONTROL_WIDTH_OPTIONS}
onChange={(newWidth: string) => {
setCurrentWidth(newWidth as ControlWidth);
}}
/>
</EuiFormRow>
{controlCount > 0 ? (
<>
<EuiSpacer size="s" />
<EuiCheckbox
id="editControls_setAllSizesCheckbox"
data-test-subj="set-all-control-sizes-checkbox"
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
checked={applyToAll}
onChange={(e) => {
setApplyToAll(e.target.checked);
<EuiForm>
<EuiFormRow label={ControlGroupStrings.management.labelPosition.getLabelPositionTitle()}>
<EuiButtonGroup
color="primary"
options={CONTROL_LAYOUT_OPTIONS}
data-test-subj="control-group-layout-options"
idSelected={controlGroupEditorState.controlStyle}
legend={ControlGroupStrings.management.labelPosition.getLabelPositionLegend()}
onChange={(newControlStyle: string) => {
// The UI copy calls this setting labelPosition, but to avoid an unnecessary migration it will be left as controlStyle in the state.
updateControlGroupEditorSetting({ controlStyle: newControlStyle as ControlStyle });
}}
/>
<EuiSpacer size="l" />
<EuiButtonEmpty
onClick={onCancel}
aria-label={'delete-all'}
data-test-subj="delete-all-controls-button"
iconType="trash"
color="danger"
flush="left"
size="s"
>
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
</EuiButtonEmpty>
</>
) : null}
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow label={ControlGroupStrings.management.getDefaultWidthTitle()}>
<>
<EuiButtonGroup
color="primary"
data-test-subj="control-group-default-size-options"
idSelected={controlGroupEditorState.defaultControlWidth}
legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()}
options={CONTROL_WIDTH_OPTIONS}
onChange={(newWidth: string) => {
updateControlGroupEditorSetting({
defaultControlWidth: newWidth as ControlWidth,
});
}}
/>
{controlCount > 0 && (
<>
<EuiSpacer size="s" />
<EuiCheckbox
id="editControls_setAllSizesCheckbox"
data-test-subj="set-all-control-sizes-checkbox"
label={ControlGroupStrings.management.getSetAllWidthsToDefaultTitle()}
checked={resetAllWidths}
onChange={(e) => {
setResetAllWidths(e.target.checked);
}}
/>
</>
)}
</>
</EuiFormRow>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
label={ControlGroupStrings.management.querySync.getQuerySettingsTitle()}
data-test-subj="control-group-query-sync"
showLabel={false}
checked={fullQuerySyncActive}
onChange={(e) => {
const newSetting = !e.target.checked;
updateIgnoreSetting({
ignoreFilters: newSetting,
ignoreTimerange: newSetting,
ignoreQuery: newSetting,
});
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>{ControlGroupStrings.management.querySync.getQuerySettingsTitle()}</h3>
</EuiTitle>
<EuiText size="s">
<p>{ControlGroupStrings.management.querySync.getQuerySettingsSubtitle()}</p>
</EuiText>
<EuiSpacer size="s" />
<EuiAccordion
data-test-subj="control-group-query-sync-advanced"
id={advancedSettingsAccordionId}
initialIsOpen={!fullQuerySyncActive}
buttonContent={ControlGroupStrings.management.querySync.getAdvancedSettingsTitle()}
>
<EuiSpacer size="s" />
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-time-range"
label={ControlGroupStrings.management.querySync.getIgnoreTimerangeTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreTimerange)}
onChange={(e) => updateIgnoreSetting({ ignoreTimerange: e.target.checked })}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-query"
label={ControlGroupStrings.management.querySync.getIgnoreQueryTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreQuery)}
onChange={(e) => updateIgnoreSetting({ ignoreQuery: e.target.checked })}
/>
</EuiFormRow>
<EuiFormRow hasChildLabel display="columnCompressedSwitch">
<EuiSwitch
data-test-subj="control-group-query-sync-filters"
label={ControlGroupStrings.management.querySync.getIgnoreFilterPillsTitle()}
compressed
checked={Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreFilters)}
onChange={(e) => updateIgnoreSetting({ ignoreFilters: e.target.checked })}
/>
</EuiFormRow>
</EuiAccordion>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
data-test-subj="control-group-validate-selections"
label={ControlGroupStrings.management.validateSelections.getValidateSelectionsTitle()}
showLabel={false}
checked={!Boolean(controlGroupEditorState.ignoreParentSettings?.ignoreValidations)}
onChange={(e) => updateIgnoreSetting({ ignoreValidations: !e.target.checked })}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>
{ControlGroupStrings.management.validateSelections.getValidateSelectionsTitle()}
</h3>
</EuiTitle>
<EuiText size="s">
<p>
{ControlGroupStrings.management.validateSelections.getValidateSelectionsSubTitle()}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<EuiSwitch
data-test-subj="control-group-chaining"
label={ControlGroupStrings.management.controlChaining.getHierarchyTitle()}
showLabel={false}
checked={controlGroupEditorState.chainingSystem === 'HIERARCHICAL'}
onChange={(e) =>
updateControlGroupEditorSetting({
chainingSystem: e.target.checked ? 'HIERARCHICAL' : 'NONE',
})
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="xxs">
<h3>{ControlGroupStrings.management.controlChaining.getHierarchyTitle()}</h3>
</EuiTitle>
<EuiText size="s">
<p>{ControlGroupStrings.management.controlChaining.getHierarchySubTitle()}</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
{controlCount > 0 && (
<>
<EuiHorizontalRule margin="m" />
<EuiSpacer size="m" />
<EuiButtonEmpty
onClick={onDeleteAll}
data-test-subj="delete-all-controls-button"
aria-label={'delete-all'}
iconType="trash"
color="danger"
flush="left"
size="s"
>
{ControlGroupStrings.management.getDeleteAllButtonTitle()}
</EuiButtonEmpty>
</>
)}
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup responsive={false} justifyContent="spaceBetween">
@ -141,15 +332,7 @@ export const ControlGroupEditor = ({
color="primary"
data-test-subj="control-group-editor-save"
onClick={() => {
if (currentControlStyle && currentControlStyle !== controlStyle) {
updateControlStyle(currentControlStyle);
}
if (currentWidth && currentWidth !== width) {
updateWidth(currentWidth);
}
if (applyToAll) {
updateAllControlWidths(currentWidth);
}
applyChangesToInput();
onClose();
}}
>

View file

@ -12,10 +12,10 @@ import React from 'react';
import { pluginServices } from '../../services';
import { ControlEditor } from './control_editor';
import { OverlayRef } from '../../../../../core/public';
import { DEFAULT_CONTROL_WIDTH } from './editor_constants';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlWidth, ControlInput, IEditableControlFactory } from '../../types';
import { toMountPoint } from '../../../../kibana_react/public';
import { DEFAULT_CONTROL_WIDTH } from '../../../common/control_group/control_group_constants';
export type CreateControlButtonTypes = 'toolbar' | 'callout';
export interface CreateControlButtonProps {

View file

@ -9,34 +9,20 @@
import React from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { DEFAULT_CONTROL_WIDTH, DEFAULT_CONTROL_STYLE } from './editor_constants';
import { ControlsPanels } from '../types';
import { pluginServices } from '../../services';
import { ControlStyle, ControlWidth } from '../../types';
import { ControlGroupStrings } from '../control_group_strings';
import { toMountPoint } from '../../../../kibana_react/public';
import { OverlayRef } from '../../../../../core/public';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlGroupEditor } from './control_group_editor';
import { OverlayRef } from '../../../../../core/public';
import { pluginServices } from '../../services';
import { ControlGroupContainer } from '..';
export interface EditControlGroupButtonProps {
controlStyle: ControlStyle;
panels?: ControlsPanels;
defaultControlWidth?: ControlWidth;
setControlStyle: (setControlStyle: ControlStyle) => void;
setDefaultControlWidth: (defaultControlWidth: ControlWidth) => void;
setAllControlWidths: (defaultControlWidth: ControlWidth) => void;
removeEmbeddable?: (panelId: string) => void;
controlGroupContainer: ControlGroupContainer;
closePopover: () => void;
}
export const EditControlGroup = ({
panels,
defaultControlWidth,
controlStyle,
setControlStyle,
setDefaultControlWidth,
setAllControlWidths,
removeEmbeddable,
controlGroupContainer,
closePopover,
}: EditControlGroupButtonProps) => {
const { overlays } = pluginServices.getServices();
@ -45,15 +31,17 @@ export const EditControlGroup = ({
const editControlGroup = () => {
const PresentationUtilProvider = pluginServices.getContextProvider();
const onCancel = (ref: OverlayRef) => {
if (!removeEmbeddable || !panels) return;
const onDeleteAll = (ref: OverlayRef) => {
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
confirmButtonText: ControlGroupStrings.management.deleteControls.getConfirm(),
cancelButtonText: ControlGroupStrings.management.deleteControls.getCancel(),
title: ControlGroupStrings.management.deleteControls.getDeleteAllTitle(),
buttonColor: 'danger',
}).then((confirmed) => {
if (confirmed) Object.keys(panels).forEach((panelId) => removeEmbeddable(panelId));
if (confirmed)
Object.keys(controlGroupContainer.getInput().panels).forEach((panelId) =>
controlGroupContainer.removeEmbeddable(panelId)
);
ref.close();
});
};
@ -62,14 +50,10 @@ export const EditControlGroup = ({
toMountPoint(
<PresentationUtilProvider>
<ControlGroupEditor
width={defaultControlWidth ?? DEFAULT_CONTROL_WIDTH}
controlStyle={controlStyle ?? DEFAULT_CONTROL_STYLE}
setAllWidths={false}
controlCount={Object.keys(panels ?? {}).length}
updateControlStyle={setControlStyle}
updateWidth={setDefaultControlWidth}
updateAllControlWidths={setAllControlWidths}
onCancel={() => onCancel(flyoutInstance)}
initialInput={controlGroupContainer.getInput()}
updateInput={(changes) => controlGroupContainer.updateInput(changes)}
controlCount={Object.keys(controlGroupContainer.getInput().panels ?? {}).length}
onDeleteAll={() => onDeleteAll(flyoutInstance)}
onClose={() => flyoutInstance.close()}
/>
</PresentationUtilProvider>

View file

@ -6,12 +6,8 @@
* Side Public License, v 1.
*/
import { ControlStyle, ControlWidth } from '../../types';
import { ControlGroupStrings } from '../control_group_strings';
export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const DEFAULT_CONTROL_STYLE: ControlStyle = 'oneLine';
export const CONTROL_WIDTH_OPTIONS = [
{
id: `auto`,
@ -39,11 +35,11 @@ export const CONTROL_LAYOUT_OPTIONS = [
{
id: `oneLine`,
'data-test-subj': 'control-editor-layout-oneLine',
label: ControlGroupStrings.management.controlStyle.getSingleLineTitle(),
label: ControlGroupStrings.management.labelPosition.getInlineTitle(),
},
{
id: `twoLine`,
'data-test-subj': 'control-editor-layout-twoLine',
label: ControlGroupStrings.management.controlStyle.getTwoLineTitle(),
label: ControlGroupStrings.management.labelPosition.getAboveTitle(),
},
];

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Filter } from '@kbn/es-query';
import { Subject } from 'rxjs';
import { ControlEmbeddable } from '../../types';
import { ChildEmbeddableOrderCache } from './control_group_container';
import { EmbeddableContainerSettings, isErrorEmbeddable } from '../../../../embeddable/public';
import { ControlGroupChainingSystem, ControlGroupInput } from '../../../common/control_group/types';
interface GetPrecedingFiltersProps {
id: string;
childOrder: ChildEmbeddableOrderCache;
getChild: (id: string) => ControlEmbeddable;
}
interface OnChildChangedProps {
childOutputChangedId: string;
recalculateFilters$: Subject<null>;
childOrder: ChildEmbeddableOrderCache;
getChild: (id: string) => ControlEmbeddable;
}
interface ChainingSystem {
getContainerSettings: (
initialInput: ControlGroupInput
) => EmbeddableContainerSettings | undefined;
getPrecedingFilters: (props: GetPrecedingFiltersProps) => Filter[] | undefined;
onChildChange: (props: OnChildChangedProps) => void;
}
export const ControlGroupChainingSystems: {
[key in ControlGroupChainingSystem]: ChainingSystem;
} = {
HIERARCHICAL: {
getContainerSettings: (initialInput) => ({
childIdInitializeOrder: Object.values(initialInput.panels)
.sort((a, b) => (a.order > b.order ? 1 : -1))
.map((panel) => panel.explicitInput.id),
initializeSequentially: true,
}),
getPrecedingFilters: ({ id, childOrder, getChild }) => {
let filters: Filter[] = [];
const order = childOrder.IdsToOrder?.[id];
if (!order || order === 0) return filters;
for (let i = 0; i < order; i++) {
const embeddable = getChild(childOrder.idsInOrder[i]);
if (!embeddable || isErrorEmbeddable(embeddable)) return filters;
filters = [...filters, ...(embeddable.getOutput().filters ?? [])];
}
return filters;
},
onChildChange: ({ childOutputChangedId, childOrder, recalculateFilters$, getChild }) => {
if (childOutputChangedId === childOrder.lastChildId) {
// the last control's output has updated, recalculate filters
recalculateFilters$.next();
return;
}
// when output changes on a child which isn't the last - make the next embeddable updateInputFromParent
const nextOrder = childOrder.IdsToOrder[childOutputChangedId] + 1;
if (nextOrder >= childOrder.idsInOrder.length) return;
setTimeout(
() => getChild(childOrder.idsInOrder[nextOrder]).refreshInputFromParent(),
1 // run on next tick
);
},
},
NONE: {
getContainerSettings: () => undefined,
getPrecedingFilters: () => undefined,
onChildChange: ({ recalculateFilters$ }) => recalculateFilters$.next(),
},
};

View file

@ -40,18 +40,18 @@ import { pluginServices } from '../../services';
import { DataView } from '../../../../data_views/public';
import { ControlGroupStrings } from '../control_group_strings';
import { EditControlGroup } from '../editor/edit_control_group';
import { DEFAULT_CONTROL_WIDTH } from '../editor/editor_constants';
import { ControlGroup } from '../component/control_group_component';
import { controlGroupReducers } from '../state/control_group_reducers';
import { Container, EmbeddableFactory } from '../../../../embeddable/public';
import { ControlEmbeddable, ControlInput, ControlOutput } from '../../types';
import { ControlGroupChainingSystems } from './control_group_chaining_system';
import { CreateControlButton, CreateControlButtonTypes } from '../editor/create_control';
import { Container, EmbeddableFactory, isErrorEmbeddable } from '../../../../embeddable/public';
const ControlGroupReduxWrapper = withSuspense<
ReduxEmbeddableWrapperPropsWithChildren<ControlGroupInput>
>(LazyReduxEmbeddableWrapper);
interface ChildEmbeddableOrderCache {
export interface ChildEmbeddableOrderCache {
IdsToOrder: { [key: string]: number };
idsInOrder: string[];
lastChildId: string;
@ -104,22 +104,7 @@ export class ControlGroupContainer extends Container<
};
private getEditControlGroupButton = (closePopover: () => void) => {
return (
<EditControlGroup
controlStyle={this.getInput().controlStyle}
panels={this.getInput().panels}
defaultControlWidth={this.getInput().defaultControlWidth}
setControlStyle={(controlStyle) => this.updateInput({ controlStyle })}
setDefaultControlWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })}
setAllControlWidths={(defaultControlWidth) => {
Object.keys(this.getInput().panels).forEach(
(panelId) => (this.getInput().panels[panelId].width = defaultControlWidth)
);
}}
removeEmbeddable={(id) => this.removeEmbeddable(id)}
closePopover={closePopover}
/>
);
return <EditControlGroup controlGroupContainer={this} closePopover={closePopover} />;
};
/**
@ -154,12 +139,7 @@ export class ControlGroupContainer extends Container<
{ embeddableLoaded: {} },
pluginServices.getServices().controls.getControlFactory,
parent,
{
childIdInitializeOrder: Object.values(initialInput.panels)
.sort((a, b) => (a.order > b.order ? 1 : -1))
.map((panel) => panel.explicitInput.id),
initializeSequentially: true,
}
ControlGroupChainingSystems[initialInput.chainingSystem]?.getContainerSettings(initialInput)
);
this.recalculateFilters$ = new Subject();
@ -226,20 +206,12 @@ export class ControlGroupContainer extends Container<
.pipe(anyChildChangePipe)
.subscribe((childOutputChangedId) => {
this.recalculateDataViews();
if (childOutputChangedId === this.childOrderCache.lastChildId) {
// the last control's output has updated, recalculate filters
this.recalculateFilters$.next();
return;
}
// when output changes on a child which isn't the last - make the next embeddable updateInputFromParent
const nextOrder = this.childOrderCache.IdsToOrder[childOutputChangedId] + 1;
if (nextOrder >= Object.keys(this.children).length) return;
setTimeout(
() =>
this.getChild(this.childOrderCache.idsInOrder[nextOrder]).refreshInputFromParent(),
1 // run on next tick
);
ControlGroupChainingSystems[this.getInput().chainingSystem].onChildChange({
childOutputChangedId,
childOrder: this.childOrderCache,
getChild: (id) => this.getChild(id),
recalculateFilters$: this.recalculateFilters$,
});
})
);
@ -251,18 +223,6 @@ export class ControlGroupContainer extends Container<
);
};
private getPrecedingFilters = (id: string) => {
let filters: Filter[] = [];
const order = this.childOrderCache.IdsToOrder?.[id];
if (!order || order === 0) return filters;
for (let i = 0; i < order; i++) {
const embeddable = this.getChild<ControlEmbeddable>(this.childOrderCache.idsInOrder[i]);
if (!embeddable || isErrorEmbeddable(embeddable)) return filters;
filters = [...filters, ...(embeddable.getOutput().filters ?? [])];
}
return filters;
};
private getEmbeddableOrderCache = (): ChildEmbeddableOrderCache => {
const panels = this.getInput().panels;
const IdsToOrder: { [key: string]: number } = {};
@ -314,20 +274,25 @@ export class ControlGroupContainer extends Container<
}
return {
order: nextOrder,
width: this.getInput().defaultControlWidth ?? DEFAULT_CONTROL_WIDTH,
width: this.getInput().defaultControlWidth,
...panelState,
} as ControlPanelState<TEmbeddableInput>;
}
protected getInheritedInput(id: string): ControlInput {
const { filters, query, ignoreParentSettings, timeRange } = this.getInput();
const { filters, query, ignoreParentSettings, timeRange, chainingSystem } = this.getInput();
const precedingFilters = this.getPrecedingFilters(id);
const precedingFilters = ControlGroupChainingSystems[chainingSystem].getPrecedingFilters({
id,
childOrder: this.childOrderCache,
getChild: (getChildId: string) => this.getChild<ControlEmbeddable>(getChildId),
});
const allFilters = [
...(ignoreParentSettings?.ignoreFilters ? [] : filters ?? []),
...precedingFilters,
...(precedingFilters ?? []),
];
return {
ignoreParentSettings,
filters: allFilters,
query: ignoreParentSettings?.ignoreQuery ? undefined : query,
timeRange: ignoreParentSettings?.ignoreTimerange ? undefined : timeRange,

View file

@ -23,6 +23,7 @@ import {
createControlGroupExtract,
createControlGroupInject,
} from '../../../common/control_group/control_group_persistable_state';
import { getDefaultControlGroupInput } from '../../../common/control_group/control_group_constants';
export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition {
public readonly isContainerType = true;
@ -42,14 +43,7 @@ export class ControlGroupContainerFactory implements EmbeddableFactoryDefinition
};
public getDefaultInput(): Partial<ControlGroupInput> {
return {
panels: {},
ignoreParentSettings: {
ignoreFilters: false,
ignoreQuery: false,
ignoreTimerange: false,
},
};
return getDefaultControlGroupInput();
}
public create = async (initialInput: ControlGroupInput, parent?: Container) => {

View file

@ -56,6 +56,7 @@ interface OptionsListDataFetchProps {
search?: string;
fieldName: string;
dataViewId: string;
validate?: boolean;
query?: ControlInput['query'];
filters?: ControlInput['filters'];
}
@ -115,6 +116,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
private setupSubscriptions = () => {
const dataFetchPipe = this.getInput$().pipe(
map((newInput) => ({
validate: !Boolean(newInput.ignoreParentSettings?.ignoreValidations),
lastReloadRequestTime: newInput.lastReloadRequestTime,
dataViewId: newInput.dataViewId,
fieldName: newInput.fieldName,
@ -218,12 +220,12 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
await this.optionsListService.runOptionsListRequest(
{
field,
query,
filters,
dataView,
timeRange,
selectedOptions,
searchString: this.searchString,
...(ignoreParentSettings?.ignoreQuery ? {} : { query }),
...(ignoreParentSettings?.ignoreFilters ? {} : { filters }),
...(ignoreParentSettings?.ignoreTimerange ? {} : { timeRange }),
},
this.abortController.signal
);

View file

@ -22,6 +22,7 @@ import { CONTROL_GROUP_TYPE } from '../../../controls/common';
const getPanelStatePrefix = (state: DashboardPanelState) => `${state.explicitInput.id}:`;
const controlGroupReferencePrefix = 'controlGroup_';
const controlGroupId = 'dashboard_control_group';
export const createInject = (
persistableStateService: EmbeddablePersistableStateService
@ -89,11 +90,12 @@ export const createInject = (
{
...workingState.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: controlGroupId,
},
controlGroupReferences
);
workingState.controlGroupInput =
injectedControlGroupState as DashboardContainerControlGroupInput;
injectedControlGroupState as unknown as DashboardContainerControlGroupInput;
}
return workingState as EmbeddableStateWithType;
@ -155,9 +157,10 @@ export const createExtract = (
persistableStateService.extract({
...workingState.controlGroupInput,
type: CONTROL_GROUP_TYPE,
id: controlGroupId,
});
workingState.controlGroupInput =
extractedControlGroupState as DashboardContainerControlGroupInput;
extractedControlGroupState as unknown as DashboardContainerControlGroupInput;
const prefixedControlGroupReferences = controlGroupReferences.map((reference) => ({
...reference,
name: `${controlGroupReferencePrefix}${reference.name}`,

View file

@ -7,57 +7,68 @@
*/
import { SerializableRecord } from '@kbn/utility-types';
import { ControlGroupInput } from '../../../controls/common';
import { ControlStyle } from '../../../controls/common/types';
import { ControlGroupInput, getDefaultControlGroupInput } from '../../../controls/common';
import { RawControlGroupAttributes } from '../types';
export const getDefaultDashboardControlGroupInput = getDefaultControlGroupInput;
export const controlGroupInputToRawAttributes = (
controlGroupInput: Omit<ControlGroupInput, 'id'>
): Omit<RawControlGroupAttributes, 'id'> => {
): RawControlGroupAttributes => {
return {
controlStyle: controlGroupInput.controlStyle,
chainingSystem: controlGroupInput.chainingSystem,
panelsJSON: JSON.stringify(controlGroupInput.panels),
ignoreParentSettingsJSON: JSON.stringify(controlGroupInput.ignoreParentSettings),
};
};
export const getDefaultDashboardControlGroupInput = () => ({
controlStyle: 'oneLine' as ControlGroupInput['controlStyle'],
panels: {},
});
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
if (!jsonString && typeof jsonString !== 'string') return;
try {
return JSON.parse(jsonString) as OutType;
} catch {
return;
}
};
export const rawAttributesToControlGroupInput = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
rawControlGroupAttributes: RawControlGroupAttributes
): Omit<ControlGroupInput, 'id'> | undefined => {
const defaultControlGroupInput = getDefaultDashboardControlGroupInput();
const defaultControlGroupInput = getDefaultControlGroupInput();
const { chainingSystem, controlStyle, ignoreParentSettingsJSON, panelsJSON } =
rawControlGroupAttributes;
const panels = safeJSONParse<ControlGroupInput['panels']>(panelsJSON);
const ignoreParentSettings =
safeJSONParse<ControlGroupInput['ignoreParentSettings']>(ignoreParentSettingsJSON);
return {
controlStyle: rawControlGroupAttributes?.controlStyle ?? defaultControlGroupInput.controlStyle,
panels:
rawControlGroupAttributes?.panelsJSON &&
typeof rawControlGroupAttributes?.panelsJSON === 'string'
? JSON.parse(rawControlGroupAttributes?.panelsJSON)
: defaultControlGroupInput.panels,
...defaultControlGroupInput,
...(chainingSystem ? { chainingSystem } : {}),
...(controlStyle ? { controlStyle } : {}),
...(ignoreParentSettings ? { ignoreParentSettings } : {}),
...(panels ? { panels } : {}),
};
};
export const rawAttributesToSerializable = (
rawControlGroupAttributes: Omit<RawControlGroupAttributes, 'id'>
): SerializableRecord => {
const defaultControlGroupInput = getDefaultDashboardControlGroupInput();
const defaultControlGroupInput = getDefaultControlGroupInput();
return {
chainingSystem: rawControlGroupAttributes?.chainingSystem,
controlStyle: rawControlGroupAttributes?.controlStyle ?? defaultControlGroupInput.controlStyle,
panels:
rawControlGroupAttributes?.panelsJSON &&
typeof rawControlGroupAttributes?.panelsJSON === 'string'
? (JSON.parse(rawControlGroupAttributes?.panelsJSON) as SerializableRecord)
: defaultControlGroupInput.panels,
ignoreParentSettings: safeJSONParse(rawControlGroupAttributes?.ignoreParentSettingsJSON) ?? {},
panels: safeJSONParse(rawControlGroupAttributes?.panelsJSON) ?? {},
};
};
export const serializableToRawAttributes = (
controlGroupInput: SerializableRecord
): Omit<RawControlGroupAttributes, 'id'> => {
serializable: SerializableRecord
): Omit<RawControlGroupAttributes, 'id' | 'type'> => {
return {
controlStyle: controlGroupInput.controlStyle as ControlStyle,
panelsJSON: JSON.stringify(controlGroupInput.panels),
controlStyle: serializable.controlStyle as RawControlGroupAttributes['controlStyle'],
chainingSystem: serializable.chainingSystem as RawControlGroupAttributes['chainingSystem'],
ignoreParentSettingsJSON: JSON.stringify(serializable.ignoreParentSettings),
panelsJSON: JSON.stringify(serializable.panels),
};
};

View file

@ -19,7 +19,6 @@ import {
convertSavedDashboardPanelToPanelState,
} from './embeddable/embeddable_saved_object_converters';
import { SavedDashboardPanel } from './types';
import { CONTROL_GROUP_TYPE } from '../../controls/common';
export interface ExtractDeps {
embeddablePersistableStateService: EmbeddablePersistableStateService;
@ -51,7 +50,6 @@ function dashboardAttributesToState(attributes: SavedObjectAttributes): {
if (controlGroupPanels && typeof controlGroupPanels === 'object') {
controlGroupInput = {
...rawControlGroupInput,
type: CONTROL_GROUP_TYPE,
panels: controlGroupPanels,
};
}

View file

@ -98,17 +98,19 @@ export type SavedDashboardPanel730ToLatest = Pick<
// Making this interface because so much of the Container type from embeddable is tied up in public
// Once that is all available from common, we should be able to move the dashboard_container type to our common as well
export interface DashboardContainerControlGroupInput extends EmbeddableStateWithType {
panels: ControlGroupInput['panels'];
controlStyle: ControlGroupInput['controlStyle'];
id: string;
}
// dashboard only persists part of the Control Group Input
export type DashboardContainerControlGroupInput = Pick<
ControlGroupInput,
'panels' | 'chainingSystem' | 'controlStyle' | 'ignoreParentSettings'
>;
export interface RawControlGroupAttributes {
controlStyle: ControlGroupInput['controlStyle'];
export type RawControlGroupAttributes = Omit<
DashboardContainerControlGroupInput,
'panels' | 'ignoreParentSettings'
> & {
ignoreParentSettingsJSON: string;
panelsJSON: string;
id: string;
}
};
export interface DashboardContainerStateWithType extends EmbeddableStateWithType {
panels: {

View file

@ -9,6 +9,7 @@
import { i18n } from '@kbn/i18n';
import { EmbeddablePersistableStateService } from 'src/plugins/embeddable/common';
import { identity, pickBy } from 'lodash';
import { DashboardContainerInput } from '../..';
import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants';
import type { DashboardContainer, DashboardContainerServices } from './dashboard_container';
@ -90,7 +91,7 @@ export class DashboardContainerFactoryDefinition
const controlGroup = await controlsGroupFactory?.create({
id: `control_group_${id ?? 'new_dashboard'}`,
...getDefaultDashboardControlGroupInput(),
...(controlGroupInput ?? {}),
...pickBy(controlGroupInput, identity), // undefined keys in initialInput should not overwrite defaults
timeRange,
viewMode,
filters,

View file

@ -11,7 +11,8 @@ import deepEqual from 'fast-deep-equal';
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query';
import { distinctUntilChanged, distinctUntilKeyChanged } from 'rxjs/operators';
import { DashboardContainer } from '..';
import { pick } from 'lodash';
import { DashboardContainer, DashboardContainerControlGroupInput } from '..';
import { DashboardState } from '../../types';
import { DashboardContainerInput, DashboardSavedObject } from '../..';
import { ControlGroupContainer, ControlGroupInput } from '../../../../controls/public';
@ -20,13 +21,6 @@ import {
getDefaultDashboardControlGroupInput,
rawAttributesToControlGroupInput,
} from '../../../common';
// only part of the control group input should be stored in dashboard state. The rest is passed down from the dashboard.
export interface DashboardControlGroupInput {
panels: ControlGroupInput['panels'];
controlStyle: ControlGroupInput['controlStyle'];
}
interface DiffChecks {
[key: string]: (a?: unknown, b?: unknown) => boolean;
}
@ -60,6 +54,8 @@ export const syncDashboardControlGroup = async ({
const controlGroupDiff: DiffChecks = {
panels: deepEqual,
controlStyle: deepEqual,
chainingSystem: deepEqual,
ignoreParentSettings: deepEqual,
};
subscriptions.add(
@ -71,9 +67,12 @@ export const syncDashboardControlGroup = async ({
)
)
.subscribe(() => {
const { panels, controlStyle } = controlGroup.getInput();
const { panels, controlStyle, chainingSystem, ignoreParentSettings } =
controlGroup.getInput();
if (!isControlGroupInputEqual()) {
dashboardContainer.updateInput({ controlGroupInput: { panels, controlStyle } });
dashboardContainer.updateInput({
controlGroupInput: { panels, controlStyle, chainingSystem, ignoreParentSettings },
});
}
})
);
@ -154,17 +153,17 @@ export const syncDashboardControlGroup = async ({
};
export const controlGroupInputIsEqual = (
a: DashboardControlGroupInput | undefined,
b: DashboardControlGroupInput | undefined
a: DashboardContainerControlGroupInput | undefined,
b: DashboardContainerControlGroupInput | undefined
) => {
const defaultInput = getDefaultDashboardControlGroupInput();
const inputA = {
panels: a?.panels ?? defaultInput.panels,
controlStyle: a?.controlStyle ?? defaultInput.controlStyle,
...defaultInput,
...pick(a, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
const inputB = {
panels: b?.panels ?? defaultInput.panels,
controlStyle: b?.controlStyle ?? defaultInput.controlStyle,
...defaultInput,
...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
};
if (deepEqual(inputA, inputB)) return true;
return false;
@ -175,7 +174,12 @@ export const serializeControlGroupToDashboardSavedObject = (
dashboardState: DashboardState
) => {
// only save to saved object if control group is not default
if (controlGroupInputIsEqual(dashboardState.controlGroupInput, {} as ControlGroupInput)) {
if (
controlGroupInputIsEqual(
dashboardState.controlGroupInput,
getDefaultDashboardControlGroupInput()
)
) {
dashboardSavedObject.controlGroupInput = undefined;
return;
}

View file

@ -10,8 +10,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Filter, Query, TimeRange } from '../../services/data';
import { ViewMode } from '../../services/embeddable';
import type { DashboardControlGroupInput } from '../lib/dashboard_control_group';
import { DashboardOptions, DashboardPanelMap, DashboardState } from '../../types';
import { DashboardContainerControlGroupInput } from '../embeddable';
export const dashboardStateSlice = createSlice({
name: 'dashboardState',
@ -44,7 +44,7 @@ export const dashboardStateSlice = createSlice({
},
setControlGroupState: (
state,
action: PayloadAction<DashboardControlGroupInput | undefined>
action: PayloadAction<DashboardContainerControlGroupInput | undefined>
) => {
state.controlGroupInput = action.payload;
},

View file

@ -29,7 +29,11 @@ import { UrlForwardingStart } from '../../url_forwarding/public';
import { UsageCollectionSetup } from './services/usage_collection';
import { NavigationPublicPluginStart } from './services/navigation';
import { Query, RefreshInterval, TimeRange } from './services/data';
import { DashboardPanelState, SavedDashboardPanel } from '../common/types';
import {
DashboardContainerControlGroupInput,
DashboardPanelState,
SavedDashboardPanel,
} from '../common/types';
import { SavedObjectsTaggingApi } from './services/saved_objects_tagging_oss';
import { DataPublicPluginStart, DataViewsContract } from './services/data';
import { ContainerInput, EmbeddableInput, ViewMode } from './services/embeddable';
@ -40,7 +44,6 @@ import type { DashboardContainer, DashboardSavedObject } from '.';
import { VisualizationsStart } from '../../visualizations/public';
import { DashboardAppLocatorParams } from './locator';
import { SpacesPluginStart } from './services/spaces';
import type { DashboardControlGroupInput } from './application/lib/dashboard_control_group';
export type { SavedDashboardPanel };
@ -71,7 +74,7 @@ export interface DashboardState {
panels: DashboardPanelMap;
timeRange?: TimeRange;
controlGroupInput?: DashboardControlGroupInput;
controlGroupInput?: DashboardContainerControlGroupInput;
}
/**
@ -81,7 +84,7 @@ export type RawDashboardState = Omit<DashboardState, 'panels'> & { panels: Saved
export interface DashboardContainerInput extends ContainerInput {
dashboardCapabilities?: DashboardAppCapabilities;
controlGroupInput?: DashboardControlGroupInput;
controlGroupInput?: DashboardContainerControlGroupInput;
refreshConfig?: RefreshInterval;
isEmbeddedExternally?: boolean;
isFullScreenMode: boolean;

View file

@ -55,7 +55,9 @@ export const createDashboardSavedObjectType = ({
controlGroupInput: {
properties: {
controlStyle: { type: 'keyword', index: false, doc_values: false },
chainingSystem: { type: 'keyword', index: false, doc_values: false },
panelsJSON: { type: 'text', index: false },
ignoreParentSettingsJSON: { type: 'text', index: false },
},
},
timeFrom: { type: 'keyword', index: false, doc_values: false },

View file

@ -8,7 +8,7 @@
import { createStubDataView } from 'src/plugins/data_views/common/mocks';
import type { DataViewsContract } from 'src/plugins/data_views/common';
import type { DatatableColumn } from 'src/plugins/expressions/common';
import type { Datatable, DatatableColumn } from 'src/plugins/expressions/common';
import { FieldFormat } from 'src/plugins/field_formats/common';
import { fieldFormatsMock } from 'src/plugins/field_formats/common/mocks';
import type { AggsCommonStart } from '../search';
@ -106,6 +106,16 @@ describe('DatatableUtilitiesService', () => {
});
});
describe('getTotalCount', () => {
it('should return a total hits count', () => {
const table = {
meta: { statistics: { totalCount: 100 } },
} as unknown as Datatable;
expect(datatableUtilitiesService.getTotalCount(table)).toBe(100);
});
});
describe('setFieldFormat', () => {
it('should set new field format', () => {
const column = { meta: {} } as DatatableColumn;

View file

@ -7,7 +7,7 @@
*/
import type { DataView, DataViewsContract, DataViewField } from 'src/plugins/data_views/common';
import type { DatatableColumn } from 'src/plugins/expressions/common';
import type { Datatable, DatatableColumn } from 'src/plugins/expressions/common';
import type { FieldFormatsStartCommon, FieldFormat } from 'src/plugins/field_formats/common';
import type { AggsCommonStart, AggConfig, CreateAggConfigParams, IAggType } from '../search';
@ -77,6 +77,10 @@ export class DatatableUtilitiesService {
return params?.interval;
}
getTotalCount(table: Datatable): number | undefined {
return table.meta?.statistics?.totalCount;
}
isFilterable(column: DatatableColumn): boolean {
if (column.meta.source !== 'esaggs') {
return false;

View file

@ -24,6 +24,9 @@ Object {
],
"meta": Object {
"source": "*",
"statistics": Object {
"totalCount": undefined,
},
"type": "eql",
},
"rows": Array [
@ -145,6 +148,9 @@ Object {
],
"meta": Object {
"source": "*",
"statistics": Object {
"totalCount": undefined,
},
"type": "eql",
},
"rows": Array [

View file

@ -42,6 +42,9 @@ Object {
],
"meta": Object {
"source": "*",
"statistics": Object {
"totalCount": 1977,
},
"type": "esdsl",
},
"rows": Array [
@ -86,6 +89,9 @@ Object {
],
"meta": Object {
"source": "*",
"statistics": Object {
"totalCount": 1977,
},
"type": "esdsl",
},
"rows": Array [
@ -172,6 +178,9 @@ Object {
],
"meta": Object {
"source": "*",
"statistics": Object {
"totalCount": 1977,
},
"type": "esdsl",
},
"rows": Array [

View file

@ -43,6 +43,22 @@ describe('eqlRawResponse', () => {
const result = eqlRawResponse.to!.datatable(response, {});
expect(result).toMatchSnapshot();
});
test('extracts total hits number', () => {
const response: EqlRawResponse = {
type: 'eql_raw_response',
body: {
hits: {
events: [],
total: {
value: 2,
},
},
},
};
const result = eqlRawResponse.to!.datatable(response, {});
expect(result).toHaveProperty('meta.statistics.totalCount', 2);
});
});
describe('converts sequences to table', () => {

View file

@ -125,6 +125,9 @@ export const eqlRawResponse: EqlRawResponseExpressionTypeDefinition = {
meta: {
type: 'eql',
source: '*',
statistics: {
totalCount: (context.body as EqlSearchResponse<unknown>).hits.total?.value,
},
},
columns,
rows,

View file

@ -82,6 +82,12 @@ export const esRawResponse: EsRawResponseExpressionTypeDefinition = {
meta: {
type: 'esdsl',
source: '*',
statistics: {
totalCount:
typeof context.body.hits.total === 'number'
? context.body.hits.total
: context.body.hits.total?.value,
},
},
columns,
rows,

View file

@ -52,6 +52,10 @@ describe('tabifyAggResponse Integration', () => {
expect(resp.rows[0]).toEqual({ 'col-0-1': 1000 });
expect(resp.columns[0]).toHaveProperty('name', aggConfigs.aggs[0].makeLabel());
expect(resp).toHaveProperty('meta.type', 'esaggs');
expect(resp).toHaveProperty('meta.source', '1234');
expect(resp).toHaveProperty('meta.statistics.totalCount', 1000);
});
describe('scaleMetricValues performance check', () => {

View file

@ -7,6 +7,7 @@
*/
import { get } from 'lodash';
import type { Datatable } from 'src/plugins/expressions';
import { TabbedAggResponseWriter } from './response_writer';
import { TabifyBuckets } from './buckets';
import type { TabbedResponseWriterOptions } from './types';
@ -20,7 +21,7 @@ export function tabifyAggResponse(
aggConfigs: IAggConfigs,
esResponse: Record<string, any>,
respOpts?: Partial<TabbedResponseWriterOptions>
) {
): Datatable {
/**
* read an aggregation from a bucket, which *might* be found at key (if
* the response came in object form), and will recurse down the aggregation
@ -152,5 +153,14 @@ export function tabifyAggResponse(
collectBucket(aggConfigs, write, topLevelBucket, '', 1);
return write.response();
return {
...write.response(),
meta: {
type: 'esaggs',
source: aggConfigs.indexPattern.id,
statistics: {
totalCount: esResponse.hits?.total,
},
},
};
}

View file

@ -36,6 +36,7 @@ export type {
EmbeddablePackageState,
EmbeddableRendererProps,
EmbeddableContainerContext,
EmbeddableContainerSettings,
} from './lib';
export {
ACTION_ADD_PANEL,

View file

@ -6,6 +6,12 @@
* Side Public License, v 1.
*/
export type { IContainer, PanelState, ContainerInput, ContainerOutput } from './i_container';
export type {
IContainer,
PanelState,
ContainerInput,
ContainerOutput,
EmbeddableContainerSettings,
} from './i_container';
export { Container } from './container';
export * from './embeddable_child_panel';

View file

@ -0,0 +1,3 @@
# Event Annotation service
The Event Annotation service contains expressions for event annotations

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { i18n } from '@kbn/i18n';
import type { EventAnnotationOutput } from '../manual_event_annotation/types';
export interface EventAnnotationGroupOutput {
type: 'event_annotation_group';
annotations: EventAnnotationOutput[];
}
export interface EventAnnotationGroupArgs {
annotations: EventAnnotationOutput[];
}
export function eventAnnotationGroup(): ExpressionFunctionDefinition<
'event_annotation_group',
null,
EventAnnotationGroupArgs,
EventAnnotationGroupOutput
> {
return {
name: 'event_annotation_group',
aliases: [],
type: 'event_annotation_group',
inputTypes: ['null'],
help: i18n.translate('eventAnnotation.group.description', {
defaultMessage: 'Event annotation group',
}),
args: {
annotations: {
types: ['manual_event_annotation'],
help: i18n.translate('eventAnnotation.group.args.annotationConfigs', {
defaultMessage: 'Annotation configs',
}),
multi: true,
},
},
fn: (input, args) => {
return {
type: 'event_annotation_group',
annotations: args.annotations,
};
},
};
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { EventAnnotationArgs, EventAnnotationOutput } from './manual_event_annotation/types';
export { manualEventAnnotation } from './manual_event_annotation';
export { eventAnnotationGroup } from './event_annotation_group';
export type { EventAnnotationGroupArgs } from './event_annotation_group';
export type { EventAnnotationConfig } from './types';

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { i18n } from '@kbn/i18n';
import type { EventAnnotationArgs, EventAnnotationOutput } from './types';
export const manualEventAnnotation: ExpressionFunctionDefinition<
'manual_event_annotation',
null,
EventAnnotationArgs,
EventAnnotationOutput
> = {
name: 'manual_event_annotation',
aliases: [],
type: 'manual_event_annotation',
help: i18n.translate('eventAnnotation.manualAnnotation.description', {
defaultMessage: `Configure manual annotation`,
}),
inputTypes: ['null'],
args: {
time: {
types: ['string'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.time', {
defaultMessage: `Timestamp for annotation`,
}),
},
label: {
types: ['string'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.label', {
defaultMessage: `The name of the annotation`,
}),
},
color: {
types: ['string'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.color', {
defaultMessage: 'The color of the line',
}),
},
lineStyle: {
types: ['string'],
options: ['solid', 'dotted', 'dashed'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.lineStyle', {
defaultMessage: 'The style of the annotation line',
}),
},
lineWidth: {
types: ['number'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.lineWidth', {
defaultMessage: 'The width of the annotation line',
}),
},
icon: {
types: ['string'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.icon', {
defaultMessage: 'An optional icon used for annotation lines',
}),
},
textVisibility: {
types: ['boolean'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.textVisibility', {
defaultMessage: 'Visibility of the label on the annotation line',
}),
},
isHidden: {
types: ['boolean'],
help: i18n.translate('eventAnnotation.manualAnnotation.args.isHidden', {
defaultMessage: `Switch to hide annotation`,
}),
},
},
fn: function fn(input: unknown, args: EventAnnotationArgs) {
return {
type: 'manual_event_annotation',
...args,
};
},
};

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { StyleProps } from '../types';
export type EventAnnotationArgs = {
time: string;
} & StyleProps;
export type EventAnnotationOutput = EventAnnotationArgs & { type: 'manual_event_annotation' };

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type LineStyle = 'solid' | 'dashed' | 'dotted';
export type AnnotationType = 'manual';
export type KeyType = 'point_in_time';
export interface StyleProps {
label: string;
color?: string;
icon?: string;
lineWidth?: number;
lineStyle?: LineStyle;
textVisibility?: boolean;
isHidden?: boolean;
}
export type EventAnnotationConfig = {
id: string;
key: {
type: KeyType;
timestamp: string;
};
} & StyleProps;

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/event_annotation'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/src/plugins/event_annotation',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/event_annotation/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -0,0 +1,17 @@
{
"id": "eventAnnotation",
"version": "kibana",
"server": true,
"ui": true,
"description": "The Event Annotation service contains expressions for event annotations",
"extraPublicDirs": [
"common"
],
"requiredPlugins": [
"expressions"
],
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"
}
}

View file

@ -0,0 +1,3 @@
# Event Annotation service
The Event Annotation service contains expressions for event annotations

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { euiLightVars } from '@kbn/ui-theme';
export const defaultAnnotationColor = euiLightVars.euiColorAccent;

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventAnnotationServiceType } from './types';
export class EventAnnotationService {
private eventAnnotationService?: EventAnnotationServiceType;
public async getService() {
if (!this.eventAnnotationService) {
const { getEventAnnotationService } = await import('./service');
this.eventAnnotationService = getEventAnnotationService();
}
return this.eventAnnotationService;
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventAnnotationServiceType } from './types';
import { defaultAnnotationColor } from './helpers';
export function hasIcon(icon: string | undefined): icon is string {
return icon != null && icon !== 'empty';
}
export function getEventAnnotationService(): EventAnnotationServiceType {
return {
toExpression: ({
label,
isHidden,
color,
lineStyle,
lineWidth,
icon,
textVisibility,
time,
}) => {
return {
type: 'expression',
chain: [
{
type: 'function',
function: 'manual_event_annotation',
arguments: {
time: [time],
label: [label],
color: [color || defaultAnnotationColor],
lineWidth: [lineWidth || 1],
lineStyle: [lineStyle || 'solid'],
icon: hasIcon(icon) ? [icon] : ['triangle'],
textVisibility: [textVisibility || false],
isHidden: [Boolean(isHidden)],
},
},
],
};
},
};
}

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ExpressionAstExpression } from '../../../expressions/common/ast';
import { EventAnnotationArgs } from '../../common';
export interface EventAnnotationServiceType {
toExpression: (props: EventAnnotationArgs) => ExpressionAstExpression;
}

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// TODO: https://github.com/elastic/kibana/issues/110891
/* eslint-disable @kbn/eslint/no_export_all */
import { EventAnnotationPlugin } from './plugin';
export const plugin = () => new EventAnnotationPlugin();
export type { EventAnnotationPluginSetup, EventAnnotationPluginStart } from './plugin';
export * from './event_annotation_service/types';
export { EventAnnotationService } from './event_annotation_service';
export { defaultAnnotationColor } from './event_annotation_service/helpers';

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getEventAnnotationService } from './event_annotation_service/service';
// not really mocking but avoiding async loading
export const eventAnnotationServiceMock = getEventAnnotationService();

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Plugin, CoreSetup } from 'kibana/public';
import { ExpressionsSetup } from '../../expressions/public';
import { manualEventAnnotation, eventAnnotationGroup } from '../common';
import { EventAnnotationService } from './event_annotation_service';
interface SetupDependencies {
expressions: ExpressionsSetup;
}
/** @public */
export type EventAnnotationPluginSetup = EventAnnotationService;
/** @public */
export type EventAnnotationPluginStart = EventAnnotationService;
/** @public */
export class EventAnnotationPlugin
implements Plugin<EventAnnotationPluginSetup, EventAnnotationPluginStart>
{
private readonly eventAnnotationService = new EventAnnotationService();
public setup(core: CoreSetup, dependencies: SetupDependencies): EventAnnotationPluginSetup {
dependencies.expressions.registerFunction(manualEventAnnotation);
dependencies.expressions.registerFunction(eventAnnotationGroup);
return this.eventAnnotationService;
}
public start(): EventAnnotationPluginStart {
return this.eventAnnotationService;
}
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { EventAnnotationServerPlugin } from './plugin';
export const plugin = () => new EventAnnotationServerPlugin();

View file

@ -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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreSetup, Plugin } from 'kibana/server';
import { manualEventAnnotation, eventAnnotationGroup } from '../common';
import { ExpressionsServerSetup } from '../../expressions/server';
interface SetupDependencies {
expressions: ExpressionsServerSetup;
}
export class EventAnnotationServerPlugin implements Plugin<object, object> {
public setup(core: CoreSetup, dependencies: SetupDependencies) {
dependencies.expressions.registerFunction(manualEventAnnotation);
dependencies.expressions.registerFunction(eventAnnotationGroup);
return {};
}
public start() {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,22 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*"
],
"references": [
{
"path": "../../core/tsconfig.json"
},
{
"path": "../expressions/tsconfig.json"
}
]
}

View file

@ -95,7 +95,7 @@ export const mapColumn: ExpressionFunctionDefinition<
input.rows.map((row) =>
args
.expression({
type: 'datatable',
...input,
columns: [...input.columns],
rows: [row],
})
@ -129,9 +129,9 @@ export const mapColumn: ExpressionFunctionDefinition<
};
return {
...input,
columns,
rows,
type: 'datatable',
};
})
);

View file

@ -79,7 +79,7 @@ export const mathColumn: ExpressionFunctionDefinition<
input.rows.map(async (row) => {
const result = await math.fn(
{
type: 'datatable',
...input,
columns: input.columns,
rows: [row],
},
@ -128,7 +128,7 @@ export const mathColumn: ExpressionFunctionDefinition<
columns.push(newColumn);
return {
type: 'datatable',
...input,
columns,
rows: newRows,
} as Datatable;

View file

@ -91,12 +91,45 @@ export interface DatatableColumn {
meta: DatatableColumnMeta;
}
/**
* Metadata with statistics about the `Datatable` source.
*/
export interface DatatableMetaStatistics {
/**
* Total hits number returned for the request generated the `Datatable`.
*/
totalCount?: number;
}
/**
* The `Datatable` meta information.
*/
export interface DatatableMeta {
/**
* Statistics about the `Datatable` source.
*/
statistics?: DatatableMetaStatistics;
/**
* The `Datatable` type (e.g. `essql`, `eql`, `esdsl`, etc.).
*/
type?: string;
/**
* The `Datatable` data source.
*/
source?: string;
[key: string]: unknown;
}
/**
* A `Datatable` in Canvas is a unique structure that represents tabulated data.
*/
export interface Datatable {
type: typeof name;
columns: DatatableColumn[];
meta?: DatatableMeta;
rows: DatatableRow[];
}

View file

@ -1,566 +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
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const security = getService('security');
const queryBar = getService('queryBar');
const pieChart = getService('pieChart');
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const dashboardAddPanel = getService('dashboardAddPanel');
const find = getService('find');
const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Dashboard controls integration', () => {
const clearAllControls = async () => {
const controlIds = await dashboardControls.getAllControlIds();
for (const controlId of controlIds) {
await dashboardControls.removeExistingControl(controlId);
}
};
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/dashboard/current/kibana'
);
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
await common.navigateToApp('dashboard');
await dashboardControls.enableControlsLab();
await common.navigateToApp('dashboard');
await dashboard.preserveCrossAppState();
});
after(async () => {
await security.testUser.restoreDefaults();
await kibanaServer.savedObjects.cleanStandardList();
});
describe('Controls callout visibility', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboard.saveDashboard('Test Controls Callout');
});
describe('does not show the empty control callout on an empty dashboard', async () => {
it('in view mode', async () => {
await dashboard.clickCancelOutOfEditMode();
await testSubjects.missingOrFail('controls-empty');
});
it('in edit mode', async () => {
await dashboard.switchToEditMode();
await testSubjects.missingOrFail('controls-empty');
});
});
it('show the empty control callout on a dashboard with panels', async () => {
await dashboard.switchToEditMode();
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await testSubjects.existOrFail('controls-empty');
});
it('adding control hides the empty control callout', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await testSubjects.missingOrFail('controls-empty');
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
describe('Control group settings', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await dashboard.saveDashboard('Test Control Group Settings');
});
it('adjust layout of controls', async () => {
await dashboard.switchToEditMode();
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.adjustControlsLayout('twoLine');
const controlGroupWrapper = await testSubjects.find('controls-group-wrapper');
expect(await controlGroupWrapper.elementHasClass('controlsWrapper--twoLine')).to.be(true);
});
describe('apply new default size', async () => {
it('to new controls only', async () => {
await dashboardControls.updateControlsSize('medium');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
});
const controlIds = await dashboardControls.getAllControlIds();
const firstControl = await find.byXPath(`//div[@data-control-id="${controlIds[0]}"]`);
expect(await firstControl.elementHasClass('controlFrameWrapper--medium')).to.be(false);
const secondControl = await find.byXPath(`//div[@data-control-id="${controlIds[1]}"]`);
expect(await secondControl.elementHasClass('controlFrameWrapper--medium')).to.be(true);
});
it('to all existing controls', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
width: 'large',
});
await dashboardControls.updateControlsSize('small', true);
const controlIds = await dashboardControls.getAllControlIds();
for (const id of controlIds) {
const control = await find.byXPath(`//div[@data-control-id="${id}"]`);
expect(await control.elementHasClass('controlFrameWrapper--small')).to.be(true);
}
});
});
describe('flyout only show settings that are relevant', async () => {
before(async () => {
await dashboard.switchToEditMode();
});
it('when no controls', async () => {
await dashboardControls.deleteAllControls();
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.missingOrFail('delete-all-controls-button');
await testSubjects.missingOrFail('set-all-control-sizes-checkbox');
});
it('when at least one control', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.existOrFail('delete-all-controls-button');
await testSubjects.existOrFail('set-all-control-sizes-checkbox', { allowHidden: true });
});
afterEach(async () => {
await testSubjects.click('euiFlyoutCloseButton');
});
after(async () => {
await dashboardControls.deleteAllControls();
});
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
describe('Options List Control creation and editing experience', async () => {
it('can add a new options list control from a blank state', async () => {
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' });
expect(await dashboardControls.getControlsCount()).to.be(1);
});
it('can add a second options list control with a non-default data view', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
expect(await dashboardControls.getControlsCount()).to.be(2);
// data views should be properly propagated from the control group to the dashboard
expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
});
it('renames an existing control', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'wow! Animal sounds?';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
});
it('can change the data view and field of an existing options list', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
await dashboardControls.optionsListEditorSetDataView('animals-*');
await dashboardControls.optionsListEditorSetfield('animal.keyword');
await dashboardControls.controlEditorSave();
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await retry.try(async () => {
await testSubjects.click('addFilter');
const indexPatternSelectExists = await testSubjects.exists('filterIndexPatternsSelect');
await filterBar.ensureFieldEditorModalIsClosed();
expect(indexPatternSelectExists).to.be(false);
});
});
it('deletes an existing control', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.removeExistingControl(firstId);
expect(await dashboardControls.getControlsCount()).to.be(1);
});
after(async () => {
await clearAllControls();
});
});
describe('Interactions between options list and dashboard', async () => {
let controlId: string;
before(async () => {
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sounds',
});
controlId = (await dashboardControls.getAllControlIds())[0];
});
describe('Apply dashboard query and filters to controls', async () => {
it('Applies dashboard query to options list control', async () => {
await queryBar.setQuery('isDog : true ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
]);
});
await queryBar.setQuery('');
await queryBar.submitQuery();
});
it('Applies dashboard filters to options list control', async () => {
await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(3);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'bow ow ow',
]);
});
});
it('Does not apply disabled dashboard filters to options list control', async () => {
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
});
await filterBar.toggleFilterEnabled('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
});
it('Negated filters apply to options control', async () => {
await filterBar.toggleFilterNegated('sound.keyword');
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'grrr',
'meow',
'growl',
'grr',
]);
});
});
after(async () => {
await filterBar.removeAllFilters();
});
});
describe('Selections made in control apply to dashboard', async () => {
it('Shows available options in options list', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can search options list for available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSearchForOption('meo');
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'meow',
]);
});
await dashboardControls.optionsListPopoverClearSearch();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can select multiple available options', async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('hiss');
await dashboardControls.optionsListPopoverSelectOption('grr');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Selected options appear in control', async () => {
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
it('Applies options list control options to dashboard', async () => {
await retry.try(async () => {
expect(await pieChart.getPieSliceCount()).to.be(2);
});
});
it('Applies options list control options to dashboard by default on open', async () => {
await dashboard.gotoDashboardLandingPage();
await header.waitUntilLoadingHasFinished();
await dashboard.clickUnsavedChangesContinueEditing('New Dashboard');
await header.waitUntilLoadingHasFinished();
expect(await pieChart.getPieSliceCount()).to.be(2);
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
expect(selectionString).to.be('hiss, grr');
});
after(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
});
describe('Options List dashboard validation', async () => {
before(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListPopoverSelectOption('bark');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
});
it('Can mark selections invalid with Query', async () => {
await queryBar.setQuery('isDog : false ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(4);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'meow',
'growl',
'grr',
'Ignored selection',
'bark',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
it('can make invalid selections valid again if the parent filter changes', async () => {
await queryBar.setQuery('');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'ruff',
'bark',
'grrr',
'meow',
'growl',
'grr',
'bow ow ow',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
expect(await pieChart.getPieSliceCount()).to.be(2);
});
it('Can mark multiple selections invalid with Filter', async () => {
await filterBar.addFilter('sound.keyword', 'is', ['hiss']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'hiss',
'Ignored selections',
'meow',
'bark',
]);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
// only valid selections are applied as filters.
expect(await pieChart.getPieSliceCount()).to.be(1);
});
});
after(async () => {
await filterBar.removeAllFilters();
await clearAllControls();
});
});
describe('Control group hierarchical chaining', async () => {
let controlIds: string[];
const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(
expectation
);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
};
before(async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
title: 'Animal',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
title: 'Animal Name',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sound',
});
controlIds = await dashboardControls.getAllControlIds();
});
it('Shows all available options in first Options List control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(2);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Selecting an option in the first Options List will filter the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']);
await ensureAvailableOptionsEql(controlIds[2], ['hiss', 'meow', 'growl', 'grr']);
});
it('Selecting an option in the second Options List will filter the third control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[1]);
await dashboardControls.optionsListPopoverSelectOption('sylvester');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]);
await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']);
});
it('Can select an option in the third Options List', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[2]);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
});
it('Selecting a conflicting option in the first control will validate the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Fee Fee',
'Rover',
'Ignored selection',
'sylvester',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
'Ignored selection',
'meow',
]);
});
});
});
}

View file

@ -72,7 +72,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./full_screen_mode'));
loadTestFile(require.resolve('./dashboard_filter_bar'));
loadTestFile(require.resolve('./dashboard_filtering'));
loadTestFile(require.resolve('./dashboard_controls_integration'));
loadTestFile(require.resolve('./panel_expand_toggle'));
loadTestFile(require.resolve('./dashboard_grid'));
loadTestFile(require.resolve('./view_edit'));

View file

@ -0,0 +1,146 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const { dashboardControls, common, dashboard, timePicker } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
]);
describe('Dashboard control group hierarchical chaining', () => {
let controlIds: string[];
const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => {
await dashboardControls.optionsListOpenPopover(controlId);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(expectation);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
};
before(async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
// populate an initial set of controls and get their ids.
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
title: 'Animal',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
title: 'Animal Name',
});
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sound',
});
controlIds = await dashboardControls.getAllControlIds();
});
it('Shows all available options in first Options List control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(2);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Selecting an option in the first Options List will filter the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']);
await ensureAvailableOptionsEql(controlIds[2], ['hiss', 'meow', 'growl', 'grr']);
});
it('Selecting an option in the second Options List will filter the third control', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[1]);
await dashboardControls.optionsListPopoverSelectOption('sylvester');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]);
await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']);
});
it('Can select an option in the third Options List', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[2]);
await dashboardControls.optionsListPopoverSelectOption('meow');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
});
it('Selecting a conflicting option in the first control will validate the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Fee Fee',
'Rover',
'Ignored selection',
'sylvester',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
'Ignored selection',
'meow',
]);
});
describe('Hierarchical chaining off', async () => {
before(async () => {
await dashboardControls.updateChainingSystem('NONE');
});
it('Selecting an option in the first Options List will not filter the second or third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('cat');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await ensureAvailableOptionsEql(controlIds[1], [
'Fluffy',
'Tiger',
'sylvester',
'Fee Fee',
'Rover',
]);
await ensureAvailableOptionsEql(controlIds[2], [
'hiss',
'ruff',
'bark',
'grrr',
'meow',
'growl',
'grr',
'bow ow ow',
]);
});
});
});
}

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const { dashboardControls, common, dashboard } = getPageObjects([
'dashboardControls',
'dashboard',
'common',
]);
describe('Dashboard control group settings', () => {
before(async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await dashboard.saveDashboard('Test Control Group Settings');
});
it('adjust layout of controls', async () => {
await dashboard.switchToEditMode();
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.adjustControlsLayout('twoLine');
const controlGroupWrapper = await testSubjects.find('controls-group-wrapper');
expect(await controlGroupWrapper.elementHasClass('controlsWrapper--twoLine')).to.be(true);
});
describe('apply new default size', async () => {
it('to new controls only', async () => {
await dashboardControls.updateControlsSize('medium');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
});
const controlIds = await dashboardControls.getAllControlIds();
const firstControl = await find.byXPath(`//div[@data-control-id="${controlIds[0]}"]`);
expect(await firstControl.elementHasClass('controlFrameWrapper--medium')).to.be(false);
const secondControl = await find.byXPath(`//div[@data-control-id="${controlIds[1]}"]`);
expect(await secondControl.elementHasClass('controlFrameWrapper--medium')).to.be(true);
});
it('to all existing controls', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'animal.keyword',
width: 'large',
});
await dashboardControls.updateControlsSize('small', true);
const controlIds = await dashboardControls.getAllControlIds();
for (const id of controlIds) {
const control = await find.byXPath(`//div[@data-control-id="${id}"]`);
expect(await control.elementHasClass('controlFrameWrapper--small')).to.be(true);
}
});
});
describe('flyout only show settings that are relevant', async () => {
before(async () => {
await dashboard.switchToEditMode();
});
it('when no controls', async () => {
await dashboardControls.deleteAllControls();
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.missingOrFail('delete-all-controls-button');
await testSubjects.missingOrFail('set-all-control-sizes-checkbox');
});
it('when at least one control', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.existOrFail('delete-all-controls-button');
await testSubjects.existOrFail('set-all-control-sizes-checkbox', { allowHidden: true });
});
afterEach(async () => {
await testSubjects.click('euiFlyoutCloseButton');
});
after(async () => {
await dashboardControls.deleteAllControls();
});
});
});
}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const dashboardAddPanel = getService('dashboardAddPanel');
const { dashboardControls, timePicker, dashboard } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Controls callout', () => {
describe('callout visibility', async () => {
before(async () => {
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboard.saveDashboard('Test Controls Callout');
});
describe('does not show the empty control callout on an empty dashboard', async () => {
it('in view mode', async () => {
await dashboard.clickCancelOutOfEditMode();
await testSubjects.missingOrFail('controls-empty');
});
it('in edit mode', async () => {
await dashboard.switchToEditMode();
await testSubjects.missingOrFail('controls-empty');
});
});
it('show the empty control callout on a dashboard with panels', async () => {
await dashboard.switchToEditMode();
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await testSubjects.existOrFail('controls-empty');
});
it('adding control hides the empty control callout', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await testSubjects.missingOrFail('controls-empty');
});
after(async () => {
await dashboard.clickCancelOutOfEditMode();
await dashboard.gotoDashboardLandingPage();
});
});
});
}

Some files were not shown because too many files have changed in this diff Show more