mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge branch 'main' into screenshotting/pdf-integration-tests
This commit is contained in:
commit
d580993d28
739 changed files with 20250 additions and 6308 deletions
17
.i18nrc.json
17
.i18nrc.json
|
@ -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": []
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@ Every team should be collecting telemetry metrics on it’s 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`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -124,3 +124,4 @@ pageLoadAssetSize:
|
|||
sessionView: 77750
|
||||
cloudSecurityPosture: 19109
|
||||
visTypeGauge: 24113
|
||||
eventAnnotation: 19334
|
||||
|
|
8
packages/kbn-pm/dist/index.js
vendored
8
packages/kbn-pm/dist/index.js
vendored
|
@ -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 */
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> => {
|
||||
|
|
|
@ -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*)'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 ')})`;
|
||||
}
|
||||
};
|
|
@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 ');
|
||||
};
|
||||
|
|
|
@ -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*)'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 ')})`;
|
||||
}
|
||||
};
|
|
@ -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*)'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 ')})`;
|
||||
}
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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']),
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as Tasks from './tasks';
|
|||
|
||||
export interface BuildOptions {
|
||||
isRelease: boolean;
|
||||
dockerCrossCompile: boolean;
|
||||
dockerPush: boolean;
|
||||
dockerTagQualifier: string | null;
|
||||
downloadFreshNode: boolean;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -32,6 +32,7 @@ const config = new Config(
|
|||
buildSha: 'abcd1234',
|
||||
buildVersion: '8.0.0',
|
||||
},
|
||||
false,
|
||||
'',
|
||||
false,
|
||||
true
|
||||
|
|
|
@ -29,6 +29,7 @@ const setup = async ({ targetAllPlatforms = true }: { targetAllPlatforms?: boole
|
|||
return await Config.create({
|
||||
isRelease: true,
|
||||
targetAllPlatforms,
|
||||
dockerCrossCompile: false,
|
||||
dockerPush: false,
|
||||
dockerTagQualifier: '',
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -50,6 +50,7 @@ const setup = async () => {
|
|||
isRelease: true,
|
||||
targetAllPlatforms: true,
|
||||
versionQualifier: '-SNAPSHOT',
|
||||
dockerCrossCompile: false,
|
||||
dockerPush: false,
|
||||
dockerTagQualifier: '',
|
||||
});
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -39,6 +39,7 @@ async function setup({ failOnUrl }: { failOnUrl?: string } = {}) {
|
|||
const config = await Config.create({
|
||||
isRelease: true,
|
||||
targetAllPlatforms: true,
|
||||
dockerCrossCompile: false,
|
||||
dockerPush: false,
|
||||
dockerTagQualifier: '',
|
||||
});
|
||||
|
|
|
@ -43,6 +43,7 @@ async function setup() {
|
|||
const config = await Config.create({
|
||||
isRelease: true,
|
||||
targetAllPlatforms: true,
|
||||
dockerCrossCompile: false,
|
||||
dockerPush: false,
|
||||
dockerTagQualifier: '',
|
||||
});
|
||||
|
|
|
@ -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: '',
|
||||
});
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: () =>
|
||||
|
|
|
@ -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();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -125,6 +125,9 @@ export const eqlRawResponse: EqlRawResponseExpressionTypeDefinition = {
|
|||
meta: {
|
||||
type: 'eql',
|
||||
source: '*',
|
||||
statistics: {
|
||||
totalCount: (context.body as EqlSearchResponse<unknown>).hits.total?.value,
|
||||
},
|
||||
},
|
||||
columns,
|
||||
rows,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export type {
|
|||
EmbeddablePackageState,
|
||||
EmbeddableRendererProps,
|
||||
EmbeddableContainerContext,
|
||||
EmbeddableContainerSettings,
|
||||
} from './lib';
|
||||
export {
|
||||
ACTION_ADD_PANEL,
|
||||
|
|
|
@ -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';
|
||||
|
|
3
src/plugins/event_annotation/README.md
Normal file
3
src/plugins/event_annotation/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Event Annotation service
|
||||
|
||||
The Event Annotation service contains expressions for event annotations
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
13
src/plugins/event_annotation/common/index.ts
Normal file
13
src/plugins/event_annotation/common/index.ts
Normal 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';
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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' };
|
29
src/plugins/event_annotation/common/types.ts
Normal file
29
src/plugins/event_annotation/common/types.ts
Normal 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;
|
18
src/plugins/event_annotation/jest.config.js
Normal file
18
src/plugins/event_annotation/jest.config.js
Normal 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}',
|
||||
],
|
||||
};
|
17
src/plugins/event_annotation/kibana.json
Normal file
17
src/plugins/event_annotation/kibana.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Event Annotation service
|
||||
|
||||
The Event Annotation service contains expressions for event annotations
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
17
src/plugins/event_annotation/public/index.ts
Normal file
17
src/plugins/event_annotation/public/index.ts
Normal 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';
|
12
src/plugins/event_annotation/public/mocks.ts
Normal file
12
src/plugins/event_annotation/public/mocks.ts
Normal 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();
|
39
src/plugins/event_annotation/public/plugin.ts
Normal file
39
src/plugins/event_annotation/public/plugin.ts
Normal 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;
|
||||
}
|
||||
}
|
10
src/plugins/event_annotation/server/index.ts
Normal file
10
src/plugins/event_annotation/server/index.ts
Normal 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();
|
30
src/plugins/event_annotation/server/plugin.ts
Normal file
30
src/plugins/event_annotation/server/plugin.ts
Normal 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() {}
|
||||
}
|
22
src/plugins/event_annotation/tsconfig.json
Normal file
22
src/plugins/event_annotation/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue