diff --git a/.ci/Dockerfile b/.ci/Dockerfile deleted file mode 100644 index 201e17b93c116..0000000000000 --- a/.ci/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -ARG NODE_VERSION=10.21.0 - -FROM node:${NODE_VERSION} AS base - -RUN apt-get update && \ - apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ - libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ - libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ - libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ - libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \ - rm -rf /var/lib/apt/lists/* - -RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ - && apt-get update \ - && apt-get install -y rsync jq bsdtar google-chrome-stable \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -RUN LATEST_VAULT_RELEASE=$(curl -s https://api.github.com/repos/hashicorp/vault/tags | jq --raw-output .[0].name[1:]) \ - && curl -L https://releases.hashicorp.com/vault/${LATEST_VAULT_RELEASE}/vault_${LATEST_VAULT_RELEASE}_linux_amd64.zip -o vault.zip \ - && unzip vault.zip \ - && rm vault.zip \ - && chmod +x vault \ - && mv vault /usr/local/bin/vault - -RUN groupadd -r kibana && useradd -r -g kibana kibana && mkdir /home/kibana && chown kibana:kibana /home/kibana - -COPY ./bash_standard_lib.sh /usr/local/bin/bash_standard_lib.sh -RUN chmod +x /usr/local/bin/bash_standard_lib.sh - -COPY ./runbld /usr/local/bin/runbld -RUN chmod +x /usr/local/bin/runbld - -USER kibana diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml index 1bcb7e22a2648..67b5002c1c437 100644 --- a/.ci/runbld_no_junit.yml +++ b/.ci/runbld_no_junit.yml @@ -3,4 +3,4 @@ profiles: - ".*": # Match any job tests: - junit-filename-pattern: false + junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist diff --git a/.gitignore b/.gitignore index 25a8c369bb704..32377ec0f1ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,6 @@ npm-debug.log* .tern-project .nyc_output .ci/pipeline-library/build/ -.ci/runbld -.ci/bash_standard_lib.sh .gradle # apm plugin diff --git a/Jenkinsfile b/Jenkinsfile index 491a1e386deb1..f6f77ccae8427 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,7 +8,50 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) ciStats.trackBuild { catchError { retryable.enable() - kibanaPipeline.allCiTasks() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'), + // 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'), + 'xpack-securitySolutionCypress': { processNumber -> + whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { + kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber) + } + }, + + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) } } } diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getquerylog.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getquerylog.md deleted file mode 100644 index e933245e81623..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getquerylog.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [getQueryLog](./kibana-plugin-plugins-data-public.getquerylog.md) - -## getQueryLog() function - -Signature: - -```typescript -export declare function getQueryLog(uiSettings: IUiSettingsClient, storage: IStorageWrapper, appName: string, language: string): PersistedLog; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| uiSettings | IUiSettingsClient | | -| storage | IStorageWrapper | | -| appName | string | | -| language | string | | - -Returns: - -`PersistedLog` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 8bd38f9e02d11..7cb6ef64431bf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -38,7 +38,6 @@ | --- | --- | | [getDefaultQuery(language)](./kibana-plugin-plugins-data-public.getdefaultquery.md) | | | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | -| [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | | | [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | | | [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index a25f4a0c373b2..e139b326b7500 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 67c4eac67a9e6..6c8f7fbdb170b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -21,6 +21,7 @@ search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; + Ipv4Address: typeof Ipv4Address; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; isNumberType: (agg: import("./search").AggConfig) => boolean; isStringType: (agg: import("./search").AggConfig) => boolean; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md index a48f4920b3d26..e515c3513df6c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -8,32 +8,33 @@ ```typescript UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; } ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index 6020498fdcb6d..09563358100b3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -12,6 +12,7 @@ search: { dateHistogramInterval: typeof dateHistogramInterval; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; + Ipv4Address: typeof Ipv4Address; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parseEsInterval: typeof parseEsInterval; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md index 855cfd11d00ea..e419b64cd43aa 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -8,32 +8,33 @@ ```typescript UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; } ``` diff --git a/docs/uptime-guide/alerting.asciidoc b/docs/uptime-guide/alerting.asciidoc deleted file mode 100644 index bf9e7693fc7a5..0000000000000 --- a/docs/uptime-guide/alerting.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -[role="xpack"] -[[uptime-alerting]] - -=== Uptime alerting - -The Uptime app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] -feature. It provides a set of built-in actions and Uptime specific threshold alerts for you to use -and enables central management of all alerts from {kibana-ref}/management.html[Kibana Management]. - -[role="screenshot"] -image::images/create-alert.png[Create alert] - -[float] -==== Monitor status alerts - -To receive alerts when a monitor goes down, use the alerting menu at the top of the -overview page. Use a query in the alert flyout to determine which monitors to check -with your alert. If you already have a query in the overview page search bar it will -be carried over into this box. - -[role="screenshot"] -image::images/monitor-status-alert.png[Create monitor status alert flyout] - -[float] -==== TLS alerts - -Uptime also provides the ability to create an alert that will notify you when one or -more of your monitors have a TLS certificate that will expire within some threshold, -or when its age exceeds a limit. The values for these thresholds are configurable on -the <>. - -[role="screenshot"] -image::images/tls-alert.png[Create TLS alert flyout] diff --git a/docs/uptime-guide/app-overview.asciidoc b/docs/uptime-guide/app-overview.asciidoc deleted file mode 100644 index 692489a7ad311..0000000000000 --- a/docs/uptime-guide/app-overview.asciidoc +++ /dev/null @@ -1,70 +0,0 @@ -[role="xpack"] -[[uptime-app]] -== Uptime app - -The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. -You can explore endpoint status over time, drill down into specific monitors, -and view a high-level snapshot of your environment at any point in time. - -[role="screenshot"] -image::images/uptime-overview.png[Uptime app overview] - -[role="xpack"] -[[uptime-app-overview]] -=== Overview - -The Uptime overview helps you quickly identify and diagnose outages and -other connectivity issues within your network or environment. You can use the date range -selection that is global to the Uptime app, to highlight -an absolute date range, or a relative one, similar to other areas of {kib}. - -[float] -=== Filter bar - -The Filter bar enables you to quickly view specific groups of monitors, or even -an individual monitor if you have defined many. - -This control allows you to use automated filter options, as well as input custom filter -text to select specific monitors by field, URL, ID, and other attributes. - -[role="screenshot"] -image::images/filter-bar.png[Filter bar] - -[float] -=== Snapshot panel - -The Snapshot panel displays the overall -status of the environment you're monitoring or a subset of those monitors. -You can see the total number of detected monitors within the selected -Uptime date range, along with the number of monitors -in an `up` or `down` state, which is based on the last check reported by Heartbeat -for each monitor. - -Next to the counts, there is a histogram displaying the change over time throughout the -selected date range. - -[role="screenshot"] -image::images/snapshot-view.png[Snapshot view] - -[float] -=== Monitor list - -Information about individual monitors is displayed in the monitor list and provides a quick -way to navigate to a more in-depth visualization for interesting hosts or endpoints. - -The information displayed includes the recent status of a host or endpoint, when the monitor was last checked, its -ID and URL, and its IP address. There is also sparkline showing its check status over time. - -[role="screenshot"] -image::images/monitor-list.png[Monitor list] - -[float] -=== Observability integrations - -The Monitor list also contains a menu of available integrations. When Uptime detects Kubernetes or -Docker related host information, it provides links to open the Metrics app or Logs app pre-filtered -for this host. Additionally, to help you quickly determine if these solutions contain data relevant to you, -this feature contains links to filter the other views on the host's IP address. - -[role="screenshot"] -image::images/observability_integrations.png[Observability integrations] diff --git a/docs/uptime-guide/certificates.asciidoc b/docs/uptime-guide/certificates.asciidoc deleted file mode 100644 index 58db91aa080eb..0000000000000 --- a/docs/uptime-guide/certificates.asciidoc +++ /dev/null @@ -1,15 +0,0 @@ -[role="xpack"] -[[uptime-certificates]] - -=== Certificates - -The certificates page enables you to visualize TLS certificate data in your indices. In addition to the -common name, associated monitors, issuer information, and SHA fingerprints, Uptime also assigns a status -derived from the threshold values in the <>. - -Several of the columns on this page are sortable. You can use the search bar at the top of the view -to find values in most of the TLS-related fields in your Uptime indices. Additionally, using the `Alerts` -dropdown at the top of the page you can create a TLS alert. - -[role="screenshot"] -image::images/certificates-page.png[Certificates] diff --git a/docs/uptime-guide/deployment-arch.asciidoc b/docs/uptime-guide/deployment-arch.asciidoc deleted file mode 100644 index c1b2f596c6665..0000000000000 --- a/docs/uptime-guide/deployment-arch.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[role="xpack"] -[[uptime-deployment-arch]] -== Deployment Architecture - -There are multiple ways to deploy Uptime and Heartbeat. -Use the information in this section to determine the best deployment for you. -A guiding principle is that when an outage takes down the service being monitored it should not also take down Heartbeat. -You want Heartbeat to be functioning even when your service is not, so the guidelines here help you maximize this possibility. - -Heartbeat is commonly run as a centralized service within a data center. -While it is possible to run it as a separate "sidecar" process paired with each process/container, we recommend against it. -Running Heartbeat centrally ensures you will still be able to see monitoring data in the event of an overloaded, disconnected, or otherwise malfunctioning server. - -For further redundancy, you may want to deploy multiple Heartbeats across geographic and network boundaries to provide more data. -To do so, specify Heartbeat's observer {heartbeat-ref}/configuration-observer-options.html[geo options]. - -Some examples might be: - -* **A site served from a content delivery network (CDN) with points of presence (POPs) around the globe:** -To check if your site is reachable via CDN POPS, you may want to have multiple Heartbeat instances at different data centers around the world. -* **A service within a single data center that is accessed across multiple VPNs:** -Set up one Heartbeat instance within the VPN the service operates from, and another within an additional VPN that users access the service from. -Having both instances helps pinpoint network errors in the event of an outage. -* **A single service running primarily in a US east coast data center, with a hot failover located in a US west coast data center:** -In each data center, run a Heartbeat instance that checks both the local copy of the service and its counterpart across the country. -Set up two monitors in each region, one for the local service and one for the remote service. -In the event of a data center failure it will be immediately apparent if the service had a connectivity issue to the outside world or if the failure was only internal. diff --git a/docs/uptime-guide/images/cert-exp.png b/docs/uptime-guide/images/cert-exp.png deleted file mode 100644 index cd87668db96dd..0000000000000 Binary files a/docs/uptime-guide/images/cert-exp.png and /dev/null differ diff --git a/docs/uptime-guide/images/certificates-page.png b/docs/uptime-guide/images/certificates-page.png deleted file mode 100644 index 598aae982cd6a..0000000000000 Binary files a/docs/uptime-guide/images/certificates-page.png and /dev/null differ diff --git a/docs/uptime-guide/images/check-history.png b/docs/uptime-guide/images/check-history.png deleted file mode 100644 index aac5efd9b91d3..0000000000000 Binary files a/docs/uptime-guide/images/check-history.png and /dev/null differ diff --git a/docs/uptime-guide/images/create-alert.png b/docs/uptime-guide/images/create-alert.png deleted file mode 100644 index 54a0c400cad4c..0000000000000 Binary files a/docs/uptime-guide/images/create-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/crosshair-example.png b/docs/uptime-guide/images/crosshair-example.png deleted file mode 100644 index f9e89c4f622e0..0000000000000 Binary files a/docs/uptime-guide/images/crosshair-example.png and /dev/null differ diff --git a/docs/uptime-guide/images/filter-bar.png b/docs/uptime-guide/images/filter-bar.png deleted file mode 100644 index b7c424d3d0d91..0000000000000 Binary files a/docs/uptime-guide/images/filter-bar.png and /dev/null differ diff --git a/docs/uptime-guide/images/indices.png b/docs/uptime-guide/images/indices.png deleted file mode 100644 index 4090747b6726c..0000000000000 Binary files a/docs/uptime-guide/images/indices.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-charts.png b/docs/uptime-guide/images/monitor-charts.png deleted file mode 100644 index 522f34662657e..0000000000000 Binary files a/docs/uptime-guide/images/monitor-charts.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-list.png b/docs/uptime-guide/images/monitor-list.png deleted file mode 100644 index c9a8eccf01f6e..0000000000000 Binary files a/docs/uptime-guide/images/monitor-list.png and /dev/null differ diff --git a/docs/uptime-guide/images/monitor-status-alert.png b/docs/uptime-guide/images/monitor-status-alert.png deleted file mode 100644 index 847a0f58f02ce..0000000000000 Binary files a/docs/uptime-guide/images/monitor-status-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/observability_integrations.png b/docs/uptime-guide/images/observability_integrations.png deleted file mode 100644 index 3b23aa2dbd2a5..0000000000000 Binary files a/docs/uptime-guide/images/observability_integrations.png and /dev/null differ diff --git a/docs/uptime-guide/images/settings.png b/docs/uptime-guide/images/settings.png deleted file mode 100644 index d19b7f842ea68..0000000000000 Binary files a/docs/uptime-guide/images/settings.png and /dev/null differ diff --git a/docs/uptime-guide/images/snapshot-view.png b/docs/uptime-guide/images/snapshot-view.png deleted file mode 100644 index b6f07fb0721aa..0000000000000 Binary files a/docs/uptime-guide/images/snapshot-view.png and /dev/null differ diff --git a/docs/uptime-guide/images/status-bar.png b/docs/uptime-guide/images/status-bar.png deleted file mode 100644 index fd72e2b78c2a0..0000000000000 Binary files a/docs/uptime-guide/images/status-bar.png and /dev/null differ diff --git a/docs/uptime-guide/images/tls-alert.png b/docs/uptime-guide/images/tls-alert.png deleted file mode 100644 index 19efe07838903..0000000000000 Binary files a/docs/uptime-guide/images/tls-alert.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-multi-deployment.png b/docs/uptime-guide/images/uptime-multi-deployment.png deleted file mode 100644 index 5440d91e48e23..0000000000000 Binary files a/docs/uptime-guide/images/uptime-multi-deployment.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-overview.png b/docs/uptime-guide/images/uptime-overview.png deleted file mode 100644 index 25c88b2d14287..0000000000000 Binary files a/docs/uptime-guide/images/uptime-overview.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-setup.png b/docs/uptime-guide/images/uptime-setup.png deleted file mode 100644 index 398125202fc4a..0000000000000 Binary files a/docs/uptime-guide/images/uptime-setup.png and /dev/null differ diff --git a/docs/uptime-guide/images/uptime-simple-deployment.png b/docs/uptime-guide/images/uptime-simple-deployment.png deleted file mode 100644 index f46dfdb2b8b86..0000000000000 Binary files a/docs/uptime-guide/images/uptime-simple-deployment.png and /dev/null differ diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc deleted file mode 100644 index 01a93cb454ea9..0000000000000 --- a/docs/uptime-guide/index.asciidoc +++ /dev/null @@ -1,22 +0,0 @@ - -include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[] -include::{asciidoc-dir}/../../shared/attributes.asciidoc[] - -= Uptime monitoring guide - -include::overview.asciidoc[] - -include::install.asciidoc[] - -include::deployment-arch.asciidoc[] - -include::app-overview.asciidoc[] - -include::monitor.asciidoc[] - -include::settings.asciidoc[] - -include::certificates.asciidoc[] - -include::alerting.asciidoc[] - diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc deleted file mode 100644 index 05b9c6665562f..0000000000000 --- a/docs/uptime-guide/install.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -[[install-uptime]] -== Install Uptime - -The easiest way to get started with Elastic Uptime is by using our hosted {es} Service on Elastic Cloud. -The {es} Service is available on both AWS and GCP, -and automatically configures {es} and {kib}. - -[float] -=== Hosted Elasticsearch Service - -Skip managing your own {es} and {kib} instance by using our -https://www.elastic.co/cloud/elasticsearch-service[hosted {es} Service] on -Elastic Cloud. - -{ess-trial}[Try out the {es} Service for free], -then jump straight to <>. - -[float] -[[before-installation]] -=== Install the stack yourself - -If you'd rather install the stack yourself, -first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility. - -* <> -* <> -* <> - -[[install-elasticsearch]] -=== Step 1: Install Elasticsearch - -Install an {es} cluster, start it up, and make sure it's running. - -. Verify that your system meets the -https://www.elastic.co/support/matrix#matrix_jvm[minimum JVM requirements] for {es}. -. {stack-gs}/get-started-elastic-stack.html#install-elasticsearch[Install Elasticsearch]. -. {stack-gs}/get-started-elastic-stack.html#_make_sure_elasticsearch_is_up_and_running[Make sure elasticsearch is up and running]. - -[[install-kibana]] -=== Step 2: Install Kibana - -Install {kib}, start it up, and open up the web interface: - -. {stack-gs}/get-started-elastic-stack.html#install-kibana[Install Kibana]. -. {stack-gs}/get-started-elastic-stack.html#_launch_the_kibana_web_interface[Launch the Kibana Web Interface]. - -[[install-heartbeat]] -=== Step 3: Install and configure Heartbeat - -Uptime requires the setup of monitors in Heartbeat. -These monitors provide the data you'll be visualizing in the {kibana-ref}/xpack-uptime.html[Uptime app]. - -For instructions on installing and configuring Heartbeat, see the *Setup Instructions* in {kib}. -Additional information is available in {heartbeat-ref}/heartbeat-configuration.html[Configure Heartbeat]. - -[role="screenshot"] -image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana] - -[[setup-security]] -=== Step 4: Set up Security - -Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation. - -[float] -==== Important considerations - -* Make sure you're using the same major versions of Heartbeat and {kib}. - -* Index patterns tell {kib} which {es} indices you want to explore. -The Uptime app requires a +heartbeat-{major-version-only}*+ index pattern. -If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the Uptime app. - -After you install and configure Heartbeat, -the {kibana-ref}/xpack-uptime.html[Uptime app] is automatically populated with the Heartbeat monitors. diff --git a/docs/uptime-guide/monitor.asciidoc b/docs/uptime-guide/monitor.asciidoc deleted file mode 100644 index bb5d315cf63eb..0000000000000 --- a/docs/uptime-guide/monitor.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[role="xpack"] -[[uptime-monitor]] -=== Monitor - -The Monitor page helps you gain insights into the performance -of a specific network endpoint. A detailed visualization of -the monitor's request duration over time, as well as the `up`/`down` -status over time, is displayed. By configuring Machine Learning jobs -on this page, you can also also detect anomalies in response time data. - - -==== Status panel - -The Status panel displays a quick summary of the latest information -regarding your monitor. You can view its latest status, click a link to -visit the targeted URL, see its most recent request duration, and determine the -amount of time that has elapsed since the last check. - -When two Heartbeat instances are configured in different geographic locations -the map will show each location as a pinpoint on the map, along with the -amount of time elapsed since data was last received from that location. - -[role="screenshot"] -image::images/status-bar.png[Status bar] - - -[float] -==== Monitor charts - -The Monitor charts visualize information over the time specified in the -date range. These charts help you gain insights into how quickly requests are being resolved -by the targeted endpoint, and give you a sense of how frequently a host or endpoint -was down in your selected timespan. - -[role="screenshot"] -image::images/monitor-charts.png[Monitor charts] - -The Monitor duration chart displays request duration information for your monitor. -The area surrounding the line is the range of request time for the corresponding -bucket. The line is the average time. In the upper right hand of this panel -you can enable Anomaly detection using Machine Learning. When response times change -in an unexpected way the time range in which they occurred are highlighted with a color. - -The pings over time chart is a graphical representation of the check statuses over time. -Hover over the charts to display crosshairs with specific numeric data. - -[role="screenshot"] -image::images/crosshair-example.png[Chart crosshair] - -[float] -==== Check history - -The Check history table lists the total count of this monitor's checks for the selected -date range. To help find recent problems on a per-check basis, you can filter the checks -by status and location. This table can help you gain some insight into more granular details -about recent individual data points that Heartbeat is logging about your host or endpoint. - -[role="screenshot"] -image::images/check-history.png[Check history view] diff --git a/docs/uptime-guide/overview.asciidoc b/docs/uptime-guide/overview.asciidoc deleted file mode 100644 index ab230b27f8cda..0000000000000 --- a/docs/uptime-guide/overview.asciidoc +++ /dev/null @@ -1,57 +0,0 @@ -[role="xpack"] -[[uptime-overview]] -== Elastic Uptime overview - -++++ -Overview -++++ - -Elastic Uptime enables you to monitor the availability and response times of applications and services in real time and to detect problems before they affect users. - -Elastic Uptime helps you to understand uptime and response time characteristics for your services and applications. -It can be deployed both inside and outside your organization's network, so that you can analyze problems from multiple vantage points. - -Elastic Uptime uses these components: *Heartbeat*, *Elasticsearch* and *Kibana*. - -[float] -=== Heartbeat - -{heartbeat-ref}/index.html[Heartbeat] is an open source data shipper that performs uptime monitoring. -Elastic Uptime uses Heartbeat to collect monitoring data from your target applications and services, and ship it to Elasticsearch. - -[float] -=== Elasticsearch - -{ref}/index.html[Elasticsearch] is a highly scalable, open source, search and analytics engine. -Elasticsearch can store, search, and analyze large volumes of data in near real-time. -Elastic Uptime uses Elasticsearch to store monitoring data from Heartbeat in Elasticsearch documents. - -[float] -=== Kibana - -{kibana-ref}/index.html[Kibana] is an open source analytics and visualization platform designed to work with Elasticsearch. -You can use Kibana to search, view, and interact with data stored in Elasticsearch. -You can easily perform advanced data analysis and visualize your data in a variety of charts, tables, and maps. - -The {kibana-ref}/xpack-uptime.html[Elasticsearch Uptime app] in Kibana provides a dedicated user interface for viewing uptime data and identifying problem areas. - -[float] -=== Example deployments -// ++ I like the Infra/logging diagram which shows Metrics and Logging apps as separate components inside Kibana -// ++ In diagram, should be Uptime app, not Uptime UI, possibly even Elastic Uptime? Also applies to Metrics/Logging/APM. -// ++ Need more whitespace around components. - -In this simple deployment, a single instance of Heartbeat is deployed at a single monitoring location to monitor a single service. -The Heartbeat instance sends the monitoring data to Elasticsearch. -Then you can use the Uptime app in Kibana to view the data from Heartbeat and determine the status of the service. - -image::images/uptime-simple-deployment.png[Uptime simple deployment] - -In this deployment, two instances of Heartbeat are deployed at two different monitoring locations. -Both instances monitor the same service. -The Heartbeat instances send the monitoring data to Elasticsearch. -As before, you can use the Uptime app in Kibana to view the Heartbeat data and determine the status of the service. -When a failure occurs, the multiple monitoring locations enable you to pinpoint the area in which the failure has occurred. - -image::images/uptime-multi-deployment.png[Uptime multiple server deployment] - diff --git a/docs/uptime-guide/settings.asciidoc b/docs/uptime-guide/settings.asciidoc deleted file mode 100644 index 59f9af631bfa7..0000000000000 --- a/docs/uptime-guide/settings.asciidoc +++ /dev/null @@ -1,51 +0,0 @@ -[role="xpack"] -[[uptime-settings]] - -=== Settings - -The Uptime settings page lets you change which Heartbeat indices are displayed -by the uptime app. Users must have the 'all' permission to modify items on this page. -Uptime settings apply to the current space only. Use different settings in different -spaces to segment different uptime use cases and domains. - -==== Indices - -Imagine your organization has one team for internal IT services, and another -for public services. Each team operates independently and is only responsible for its -own services. In this scenario, you might set up separate Heartbeat instances for each team, -writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would -create separate roles and users for each in Elasticsearch, each with access to their own spaces, -named `it` and `external` respectively. Within each space you would navigate to the settings page -and set the correct index pattern to match only the indices that space is allowed to access. - -Note: The pattern set here only restricts what the Uptime app shows. Users may still be able -to manually query Elasticsearch for data outside this pattern. - -[role="screenshot"] -image::images/indices.png[Heartbeat indices] - -See the {kibana-ref}/uptime-security.html[Uptime security] and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security] -docs for more information. - -==== Certificate thresholds - -You can modify settings in this section to control how Uptime will visualize your TLS values in -the <>. These settings also determine which certificates will be -selected by any TLS alert you define. - -There are two fields, `age` and `expiration`. Use the `age` threshold to specify when Uptime should warn -you about certificates that have been valid for too long. Use the `expiration` threshold to specify when Uptime should warn you -about certificates that have approaching expiration dates. - -For example, a common security requirement is to make sure that none of your organization's TLS certificates have been -valid for longer than one year. Modifying the `Age limit` field's value to 365 days will help you keep track of which -certificates you may want to refresh. - -Likewise, to see which of your TLS certificates are close to expiring ahead of time, specify -an `Expiration threshold` on this page. When the count of a certificate's remaining valid days falls -below this threshold, Uptime will consider it in a warning state. When you define a TLS alert, you receive a -notification from Uptime about the certificate. - -[role="screenshot"] -image::images/cert-exp.png[Certification expiration thresholds] - diff --git a/examples/alerting_example/public/components/view_alert.tsx b/examples/alerting_example/public/components/view_alert.tsx index 75a515bfa1b25..0f7fc70648a9e 100644 --- a/examples/alerting_example/public/components/view_alert.tsx +++ b/examples/alerting_example/public/components/view_alert.tsx @@ -49,10 +49,10 @@ export const ViewAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/${id}`).then(setAlert); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/${id}/state`).then(setAlertState); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/examples/alerting_example/public/components/view_astros_alert.tsx b/examples/alerting_example/public/components/view_astros_alert.tsx index 19f235a3f3e4e..b2d3cec269b72 100644 --- a/examples/alerting_example/public/components/view_astros_alert.tsx +++ b/examples/alerting_example/public/components/view_astros_alert.tsx @@ -55,10 +55,10 @@ export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => { useEffect(() => { if (!alert) { - http.get(`${BASE_ALERT_API_PATH}/${id}`).then(setAlert); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}`).then(setAlert); } if (!alertState) { - http.get(`${BASE_ALERT_API_PATH}/${id}/state`).then(setAlertState); + http.get(`${BASE_ALERT_API_PATH}/alert/${id}/state`).then(setAlertState); } }, [alert, alertState, http, id]); diff --git a/package.json b/package.json index bb28c9e27e9f7..1a497a2ec8b10 100644 --- a/package.json +++ b/package.json @@ -123,8 +123,9 @@ "@babel/plugin-transform-modules-commonjs": "^7.10.1", "@babel/register": "^7.10.1", "@elastic/apm-rum": "^5.2.0", - "@elastic/charts": "19.8.0", + "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", + "@elastic/elasticsearch": "7.8.0", "@elastic/ems-client": "7.9.3", "@elastic/eui": "24.1.0", "@elastic/filesaver": "1.1.2", @@ -294,7 +295,6 @@ "devDependencies": { "@babel/parser": "^7.10.2", "@babel/types": "^7.10.2", - "@elastic/elasticsearch": "^7.4.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json index 885fe0e38dacf..e87699825b4e1 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json @@ -17,7 +17,18 @@ "type": "boolean" } } - } + }, + "my_array": { + "properties": { + "total": { + "type": "number" + }, + "type": { + "type": "boolean" + } + } + }, + "my_str_array": { "type": "keyword" } } } } diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index 25e49ea221c94..803bc7f13f59e 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -40,6 +40,13 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ type: 'boolean', }, }, + my_array: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, + }, + my_str_array: { type: 'keyword' }, }, }, fetch: { @@ -63,6 +70,20 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ type: 'BooleanKeyword', }, }, + my_array: { + total: { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', + }, + type: { + kind: SyntaxKind.BooleanKeyword, + type: 'BooleanKeyword', + }, + }, + my_str_array: { + kind: SyntaxKind.StringKeyword, + type: 'StringKeyword', + }, }, }, }, diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 44a12dfa9030c..fc933b6c7fd35 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -122,6 +122,16 @@ Array [ "kind": 143, "type": "StringKeyword", }, + "my_array": Object { + "total": Object { + "kind": 140, + "type": "NumberKeyword", + }, + "type": Object { + "kind": 128, + "type": "BooleanKeyword", + }, + }, "my_objects": Object { "total": Object { "kind": 140, @@ -136,6 +146,10 @@ Array [ "kind": 143, "type": "StringKeyword", }, + "my_str_array": Object { + "kind": 143, + "type": "StringKeyword", + }, }, "typeName": "Usage", }, @@ -144,6 +158,14 @@ Array [ "flat": Object { "type": "keyword", }, + "my_array": Object { + "total": Object { + "type": "number", + }, + "type": Object { + "type": "boolean", + }, + }, "my_objects": Object { "total": Object { "type": "number", @@ -155,6 +177,9 @@ Array [ "my_str": Object { "type": "text", }, + "my_str_array": Object { + "type": "keyword", + }, }, }, }, diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index f4d9beb038966..6ea4a621f92f6 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "19.8.0", + "@elastic/charts": "19.8.1", "@elastic/eui": "24.1.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts new file mode 100644 index 0000000000000..675d8840e7118 --- /dev/null +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -0,0 +1,483 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { duration } from 'moment'; +import { ElasticsearchClientConfig, parseClientOptions } from './client_config'; + +const createConfig = ( + parts: Partial = {} +): ElasticsearchClientConfig => { + return { + customHeaders: {}, + logQueries: false, + sniffOnStart: false, + sniffOnConnectionFault: false, + sniffInterval: false, + requestHeadersWhitelist: ['authorization'], + hosts: ['http://localhost:80'], + ...parts, + }; +}; + +describe('parseClientOptions', () => { + describe('basic options', () => { + it('`customHeaders` option', () => { + const config = createConfig({ + customHeaders: { + foo: 'bar', + hello: 'dolly', + }, + }); + + expect(parseClientOptions(config, false)).toEqual( + expect.objectContaining({ + headers: { + foo: 'bar', + hello: 'dolly', + }, + }) + ); + }); + + it('`keepAlive` option', () => { + expect(parseClientOptions(createConfig({ keepAlive: true }), false)).toEqual( + expect.objectContaining({ agent: { keepAlive: true } }) + ); + expect(parseClientOptions(createConfig({ keepAlive: false }), false).agent).toBeUndefined(); + }); + + it('`sniffOnStart` options', () => { + expect( + parseClientOptions( + createConfig({ + sniffOnStart: true, + }), + false + ).sniffOnStart + ).toEqual(true); + + expect( + parseClientOptions( + createConfig({ + sniffOnStart: false, + }), + false + ).sniffOnStart + ).toEqual(false); + }); + it('`sniffOnConnectionFault` options', () => { + expect( + parseClientOptions( + createConfig({ + sniffOnConnectionFault: true, + }), + false + ).sniffOnConnectionFault + ).toEqual(true); + + expect( + parseClientOptions( + createConfig({ + sniffOnConnectionFault: false, + }), + false + ).sniffOnConnectionFault + ).toEqual(false); + }); + it('`sniffInterval` options', () => { + expect( + parseClientOptions( + createConfig({ + sniffInterval: false, + }), + false + ).sniffInterval + ).toEqual(false); + + expect( + parseClientOptions( + createConfig({ + sniffInterval: duration(100, 'ms'), + }), + false + ).sniffInterval + ).toEqual(100); + }); + + it('`hosts` option', () => { + const options = parseClientOptions( + createConfig({ + hosts: ['http://node-A:9200', 'http://node-B', 'https://node-C'], + }), + false + ); + + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + Object { + "url": "http://node-b/", + }, + Object { + "url": "https://node-c/", + }, + ] + `); + }); + }); + + describe('authorization', () => { + describe('when `scoped` is false', () => { + it('adds the `auth` option if both `username` and `password` are set', () => { + expect( + parseClientOptions( + createConfig({ + username: 'user', + }), + false + ).auth + ).toBeUndefined(); + + expect( + parseClientOptions( + createConfig({ + password: 'pass', + }), + false + ).auth + ).toBeUndefined(); + + expect( + parseClientOptions( + createConfig({ + username: 'user', + password: 'pass', + }), + false + ) + ).toEqual( + expect.objectContaining({ + auth: { + username: 'user', + password: 'pass', + }, + }) + ); + }); + + it('adds auth to the nodes if both `username` and `password` are set', () => { + let options = parseClientOptions( + createConfig({ + username: 'user', + hosts: ['http://node-A:9200'], + }), + false + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + ] + `); + + options = parseClientOptions( + createConfig({ + password: 'pass', + hosts: ['http://node-A:9200'], + }), + false + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + ] + `); + + options = parseClientOptions( + createConfig({ + username: 'user', + password: 'pass', + hosts: ['http://node-A:9200'], + }), + false + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://user:pass@node-a:9200/", + }, + ] + `); + }); + }); + describe('when `scoped` is true', () => { + it('does not add the `auth` option even if both `username` and `password` are set', () => { + expect( + parseClientOptions( + createConfig({ + username: 'user', + password: 'pass', + }), + true + ).auth + ).toBeUndefined(); + }); + + it('does not add auth to the nodes even if both `username` and `password` are set', () => { + const options = parseClientOptions( + createConfig({ + username: 'user', + password: 'pass', + hosts: ['http://node-A:9200'], + }), + true + ); + expect(options.nodes).toMatchInlineSnapshot(` + Array [ + Object { + "url": "http://node-a:9200/", + }, + ] + `); + }); + }); + }); + + describe('ssl config', () => { + it('does not generate ssl option is ssl config is not set', () => { + expect(parseClientOptions(createConfig({}), false).ssl).toBeUndefined(); + expect(parseClientOptions(createConfig({}), true).ssl).toBeUndefined(); + }); + + it('handles the `certificateAuthorities` option', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { verificationMode: 'full', certificateAuthorities: ['content-of-ca-path'] }, + }), + false + ).ssl!.ca + ).toEqual(['content-of-ca-path']); + expect( + parseClientOptions( + createConfig({ + ssl: { verificationMode: 'full', certificateAuthorities: ['content-of-ca-path'] }, + }), + true + ).ssl!.ca + ).toEqual(['content-of-ca-path']); + }); + + describe('verificationMode', () => { + it('handles `none` value', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'none', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "rejectUnauthorized": false, + } + `); + }); + it('handles `certificate` value', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'certificate', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "checkServerIdentity": [Function], + "rejectUnauthorized": true, + } + `); + }); + it('handles `full` value', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "rejectUnauthorized": true, + } + `); + }); + it('throws for invalid values', () => { + expect( + () => + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'unknown' as any, + }, + }), + false + ).ssl + ).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: unknown"`); + }); + it('throws for undefined values', () => { + expect( + () => + parseClientOptions( + createConfig({ + ssl: { + verificationMode: undefined as any, + }, + }), + false + ).ssl + ).toThrowErrorMatchingInlineSnapshot(`"Unknown ssl verificationMode: undefined"`); + }); + }); + + describe('`certificate`, `key` and `passphrase`', () => { + it('are not added if `key` is not present', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + certificate: 'content-of-cert', + keyPassphrase: 'passphrase', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "rejectUnauthorized": true, + } + `); + }); + + it('are not added if `certificate` is not present', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + key: 'content-of-key', + keyPassphrase: 'passphrase', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "rejectUnauthorized": true, + } + `); + }); + + it('are added if `key` and `certificate` are present and `scoped` is false', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + key: 'content-of-key', + certificate: 'content-of-cert', + keyPassphrase: 'passphrase', + }, + }), + false + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "cert": "content-of-cert", + "key": "content-of-key", + "passphrase": "passphrase", + "rejectUnauthorized": true, + } + `); + }); + + it('are not added if `scoped` is true unless `alwaysPresentCertificate` is true', () => { + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + key: 'content-of-key', + certificate: 'content-of-cert', + keyPassphrase: 'passphrase', + }, + }), + true + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "rejectUnauthorized": true, + } + `); + + expect( + parseClientOptions( + createConfig({ + ssl: { + verificationMode: 'full', + key: 'content-of-key', + certificate: 'content-of-cert', + keyPassphrase: 'passphrase', + alwaysPresentCertificate: true, + }, + }), + true + ).ssl + ).toMatchInlineSnapshot(` + Object { + "ca": undefined, + "cert": "content-of-cert", + "key": "content-of-key", + "passphrase": "passphrase", + "rejectUnauthorized": true, + } + `); + }); + }); + }); +}); diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts new file mode 100644 index 0000000000000..f365ca331cfea --- /dev/null +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -0,0 +1,158 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConnectionOptions as TlsConnectionOptions } from 'tls'; +import { URL } from 'url'; +import { Duration } from 'moment'; +import { ClientOptions, NodeOptions } from '@elastic/elasticsearch'; +import { ElasticsearchConfig } from '../elasticsearch_config'; + +/** + * Configuration options to be used to create a {@link IClusterClient | cluster client} using the + * {@link ElasticsearchServiceStart.createClient | createClient API} + * + * @public + */ +export type ElasticsearchClientConfig = Pick< + ElasticsearchConfig, + | 'customHeaders' + | 'logQueries' + | 'sniffOnStart' + | 'sniffOnConnectionFault' + | 'requestHeadersWhitelist' + | 'sniffInterval' + | 'hosts' + | 'username' + | 'password' +> & { + pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout']; + requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout']; + ssl?: Partial; + keepAlive?: boolean; +}; + +/** + * Parse the client options from given client config and `scoped` flag. + * + * @param config The config to generate the client options from. + * @param scoped if true, will adapt the configuration to be used by a scoped client + * (will remove basic auth and ssl certificates) + */ +export function parseClientOptions( + config: ElasticsearchClientConfig, + scoped: boolean +): ClientOptions { + const clientOptions: ClientOptions = { + sniffOnStart: config.sniffOnStart, + sniffOnConnectionFault: config.sniffOnConnectionFault, + headers: config.customHeaders, + }; + + if (config.pingTimeout != null) { + clientOptions.pingTimeout = getDurationAsMs(config.pingTimeout); + } + if (config.requestTimeout != null) { + clientOptions.requestTimeout = getDurationAsMs(config.requestTimeout); + } + if (config.sniffInterval != null) { + clientOptions.sniffInterval = + typeof config.sniffInterval === 'boolean' + ? config.sniffInterval + : getDurationAsMs(config.sniffInterval); + } + if (config.keepAlive) { + clientOptions.agent = { + keepAlive: config.keepAlive, + }; + } + + if (config.username && config.password && !scoped) { + clientOptions.auth = { + username: config.username, + password: config.password, + }; + } + + clientOptions.nodes = config.hosts.map((host) => convertHost(host, !scoped, config)); + + if (config.ssl) { + clientOptions.ssl = generateSslConfig( + config.ssl, + scoped && !config.ssl.alwaysPresentCertificate + ); + } + + return clientOptions; +} + +const generateSslConfig = ( + sslConfig: Required['ssl'], + ignoreCertAndKey: boolean +): TlsConnectionOptions => { + const ssl: TlsConnectionOptions = { + ca: sslConfig.certificateAuthorities, + }; + + const verificationMode = sslConfig.verificationMode; + switch (verificationMode) { + case 'none': + ssl.rejectUnauthorized = false; + break; + case 'certificate': + ssl.rejectUnauthorized = true; + // by default, NodeJS is checking the server identify + ssl.checkServerIdentity = () => undefined; + break; + case 'full': + ssl.rejectUnauthorized = true; + break; + default: + throw new Error(`Unknown ssl verificationMode: ${verificationMode}`); + } + + // Add client certificate and key if required by elasticsearch + if (!ignoreCertAndKey && sslConfig.certificate && sslConfig.key) { + ssl.cert = sslConfig.certificate; + ssl.key = sslConfig.key; + ssl.passphrase = sslConfig.keyPassphrase; + } + + return ssl; +}; + +const convertHost = ( + host: string, + needAuth: boolean, + { username, password }: ElasticsearchClientConfig +): NodeOptions => { + const url = new URL(host); + const isHTTPS = url.protocol === 'https:'; + url.port = url.port || (isHTTPS ? '443' : '80'); + if (needAuth && username && password) { + url.username = username; + url.password = password; + } + + return { + url, + }; +}; + +const getDurationAsMs = (duration: number | Duration) => + typeof duration === 'number' ? duration : duration.asMilliseconds(); diff --git a/src/plugins/kibana_utils/public/parse/index.ts b/src/core/server/elasticsearch/client/cluster_client.test.mocks.ts similarity index 85% rename from src/plugins/kibana_utils/public/parse/index.ts rename to src/core/server/elasticsearch/client/cluster_client.test.mocks.ts index 997cf1b9ae4d1..e08c0d55b4551 100644 --- a/src/plugins/kibana_utils/public/parse/index.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.mocks.ts @@ -17,4 +17,7 @@ * under the License. */ -export * from './ipv4_address'; +export const configureClientMock = jest.fn(); +jest.doMock('./configure_client', () => ({ + configureClient: configureClientMock, +})); diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts new file mode 100644 index 0000000000000..85517b80745f1 --- /dev/null +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -0,0 +1,376 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { configureClientMock } from './cluster_client.test.mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { httpServerMock } from '../../http/http_server.mocks'; +import { GetAuthHeaders } from '../../http'; +import { elasticsearchClientMock } from './mocks'; +import { ClusterClient } from './cluster_client'; +import { ElasticsearchClientConfig } from './client_config'; + +const createConfig = ( + parts: Partial = {} +): ElasticsearchClientConfig => { + return { + logQueries: false, + sniffOnStart: false, + sniffOnConnectionFault: false, + sniffInterval: false, + requestHeadersWhitelist: ['authorization'], + customHeaders: {}, + hosts: ['http://localhost'], + ...parts, + }; +}; + +describe('ClusterClient', () => { + let logger: ReturnType; + let getAuthHeaders: jest.MockedFunction; + let internalClient: ReturnType; + let scopedClient: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + internalClient = elasticsearchClientMock.createInternalClient(); + scopedClient = elasticsearchClientMock.createInternalClient(); + getAuthHeaders = jest.fn().mockImplementation(() => ({ + authorization: 'auth', + foo: 'bar', + })); + + configureClientMock.mockImplementation((config, { scoped = false }) => { + return scoped ? scopedClient : internalClient; + }); + }); + + afterEach(() => { + configureClientMock.mockReset(); + }); + + it('creates a single internal and scoped client during initialization', () => { + const config = createConfig(); + + new ClusterClient(config, logger, getAuthHeaders); + + expect(configureClientMock).toHaveBeenCalledTimes(2); + expect(configureClientMock).toHaveBeenCalledWith(config, { logger }); + expect(configureClientMock).toHaveBeenCalledWith(config, { logger, scoped: true }); + }); + + describe('#asInternalUser', () => { + it('returns the internal client', () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + + expect(clusterClient.asInternalUser).toBe(internalClient); + }); + }); + + describe('#asScoped', () => { + it('returns a scoped cluster client bound to the request', () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest(); + + const scopedClusterClient = clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ headers: expect.any(Object) }); + + expect(scopedClusterClient.asInternalUser).toBe(clusterClient.asInternalUser); + expect(scopedClusterClient.asCurrentUser).toBe(scopedClient.child.mock.results[0].value); + }); + + it('returns a distinct scoped cluster client on each call', () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest(); + + const scopedClusterClient1 = clusterClient.asScoped(request); + const scopedClusterClient2 = clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(2); + + expect(scopedClusterClient1).not.toBe(scopedClusterClient2); + expect(scopedClusterClient1.asInternalUser).toBe(scopedClusterClient2.asInternalUser); + }); + + it('creates a scoped client with filtered request headers', () => { + const config = createConfig({ + requestHeadersWhitelist: ['foo'], + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + headers: { + foo: 'bar', + hello: 'dolly', + }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { foo: 'bar' }, + }); + }); + + it('creates a scoped facade with filtered auth headers', () => { + const config = createConfig({ + requestHeadersWhitelist: ['authorization'], + }); + getAuthHeaders.mockReturnValue({ + authorization: 'auth', + other: 'nope', + }); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({}); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { authorization: 'auth' }, + }); + }); + + it('respects auth headers precedence', () => { + const config = createConfig({ + requestHeadersWhitelist: ['authorization'], + }); + getAuthHeaders.mockReturnValue({ + authorization: 'auth', + other: 'nope', + }); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + headers: { + authorization: 'override', + }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { authorization: 'auth' }, + }); + }); + + it('includes the `customHeaders` from the config without filtering them', () => { + const config = createConfig({ + customHeaders: { + foo: 'bar', + hello: 'dolly', + }, + requestHeadersWhitelist: ['authorization'], + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({}); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + foo: 'bar', + hello: 'dolly', + }, + }); + }); + + it('respect the precedence of auth headers over config headers', () => { + const config = createConfig({ + customHeaders: { + foo: 'config', + hello: 'dolly', + }, + requestHeadersWhitelist: ['foo'], + }); + getAuthHeaders.mockReturnValue({ + foo: 'auth', + }); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({}); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + foo: 'auth', + hello: 'dolly', + }, + }); + }); + + it('respect the precedence of request headers over config headers', () => { + const config = createConfig({ + customHeaders: { + foo: 'config', + hello: 'dolly', + }, + requestHeadersWhitelist: ['foo'], + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + headers: { foo: 'request' }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + foo: 'request', + hello: 'dolly', + }, + }); + }); + + it('filter headers when called with a `FakeRequest`', () => { + const config = createConfig({ + requestHeadersWhitelist: ['authorization'], + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = { + headers: { + authorization: 'auth', + hello: 'dolly', + }, + }; + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { authorization: 'auth' }, + }); + }); + + it('does not add auth headers when called with a `FakeRequest`', () => { + const config = createConfig({ + requestHeadersWhitelist: ['authorization', 'foo'], + }); + getAuthHeaders.mockReturnValue({ + authorization: 'auth', + }); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = { + headers: { + foo: 'bar', + hello: 'dolly', + }, + }; + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { foo: 'bar' }, + }); + }); + }); + + describe('#close', () => { + it('closes both underlying clients', async () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + + await clusterClient.close(); + + expect(internalClient.close).toHaveBeenCalledTimes(1); + expect(scopedClient.close).toHaveBeenCalledTimes(1); + }); + + it('waits for both clients to close', async (done) => { + expect.assertions(4); + + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + + let internalClientClosed = false; + let scopedClientClosed = false; + let clusterClientClosed = false; + + let closeInternalClient: () => void; + let closeScopedClient: () => void; + + internalClient.close.mockReturnValue( + new Promise((resolve) => { + closeInternalClient = resolve; + }).then(() => { + expect(clusterClientClosed).toBe(false); + internalClientClosed = true; + }) + ); + scopedClient.close.mockReturnValue( + new Promise((resolve) => { + closeScopedClient = resolve; + }).then(() => { + expect(clusterClientClosed).toBe(false); + scopedClientClosed = true; + }) + ); + + clusterClient.close().then(() => { + clusterClientClosed = true; + expect(internalClientClosed).toBe(true); + expect(scopedClientClosed).toBe(true); + done(); + }); + + closeInternalClient!(); + closeScopedClient!(); + }); + + it('return a rejected promise is any client rejects', async () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + + internalClient.close.mockRejectedValue(new Error('error closing client')); + + expect(clusterClient.close()).rejects.toThrowErrorMatchingInlineSnapshot( + `"error closing client"` + ); + }); + + it('does nothing after the first call', async () => { + const clusterClient = new ClusterClient(createConfig(), logger, getAuthHeaders); + + await clusterClient.close(); + + expect(internalClient.close).toHaveBeenCalledTimes(1); + expect(scopedClient.close).toHaveBeenCalledTimes(1); + + await clusterClient.close(); + await clusterClient.close(); + + expect(internalClient.close).toHaveBeenCalledTimes(1); + expect(scopedClient.close).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts new file mode 100644 index 0000000000000..d9a0e6fe3f238 --- /dev/null +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Client } from '@elastic/elasticsearch'; +import { Logger } from '../../logging'; +import { GetAuthHeaders, isRealRequest, Headers } from '../../http'; +import { ensureRawRequest, filterHeaders } from '../../http/router'; +import { ScopeableRequest } from '../types'; +import { ElasticsearchClient } from './types'; +import { configureClient } from './configure_client'; +import { ElasticsearchClientConfig } from './client_config'; +import { ScopedClusterClient, IScopedClusterClient } from './scoped_cluster_client'; + +const noop = () => undefined; + +/** + * Represents an Elasticsearch cluster API client created by the platform. + * It allows to call API on behalf of the internal Kibana user and + * the actual user that is derived from the request headers (via `asScoped(...)`). + * + * @public + **/ +export interface IClusterClient { + /** + * A {@link ElasticsearchClient | client} to be used to query the ES cluster on behalf of the Kibana internal user + */ + readonly asInternalUser: ElasticsearchClient; + /** + * Creates a {@link IScopedClusterClient | scoped cluster client} bound to given {@link ScopeableRequest | request} + */ + asScoped: (request: ScopeableRequest) => IScopedClusterClient; +} + +/** + * See {@link IClusterClient} + * + * @public + */ +export interface ICustomClusterClient extends IClusterClient { + /** + * Closes the cluster client. After that client cannot be used and one should + * create a new client instance to be able to interact with Elasticsearch API. + */ + close: () => Promise; +} + +/** @internal **/ +export class ClusterClient implements ICustomClusterClient { + public readonly asInternalUser: Client; + private readonly rootScopedClient: Client; + + private isClosed = false; + + constructor( + private readonly config: ElasticsearchClientConfig, + logger: Logger, + private readonly getAuthHeaders: GetAuthHeaders = noop + ) { + this.asInternalUser = configureClient(config, { logger }); + this.rootScopedClient = configureClient(config, { logger, scoped: true }); + } + + asScoped(request: ScopeableRequest) { + const scopedHeaders = this.getScopedHeaders(request); + const scopedClient = this.rootScopedClient.child({ + headers: scopedHeaders, + }); + return new ScopedClusterClient(this.asInternalUser, scopedClient); + } + + public async close() { + if (this.isClosed) { + return; + } + this.isClosed = true; + await Promise.all([this.asInternalUser.close(), this.rootScopedClient.close()]); + } + + private getScopedHeaders(request: ScopeableRequest): Headers { + let scopedHeaders: Headers; + if (isRealRequest(request)) { + const authHeaders = this.getAuthHeaders(request); + const requestHeaders = ensureRawRequest(request).headers; + scopedHeaders = filterHeaders( + { ...requestHeaders, ...authHeaders }, + this.config.requestHeadersWhitelist + ); + } else { + scopedHeaders = filterHeaders(request?.headers ?? {}, this.config.requestHeadersWhitelist); + } + + return { + ...this.config.customHeaders, + ...scopedHeaders, + }; + } +} diff --git a/src/core/server/elasticsearch/client/configure_client.test.mocks.ts b/src/core/server/elasticsearch/client/configure_client.test.mocks.ts new file mode 100644 index 0000000000000..0a74f57120fb0 --- /dev/null +++ b/src/core/server/elasticsearch/client/configure_client.test.mocks.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const parseClientOptionsMock = jest.fn(); +jest.doMock('./client_config', () => ({ + parseClientOptions: parseClientOptionsMock, +})); + +export const ClientMock = jest.fn(); +jest.doMock('@elastic/elasticsearch', () => { + const actual = jest.requireActual('@elastic/elasticsearch'); + return { + ...actual, + Client: ClientMock, + }; +}); diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts new file mode 100644 index 0000000000000..32da142764a78 --- /dev/null +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -0,0 +1,279 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RequestEvent, errors } from '@elastic/elasticsearch'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; + +import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import EventEmitter from 'events'; +import type { ElasticsearchClientConfig } from './client_config'; +import { configureClient } from './configure_client'; + +const createFakeConfig = ( + parts: Partial = {} +): ElasticsearchClientConfig => { + return ({ + type: 'fake-config', + ...parts, + } as unknown) as ElasticsearchClientConfig; +}; + +const createFakeClient = () => { + const client = new EventEmitter(); + jest.spyOn(client, 'on'); + return client; +}; + +const createApiResponse = ({ + body, + statusCode = 200, + headers = {}, + warnings = [], + params, +}: { + body: T; + statusCode?: number; + headers?: Record; + warnings?: string[]; + params?: TransportRequestParams; +}): RequestEvent => { + return { + body, + statusCode, + headers, + warnings, + meta: { + request: { + params: params!, + } as any, + } as any, + }; +}; + +describe('configureClient', () => { + let logger: ReturnType; + let config: ElasticsearchClientConfig; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + config = createFakeConfig(); + parseClientOptionsMock.mockReturnValue({}); + ClientMock.mockImplementation(() => createFakeClient()); + }); + + afterEach(() => { + parseClientOptionsMock.mockReset(); + ClientMock.mockReset(); + }); + + it('calls `parseClientOptions` with the correct parameters', () => { + configureClient(config, { logger, scoped: false }); + + expect(parseClientOptionsMock).toHaveBeenCalledTimes(1); + expect(parseClientOptionsMock).toHaveBeenCalledWith(config, false); + + parseClientOptionsMock.mockClear(); + + configureClient(config, { logger, scoped: true }); + + expect(parseClientOptionsMock).toHaveBeenCalledTimes(1); + expect(parseClientOptionsMock).toHaveBeenCalledWith(config, true); + }); + + it('constructs a client using the options returned by `parseClientOptions`', () => { + const parsedOptions = { + nodes: ['http://localhost'], + }; + parseClientOptionsMock.mockReturnValue(parsedOptions); + + const client = configureClient(config, { logger, scoped: false }); + + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).toHaveBeenCalledWith(parsedOptions); + expect(client).toBe(ClientMock.mock.results[0].value); + }); + + it('listens to client on `response` events', () => { + const client = configureClient(config, { logger, scoped: false }); + + expect(client.on).toHaveBeenCalledTimes(1); + expect(client.on).toHaveBeenCalledWith('response', expect.any(Function)); + }); + + describe('Client logging', () => { + it('logs error when the client emits an error', () => { + const client = configureClient(config, { logger, scoped: false }); + + const response = createApiResponse({ + body: { + error: { + type: 'error message', + }, + }, + }); + client.emit('response', new errors.ResponseError(response), null); + client.emit('response', new Error('some error'), null); + + expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + "ResponseError: error message", + ], + Array [ + "Error: some error", + ], + ] + `); + }); + + it('logs each queries if `logQueries` is true', () => { + const client = configureClient( + createFakeConfig({ + logQueries: true, + }), + { logger, scoped: false } + ); + + const response = createApiResponse({ + body: {}, + statusCode: 200, + params: { + method: 'GET', + path: '/foo', + querystring: { hello: 'dolly' }, + }, + }); + + client.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo + hello=dolly", + Object { + "tags": Array [ + "query", + ], + }, + ], + ] + `); + }); + + it('properly encode queries', () => { + const client = configureClient( + createFakeConfig({ + logQueries: true, + }), + { logger, scoped: false } + ); + + const response = createApiResponse({ + body: {}, + statusCode: 200, + params: { + method: 'GET', + path: '/foo', + querystring: { city: 'Münich' }, + }, + }); + + client.emit('response', null, response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "200 + GET /foo + city=M%C3%BCnich", + Object { + "tags": Array [ + "query", + ], + }, + ], + ] + `); + }); + + it('logs queries even in case of errors if `logQueries` is true', () => { + const client = configureClient( + createFakeConfig({ + logQueries: true, + }), + { logger, scoped: false } + ); + + const response = createApiResponse({ + statusCode: 500, + body: { + error: { + type: 'internal server error', + }, + }, + params: { + method: 'GET', + path: '/foo', + querystring: { hello: 'dolly' }, + }, + }); + client.emit('response', new errors.ResponseError(response), response); + + expect(loggingSystemMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "500 + GET /foo + hello=dolly", + Object { + "tags": Array [ + "query", + ], + }, + ], + ] + `); + }); + + it('does not log queries if `logQueries` is false', () => { + const client = configureClient( + createFakeConfig({ + logQueries: false, + }), + { logger, scoped: false } + ); + + const response = createApiResponse({ + body: {}, + statusCode: 200, + params: { + method: 'GET', + path: '/foo', + }, + }); + + client.emit('response', null, response); + + expect(logger.debug).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts new file mode 100644 index 0000000000000..5377f8ca1b070 --- /dev/null +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { stringify } from 'querystring'; +import { Client } from '@elastic/elasticsearch'; +import { Logger } from '../../logging'; +import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; + +export const configureClient = ( + config: ElasticsearchClientConfig, + { logger, scoped = false }: { logger: Logger; scoped?: boolean } +): Client => { + const clientOptions = parseClientOptions(config, scoped); + + const client = new Client(clientOptions); + addLogging(client, logger, config.logQueries); + + return client; +}; + +const addLogging = (client: Client, logger: Logger, logQueries: boolean) => { + client.on('response', (err, event) => { + if (err) { + logger.error(`${err.name}: ${err.message}`); + } + if (event && logQueries) { + const params = event.meta.request.params; + + // definition is wrong, `params.querystring` can be either a string or an object + const querystring = convertQueryString(params.querystring); + + logger.debug( + `${event.statusCode}\n${params.method} ${params.path}${ + querystring ? `\n${querystring}` : '' + }`, + { + tags: ['query'], + } + ); + } + }); +}; + +const convertQueryString = (qs: string | Record | undefined): string => { + if (qs === undefined || typeof qs === 'string') { + return qs ?? ''; + } + return stringify(qs); +}; diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts new file mode 100644 index 0000000000000..18e84482024ca --- /dev/null +++ b/src/core/server/elasticsearch/client/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { ElasticsearchClient } from './types'; +export { IScopedClusterClient, ScopedClusterClient } from './scoped_cluster_client'; +export { ElasticsearchClientConfig } from './client_config'; +export { IClusterClient, ICustomClusterClient, ClusterClient } from './cluster_client'; +export { configureClient } from './configure_client'; diff --git a/src/core/server/elasticsearch/client/mocks.test.ts b/src/core/server/elasticsearch/client/mocks.test.ts new file mode 100644 index 0000000000000..b882f8d0c5d79 --- /dev/null +++ b/src/core/server/elasticsearch/client/mocks.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { elasticsearchClientMock } from './mocks'; + +describe('Mocked client', () => { + let client: ReturnType; + + const expectMocked = (fn: jest.MockedFunction | undefined) => { + expect(fn).toBeDefined(); + expect(fn.mockReturnValue).toEqual(expect.any(Function)); + }; + + beforeEach(() => { + client = elasticsearchClientMock.createInternalClient(); + }); + + it('`transport.request` should be mocked', () => { + expectMocked(client.transport.request); + }); + + it('root level API methods should be mocked', () => { + expectMocked(client.bulk); + expectMocked(client.search); + }); + + it('nested level API methods should be mocked', () => { + expectMocked(client.asyncSearch.get); + expectMocked(client.nodes.info); + }); + + it('`close` should be mocked', () => { + expectMocked(client.close); + }); + + it('`child` should be mocked and return a mocked Client', () => { + expectMocked(client.child); + + const child = client.child(); + + expect(child).not.toBe(client); + expectMocked(child.search); + }); +}); diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts new file mode 100644 index 0000000000000..75644435a7f2a --- /dev/null +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Client, ApiResponse } from '@elastic/elasticsearch'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { ElasticsearchClient } from './types'; +import { ICustomClusterClient } from './cluster_client'; + +const createInternalClientMock = (): DeeplyMockedKeys => { + // we mimic 'reflection' on a concrete instance of the client to generate the mocked functions. + const client = new Client({ + node: 'http://localhost', + }) as any; + + const blackListedProps = [ + '_events', + '_eventsCount', + '_maxListeners', + 'name', + 'serializer', + 'connectionPool', + 'transport', + 'helpers', + ]; + + const mockify = (obj: Record, blacklist: string[] = []) => { + Object.keys(obj) + .filter((key) => !blacklist.includes(key)) + .forEach((key) => { + const propType = typeof obj[key]; + if (propType === 'function') { + obj[key] = jest.fn(); + } else if (propType === 'object' && obj[key] != null) { + mockify(obj[key]); + } + }); + }; + + mockify(client, blackListedProps); + + client.transport = { + request: jest.fn(), + }; + client.close = jest.fn().mockReturnValue(Promise.resolve()); + client.child = jest.fn().mockImplementation(() => createInternalClientMock()); + + return (client as unknown) as DeeplyMockedKeys; +}; + +export type ElasticSearchClientMock = DeeplyMockedKeys; + +const createClientMock = (): ElasticSearchClientMock => + (createInternalClientMock() as unknown) as ElasticSearchClientMock; + +interface ScopedClusterClientMock { + asInternalUser: ElasticSearchClientMock; + asCurrentUser: ElasticSearchClientMock; +} + +const createScopedClusterClientMock = () => { + const mock: ScopedClusterClientMock = { + asInternalUser: createClientMock(), + asCurrentUser: createClientMock(), + }; + + return mock; +}; + +export interface ClusterClientMock { + asInternalUser: ElasticSearchClientMock; + asScoped: jest.MockedFunction<() => ScopedClusterClientMock>; +} + +const createClusterClientMock = () => { + const mock: ClusterClientMock = { + asInternalUser: createClientMock(), + asScoped: jest.fn(), + }; + + mock.asScoped.mockReturnValue(createScopedClusterClientMock()); + + return mock; +}; + +export type CustomClusterClientMock = jest.Mocked & ClusterClientMock; + +const createCustomClusterClientMock = () => { + const mock: CustomClusterClientMock = { + asInternalUser: createClientMock(), + asScoped: jest.fn(), + close: jest.fn(), + }; + + mock.asScoped.mockReturnValue(createScopedClusterClientMock()); + mock.close.mockReturnValue(Promise.resolve()); + + return mock; +}; + +export type MockedTransportRequestPromise = TransportRequestPromise & { + abort: jest.MockedFunction<() => undefined>; +}; + +const createMockedClientResponse = (body: T): MockedTransportRequestPromise> => { + const response: ApiResponse = { + body, + statusCode: 200, + warnings: [], + headers: {}, + meta: {} as any, + }; + const promise = Promise.resolve(response); + (promise as MockedTransportRequestPromise>).abort = jest.fn(); + + return promise as MockedTransportRequestPromise>; +}; + +const createMockedClientError = (err: any): MockedTransportRequestPromise => { + const promise = Promise.reject(err); + (promise as MockedTransportRequestPromise).abort = jest.fn(); + return promise as MockedTransportRequestPromise; +}; + +export const elasticsearchClientMock = { + createClusterClient: createClusterClientMock, + createCustomClusterClient: createCustomClusterClientMock, + createScopedClusterClient: createScopedClusterClientMock, + createElasticSearchClient: createClientMock, + createInternalClient: createInternalClientMock, + createClientResponse: createMockedClientResponse, + createClientError: createMockedClientError, +}; diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts new file mode 100644 index 0000000000000..78ca8fcbd3c07 --- /dev/null +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.test.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { elasticsearchClientMock } from './mocks'; +import { ScopedClusterClient } from './scoped_cluster_client'; + +describe('ScopedClusterClient', () => { + it('uses the internal client passed in the constructor', () => { + const internalClient = elasticsearchClientMock.createElasticSearchClient(); + const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + + const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); + + expect(scopedClusterClient.asInternalUser).toBe(internalClient); + }); + + it('uses the scoped client passed in the constructor', () => { + const internalClient = elasticsearchClientMock.createElasticSearchClient(); + const scopedClient = elasticsearchClientMock.createElasticSearchClient(); + + const scopedClusterClient = new ScopedClusterClient(internalClient, scopedClient); + + expect(scopedClusterClient.asCurrentUser).toBe(scopedClient); + }); +}); diff --git a/src/core/server/elasticsearch/client/scoped_cluster_client.ts b/src/core/server/elasticsearch/client/scoped_cluster_client.ts new file mode 100644 index 0000000000000..1af7948a65e16 --- /dev/null +++ b/src/core/server/elasticsearch/client/scoped_cluster_client.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ElasticsearchClient } from './types'; + +/** + * Serves the same purpose as the normal {@link ClusterClient | cluster client} but exposes + * an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal + * user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers + * extracted from the current user request to the API instead. + * + * @public + **/ +export interface IScopedClusterClient { + /** + * A {@link ElasticsearchClient | client} to be used to query the elasticsearch cluster + * on behalf of the internal Kibana user. + */ + readonly asInternalUser: ElasticsearchClient; + /** + * A {@link ElasticsearchClient | client} to be used to query the elasticsearch cluster + * on behalf of the user that initiated the request to the Kibana server. + */ + readonly asCurrentUser: ElasticsearchClient; +} + +/** @internal **/ +export class ScopedClusterClient implements IScopedClusterClient { + constructor( + public readonly asInternalUser: ElasticsearchClient, + public readonly asCurrentUser: ElasticsearchClient + ) {} +} diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts new file mode 100644 index 0000000000000..934120c330e92 --- /dev/null +++ b/src/core/server/elasticsearch/client/types.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { Client } from '@elastic/elasticsearch'; +import type { + ApiResponse, + TransportRequestOptions, + TransportRequestParams, +} from '@elastic/elasticsearch/lib/Transport'; + +/** + * Client used to query the elasticsearch cluster. + * + * @public + */ +export type ElasticsearchClient = Omit< + Client, + 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'helpers' | 'child' | 'close' +> & { + transport: { + request( + params: TransportRequestParams, + options?: TransportRequestOptions + ): Promise; + }; +}; diff --git a/src/core/server/elasticsearch/elasticsearch_service.mock.ts b/src/core/server/elasticsearch/elasticsearch_service.mock.ts index f524781de4c7e..b97f6df6b0afc 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.mock.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.mock.ts @@ -19,6 +19,11 @@ import { BehaviorSubject } from 'rxjs'; import { ILegacyClusterClient, ILegacyCustomClusterClient } from './legacy'; +import { + elasticsearchClientMock, + ClusterClientMock, + CustomClusterClientMock, +} from './client/mocks'; import { legacyClientMock } from './legacy/mocks'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; @@ -33,6 +38,13 @@ interface MockedElasticSearchServiceSetup { }; } +type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; + +interface MockedInternalElasticSearchServiceStart extends MockedElasticSearchServiceStart { + client: ClusterClientMock; + createClient: jest.MockedFunction<() => CustomClusterClientMock>; +} + const createSetupContractMock = () => { const setupContract: MockedElasticSearchServiceSetup = { legacy: { @@ -47,8 +59,6 @@ const createSetupContractMock = () => { return setupContract; }; -type MockedElasticSearchServiceStart = MockedElasticSearchServiceSetup; - const createStartContractMock = () => { const startContract: MockedElasticSearchServiceStart = { legacy: { @@ -60,6 +70,17 @@ const createStartContractMock = () => { startContract.legacy.client.asScoped.mockReturnValue( legacyClientMock.createScopedClusterClient() ); + return startContract; +}; + +const createInternalStartContractMock = () => { + const startContract: MockedInternalElasticSearchServiceStart = { + ...createStartContractMock(), + client: elasticsearchClientMock.createClusterClient(), + createClient: jest.fn(), + }; + + startContract.createClient.mockReturnValue(elasticsearchClientMock.createCustomClusterClient()); return startContract; }; @@ -100,7 +121,7 @@ const createMock = () => { stop: jest.fn(), }; mocked.setup.mockResolvedValue(createInternalSetupContractMock()); - mocked.start.mockResolvedValueOnce(createStartContractMock()); + mocked.start.mockResolvedValueOnce(createInternalStartContractMock()); mocked.stop.mockResolvedValue(); return mocked; }; @@ -109,6 +130,7 @@ export const elasticsearchServiceMock = { create: createMock, createInternalSetup: createInternalSetupContractMock, createSetup: createSetupContractMock, + createInternalStart: createInternalStartContractMock, createStart: createStartContractMock, createLegacyClusterClient: legacyClientMock.createClusterClient, createLegacyCustomClusterClient: legacyClientMock.createCustomClusterClient, diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts b/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts index c30230a7847a0..955ab197ffce1 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.mocks.ts @@ -17,5 +17,8 @@ * under the License. */ +export const MockLegacyClusterClient = jest.fn(); +jest.mock('./legacy/cluster_client', () => ({ LegacyClusterClient: MockLegacyClusterClient })); + export const MockClusterClient = jest.fn(); -jest.mock('./legacy/cluster_client', () => ({ LegacyClusterClient: MockClusterClient })); +jest.mock('./client/cluster_client', () => ({ ClusterClient: MockClusterClient })); diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 8f3dc5688f6fc..b36af2a7e4671 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -19,7 +19,7 @@ import { first } from 'rxjs/operators'; -import { MockClusterClient } from './elasticsearch_service.test.mocks'; +import { MockLegacyClusterClient, MockClusterClient } from './elasticsearch_service.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { Env } from '../config'; @@ -28,9 +28,11 @@ import { CoreContext } from '../core_context'; import { configServiceMock } from '../config/config_service.mock'; import { loggingSystemMock } from '../logging/logging_system.mock'; import { httpServiceMock } from '../http/http_service.mock'; +import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; import { elasticsearchServiceMock } from './elasticsearch_service.mock'; +import { elasticsearchClientMock } from './client/mocks'; import { duration } from 'moment'; const delay = async (durationMs: number) => @@ -38,9 +40,12 @@ const delay = async (durationMs: number) => let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); -const deps = { +const setupDeps = { http: httpServiceMock.createInternalSetupContract(), }; +const startDeps = { + auditTrail: auditTrailServiceMock.createStartContract(), +}; configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], @@ -56,49 +61,58 @@ configService.atPath.mockReturnValue( let env: Env; let coreContext: CoreContext; const logger = loggingSystemMock.create(); + +let mockClusterClientInstance: ReturnType; +let mockLegacyClusterClientInstance: ReturnType; + beforeEach(() => { env = Env.createDefault(getEnvOptions()); coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; elasticsearchService = new ElasticsearchService(coreContext); + + MockLegacyClusterClient.mockClear(); + MockClusterClient.mockClear(); + + mockLegacyClusterClientInstance = elasticsearchServiceMock.createLegacyCustomClusterClient(); + MockLegacyClusterClient.mockImplementation(() => mockLegacyClusterClientInstance); + mockClusterClientInstance = elasticsearchClientMock.createCustomClusterClient(); + MockClusterClient.mockImplementation(() => mockClusterClientInstance); }); afterEach(() => jest.clearAllMocks()); describe('#setup', () => { it('returns legacy Elasticsearch config as a part of the contract', async () => { - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); await expect(setupContract.legacy.config$.pipe(first()).toPromise()).resolves.toBeInstanceOf( ElasticsearchConfig ); }); - it('returns elasticsearch client as a part of the contract', async () => { - const mockClusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); - MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - - const setupContract = await elasticsearchService.setup(deps); + it('returns legacy elasticsearch client as a part of the contract', async () => { + const setupContract = await elasticsearchService.setup(setupDeps); const client = setupContract.legacy.client; - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); await client.callAsInternalUser('any'); - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); }); - describe('#createClient', () => { + describe('#createLegacyClient', () => { it('allows to specify config properties', async () => { - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); - const mockClusterClientInstance = { close: jest.fn() }; - MockClusterClient.mockImplementation(() => mockClusterClientInstance); + // reset all mocks called during setup phase + MockLegacyClusterClient.mockClear(); const customConfig = { logQueries: true }; const clusterClient = setupContract.legacy.createClient('some-custom-type', customConfig); - expect(clusterClient).toBe(mockClusterClientInstance); + expect(clusterClient).toBe(mockLegacyClusterClientInstance); - expect(MockClusterClient).toHaveBeenCalledWith( + expect(MockLegacyClusterClient).toHaveBeenCalledWith( expect.objectContaining(customConfig), expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), expect.any(Function), @@ -107,9 +121,10 @@ describe('#setup', () => { }); it('falls back to elasticsearch default config values if property not specified', async () => { - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); + // reset all mocks called during setup phase - MockClusterClient.mockClear(); + MockLegacyClusterClient.mockClear(); const customConfig = { hosts: ['http://8.8.8.8'], @@ -118,7 +133,7 @@ describe('#setup', () => { }; setupContract.legacy.createClient('some-custom-type', customConfig); - const config = MockClusterClient.mock.calls[0][0]; + const config = MockLegacyClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { "healthCheckDelay": "PT0.01S", @@ -137,13 +152,14 @@ describe('#setup', () => { `); }); it('falls back to elasticsearch config if custom config not passed', async () => { - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); + // reset all mocks called during setup phase - MockClusterClient.mockClear(); + MockLegacyClusterClient.mockClear(); setupContract.legacy.createClient('another-type'); - const config = MockClusterClient.mock.calls[0][0]; + const config = MockLegacyClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { "healthCheckDelay": "PT0.01S", @@ -178,9 +194,10 @@ describe('#setup', () => { } as any) ); elasticsearchService = new ElasticsearchService(coreContext); - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); + // reset all mocks called during setup phase - MockClusterClient.mockClear(); + MockLegacyClusterClient.mockClear(); const customConfig = { hosts: ['http://8.8.8.8'], @@ -189,7 +206,7 @@ describe('#setup', () => { }; setupContract.legacy.createClient('some-custom-type', customConfig); - const config = MockClusterClient.mock.calls[0][0]; + const config = MockLegacyClusterClient.mock.calls[0][0]; expect(config).toMatchInlineSnapshot(` Object { "healthCheckDelay": "PT2S", @@ -210,66 +227,142 @@ describe('#setup', () => { }); it('esNodeVersionCompatibility$ only starts polling when subscribed to', async (done) => { - const clusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); - MockClusterClient.mockImplementationOnce(() => clusterClientInstance); + mockLegacyClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); - clusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); - - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); await delay(10); - expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); setupContract.esNodesCompatibility$.subscribe(() => { - expect(clusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); it('esNodeVersionCompatibility$ stops polling when unsubscribed from', async (done) => { - const mockClusterClientInstance = elasticsearchServiceMock.createLegacyClusterClient(); - MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - - mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockLegacyClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(0); const sub = setupContract.esNodesCompatibility$.subscribe(async () => { sub.unsubscribe(); await delay(100); - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); }); -describe('#stop', () => { - it('stops both admin and data clients', async () => { - const mockClusterClientInstance = { close: jest.fn() }; - MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); +describe('#start', () => { + it('throws if called before `setup`', async () => { + expect(() => elasticsearchService.start(startDeps)).rejects.toMatchInlineSnapshot( + `[Error: ElasticsearchService needs to be setup before calling start]` + ); + }); + + it('returns elasticsearch client as a part of the contract', async () => { + await elasticsearchService.setup(setupDeps); + const startContract = await elasticsearchService.start(startDeps); + const client = startContract.client; + + expect(client.asInternalUser).toBe(mockClusterClientInstance.asInternalUser); + }); + + describe('#createClient', () => { + it('allows to specify config properties', async () => { + await elasticsearchService.setup(setupDeps); + const startContract = await elasticsearchService.start(startDeps); + + // reset all mocks called during setup phase + MockClusterClient.mockClear(); + + const customConfig = { logQueries: true }; + const clusterClient = startContract.createClient('custom-type', customConfig); + + expect(clusterClient).toBe(mockClusterClientInstance); + + expect(MockClusterClient).toHaveBeenCalledTimes(1); + expect(MockClusterClient).toHaveBeenCalledWith( + expect.objectContaining(customConfig), + expect.objectContaining({ context: ['elasticsearch', 'custom-type'] }), + expect.any(Function) + ); + }); + it('creates a new client on each call', async () => { + await elasticsearchService.setup(setupDeps); + const startContract = await elasticsearchService.start(startDeps); + + // reset all mocks called during setup phase + MockClusterClient.mockClear(); + + const customConfig = { logQueries: true }; + + startContract.createClient('custom-type', customConfig); + startContract.createClient('another-type', customConfig); + + expect(MockClusterClient).toHaveBeenCalledTimes(2); + }); + + it('falls back to elasticsearch default config values if property not specified', async () => { + await elasticsearchService.setup(setupDeps); + const startContract = await elasticsearchService.start(startDeps); + + // reset all mocks called during setup phase + MockClusterClient.mockClear(); + + const customConfig = { + hosts: ['http://8.8.8.8'], + logQueries: true, + ssl: { certificate: 'certificate-value' }, + }; + + startContract.createClient('some-custom-type', customConfig); + const config = MockClusterClient.mock.calls[0][0]; - await elasticsearchService.setup(deps); + expect(config).toMatchInlineSnapshot(` + Object { + "healthCheckDelay": "PT0.01S", + "hosts": Array [ + "http://8.8.8.8", + ], + "logQueries": true, + "requestHeadersWhitelist": Array [ + undefined, + ], + "ssl": Object { + "certificate": "certificate-value", + "verificationMode": "none", + }, + } + `); + }); + }); +}); + +describe('#stop', () => { + it('stops both legacy and new clients', async () => { + await elasticsearchService.setup(setupDeps); + await elasticsearchService.start(startDeps); await elasticsearchService.stop(); + expect(mockLegacyClusterClientInstance.close).toHaveBeenCalledTimes(1); expect(mockClusterClientInstance.close).toHaveBeenCalledTimes(1); }); it('stops pollEsNodeVersions even if there are active subscriptions', async (done) => { expect.assertions(2); - const mockClusterClientInstance = elasticsearchServiceMock.createLegacyCustomClusterClient(); - - MockClusterClient.mockImplementationOnce(() => mockClusterClientInstance); - mockClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); + mockLegacyClusterClientInstance.callAsInternalUser.mockRejectedValue(new Error()); - const setupContract = await elasticsearchService.setup(deps); + const setupContract = await elasticsearchService.setup(setupDeps); setupContract.esNodesCompatibility$.subscribe(async () => { - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); await elasticsearchService.stop(); await delay(100); - expect(mockClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockLegacyClusterClientInstance.callAsInternalUser).toHaveBeenCalledTimes(1); done(); }); }); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index 4ea10f6ae4e2e..9b05fb9887a3b 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -17,17 +17,8 @@ * under the License. */ -import { ConnectableObservable, Observable, Subscription, Subject } from 'rxjs'; -import { - filter, - first, - map, - publishReplay, - switchMap, - take, - shareReplay, - takeUntil, -} from 'rxjs/operators'; +import { Observable, Subject } from 'rxjs'; +import { first, map, shareReplay, takeUntil } from 'rxjs/operators'; import { CoreService } from '../../types'; import { merge } from '../../utils'; @@ -35,28 +26,17 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { LegacyClusterClient, - ILegacyClusterClient, ILegacyCustomClusterClient, LegacyElasticsearchClientConfig, - LegacyCallAPIOptions, } from './legacy'; +import { ClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; import { InternalHttpServiceSetup, GetAuthHeaders } from '../http/'; import { AuditTrailStart, AuditorFactory } from '../audit_trail'; -import { - InternalElasticsearchServiceSetup, - ElasticsearchServiceStart, - ScopeableRequest, -} from './types'; +import { InternalElasticsearchServiceSetup, InternalElasticsearchServiceStart } from './types'; import { pollEsNodesVersion } from './version_check/ensure_es_version'; import { calculateStatus$ } from './status'; -/** @internal */ -interface CoreClusterClients { - config: ElasticsearchConfig; - client: LegacyClusterClient; -} - interface SetupDeps { http: InternalHttpServiceSetup; } @@ -67,18 +47,21 @@ interface StartDeps { /** @internal */ export class ElasticsearchService - implements CoreService { + implements CoreService { private readonly log: Logger; private readonly config$: Observable; - private subscription?: Subscription; private auditorFactory?: AuditorFactory; private stop$ = new Subject(); private kibanaVersion: string; - private createClient?: ( + private getAuthHeaders?: GetAuthHeaders; + + private createLegacyCustomClient?: ( type: string, clientConfig?: Partial ) => ILegacyCustomClusterClient; - private client?: ILegacyClusterClient; + private legacyClient?: LegacyClusterClient; + + private client?: ClusterClient; constructor(private readonly coreContext: CoreContext) { this.kibanaVersion = coreContext.env.packageInfo.version; @@ -91,139 +74,86 @@ export class ElasticsearchService public async setup(deps: SetupDeps): Promise { this.log.debug('Setting up elasticsearch service'); - const clients$ = this.config$.pipe( - filter(() => { - if (this.subscription !== undefined) { - this.log.error('Clients cannot be changed after they are created'); - return false; - } - - return true; - }), - switchMap( - (config) => - new Observable((subscriber) => { - this.log.debug('Creating elasticsearch client'); - - const coreClients = { - config, - client: this.createClusterClient('data', config, deps.http.getAuthHeaders), - }; - - subscriber.next(coreClients); - - return () => { - this.log.debug('Closing elasticsearch client'); - - coreClients.client.close(); - }; - }) - ), - publishReplay(1) - ) as ConnectableObservable; - - this.subscription = clients$.connect(); - const config = await this.config$.pipe(first()).toPromise(); - const client$ = clients$.pipe(map((clients) => clients.client)); - - const client = { - async callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) { - const _client = await client$.pipe(take(1)).toPromise(); - return await _client.callAsInternalUser(endpoint, clientParams, options); - }, - asScoped(request: ScopeableRequest) { - const _clientPromise = client$.pipe(take(1)).toPromise(); - return { - async callAsInternalUser( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) { - const _client = await _clientPromise; - return await _client - .asScoped(request) - .callAsInternalUser(endpoint, clientParams, options); - }, - async callAsCurrentUser( - endpoint: string, - clientParams: Record = {}, - options?: LegacyCallAPIOptions - ) { - const _client = await _clientPromise; - return await _client - .asScoped(request) - .callAsCurrentUser(endpoint, clientParams, options); - }, - }; - }, - }; - - this.client = client; + this.getAuthHeaders = deps.http.getAuthHeaders; + this.legacyClient = this.createLegacyClusterClient('data', config); const esNodesCompatibility$ = pollEsNodesVersion({ - callWithInternalUser: client.callAsInternalUser, + callWithInternalUser: this.legacyClient.callAsInternalUser, log: this.log, ignoreVersionMismatch: config.ignoreVersionMismatch, esVersionCheckInterval: config.healthCheckDelay.asMilliseconds(), kibanaVersion: this.kibanaVersion, }).pipe(takeUntil(this.stop$), shareReplay({ refCount: true, bufferSize: 1 })); - this.createClient = ( - type: string, - clientConfig: Partial = {} - ) => { + this.createLegacyCustomClient = (type, clientConfig = {}) => { const finalConfig = merge({}, config, clientConfig); - return this.createClusterClient(type, finalConfig, deps.http.getAuthHeaders); + return this.createLegacyClusterClient(type, finalConfig); }; return { legacy: { - config$: clients$.pipe(map((clients) => clients.config)), - client, - createClient: this.createClient, + config$: this.config$, + client: this.legacyClient, + createClient: this.createLegacyCustomClient, }, esNodesCompatibility$, status$: calculateStatus$(esNodesCompatibility$), }; } - public async start({ auditTrail }: StartDeps) { + public async start({ auditTrail }: StartDeps): Promise { this.auditorFactory = auditTrail; - if (typeof this.client === 'undefined' || typeof this.createClient === 'undefined') { + if (!this.legacyClient || !this.createLegacyCustomClient) { throw new Error('ElasticsearchService needs to be setup before calling start'); - } else { - return { - legacy: { - client: this.client, - createClient: this.createClient, - }, - }; } + + const config = await this.config$.pipe(first()).toPromise(); + this.client = this.createClusterClient('data', config); + + const createClient = ( + type: string, + clientConfig: Partial = {} + ): ICustomClusterClient => { + const finalConfig = merge({}, config, clientConfig); + return this.createClusterClient(type, finalConfig); + }; + + return { + client: this.client, + createClient, + legacy: { + client: this.legacyClient, + createClient: this.createLegacyCustomClient, + }, + }; } public async stop() { this.log.debug('Stopping elasticsearch service'); - if (this.subscription !== undefined) { - this.subscription.unsubscribe(); - } this.stop$.next(); + if (this.client) { + this.client.close(); + } + if (this.legacyClient) { + this.legacyClient.close(); + } } - private createClusterClient( - type: string, - config: LegacyElasticsearchClientConfig, - getAuthHeaders?: GetAuthHeaders - ) { + private createClusterClient(type: string, config: ElasticsearchClientConfig) { + return new ClusterClient( + config, + this.coreContext.logger.get('elasticsearch', type), + this.getAuthHeaders + ); + } + + private createLegacyClusterClient(type: string, config: LegacyElasticsearchClientConfig) { return new LegacyClusterClient( config, this.coreContext.logger.get('elasticsearch', type), this.getAuditorFactory, - getAuthHeaders + this.getAuthHeaders ); } diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index f5f5f5cc7b6f8..8bb77b5dfdee0 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -25,7 +25,15 @@ export { ElasticsearchServiceStart, ElasticsearchStatusMeta, InternalElasticsearchServiceSetup, + InternalElasticsearchServiceStart, FakeRequest, ScopeableRequest, } from './types'; export * from './legacy'; +export { + IClusterClient, + ICustomClusterClient, + ElasticsearchClientConfig, + ElasticsearchClient, + IScopedClusterClient, +} from './client'; diff --git a/src/core/server/elasticsearch/types.ts b/src/core/server/elasticsearch/types.ts index 2b4ba4b0a0a55..40399aecbc446 100644 --- a/src/core/server/elasticsearch/types.ts +++ b/src/core/server/elasticsearch/types.ts @@ -26,6 +26,7 @@ import { ILegacyClusterClient, ILegacyCustomClusterClient, } from './legacy'; +import { IClusterClient, ICustomClusterClient, ElasticsearchClientConfig } from './client'; import { NodesVersionCompatibility } from './version_check/ensure_es_version'; import { ServiceStatus } from '../status'; @@ -80,6 +81,16 @@ export interface ElasticsearchServiceSetup { }; } +/** @internal */ +export interface InternalElasticsearchServiceSetup { + // Required for the BWC with the legacy Kibana only. + readonly legacy: ElasticsearchServiceSetup['legacy'] & { + readonly config$: Observable; + }; + esNodesCompatibility$: Observable; + status$: Observable>; +} + /** * @public */ @@ -103,7 +114,7 @@ export interface ElasticsearchServiceStart { * * @example * ```js - * const client = elasticsearch.createCluster('my-app-name', config); + * const client = elasticsearch.legacy.createClient('my-app-name', config); * const data = await client.callAsInternalUser(); * ``` */ @@ -113,26 +124,51 @@ export interface ElasticsearchServiceStart { ) => ILegacyCustomClusterClient; /** - * A pre-configured Elasticsearch client. All Elasticsearch config value changes are processed under the hood. - * See {@link ILegacyClusterClient}. + * A pre-configured {@link ILegacyClusterClient | legacy Elasticsearch client}. * * @example * ```js - * const client = core.elasticsearch.client; + * const client = core.elasticsearch.legacy.client; * ``` */ readonly client: ILegacyClusterClient; }; } -/** @internal */ -export interface InternalElasticsearchServiceSetup { - // Required for the BWC with the legacy Kibana only. - readonly legacy: ElasticsearchServiceSetup['legacy'] & { - readonly config$: Observable; - }; - esNodesCompatibility$: Observable; - status$: Observable>; +/** + * @internal + */ +export interface InternalElasticsearchServiceStart extends ElasticsearchServiceStart { + /** + * A pre-configured {@link IClusterClient | Elasticsearch client} + * + * @example + * ```js + * const client = core.elasticsearch.client; + * ``` + */ + readonly client: IClusterClient; + /** + * Create application specific Elasticsearch cluster API client with customized config. See {@link IClusterClient}. + * + * @param type Unique identifier of the client + * @param clientConfig A config consists of Elasticsearch JS client options and + * valid sub-set of Elasticsearch service config. + * We fill all the missing properties in the `clientConfig` using the default + * Elasticsearch config so that we don't depend on default values set and + * controlled by underlying Elasticsearch JS client. + * We don't run validation against the passed config and expect it to be valid. + * + * @example + * ```js + * const client = elasticsearch.createClient('my-app-name', config); + * const data = await client.asInternalUser().search(); + * ``` + */ + readonly createClient: ( + type: string, + clientConfig?: Partial + ) => ICustomClusterClient; } /** @public */ diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index 3f562dac22a02..dc56d982d7b4a 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -29,7 +29,7 @@ import { esVersionEqualsKibana, } from './es_kibana_version_compatability'; import { Logger } from '../../logging'; -import { LegacyAPICaller } from '..'; +import { LegacyAPICaller } from '../legacy'; export interface PollEsNodesVersionOptions { callWithInternalUser: LegacyAPICaller; diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 879cbc689f8e7..cbab14115ba6b 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -19,6 +19,7 @@ import supertest from 'supertest'; import request from 'request'; +import { schema } from '@kbn/config-schema'; import { ensureRawRequest } from '../router'; import { HttpService } from '../http_service'; @@ -222,6 +223,39 @@ describe('OnPreAuth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerOnPreAuth((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('OnPostAuth', () => { @@ -356,6 +390,39 @@ describe('OnPostAuth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerOnPostAuth((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('Auth', () => { @@ -852,10 +919,43 @@ describe('Auth', () => { await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' }); }); + + it('has no access to request body', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + let requestBody = null; + registerAuth((req, res, t) => { + requestBody = req.body; + return t.authenticated({}); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); describe('OnPreResponse', () => { - it('supports registering response inceptors', async () => { + it('supports registering response interceptors', async () => { const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( setupDeps ); @@ -1001,4 +1101,39 @@ describe('OnPreResponse', () => { await supertest(innerServer.listener).get('/').expect(200); }); + + it('has no access to request body', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + let requestBody = null; + registerOnPreResponse((req, res, t) => { + requestBody = req.body; + return t.next(); + }); + + router.post( + { + path: '/', + validate: { + body: schema.object({ + term: schema.string(), + }), + }, + }, + (context, req, res) => res.ok({ body: req.body.term }) + ); + + await server.start(); + + await supertest(innerServer.listener) + .post('/') + .send({ + term: 'foo', + }) + .expect(200, 'foo'); + + expect(requestBody).toStrictEqual({}); + }); }); diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 24080f2529beb..4f4bf50f07b8e 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -22,7 +22,10 @@ import { Type } from '@kbn/config-schema'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { ConfigDeprecationProvider } from './config'; import { ContextSetup } from './context'; -import { InternalElasticsearchServiceSetup, ElasticsearchServiceStart } from './elasticsearch'; +import { + InternalElasticsearchServiceSetup, + InternalElasticsearchServiceStart, +} from './elasticsearch'; import { InternalHttpServiceSetup, InternalHttpServiceStart } from './http'; import { InternalSavedObjectsServiceSetup, @@ -58,7 +61,7 @@ export interface InternalCoreSetup { */ export interface InternalCoreStart { capabilities: CapabilitiesStart; - elasticsearch: ElasticsearchServiceStart; + elasticsearch: InternalElasticsearchServiceStart; http: InternalHttpServiceStart; metrics: InternalMetricsServiceStart; savedObjects: InternalSavedObjectsServiceStart; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 75ca88627814b..a3dbb279d19eb 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -177,7 +177,7 @@ function createInternalCoreSetupMock() { function createInternalCoreStartMock() { const startDeps: InternalCoreStart = { capabilities: capabilitiesServiceMock.createStartContract(), - elasticsearch: elasticsearchServiceMock.createStart(), + elasticsearch: elasticsearchServiceMock.createInternalStart(), http: httpServiceMock.createInternalStartContract(), metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index b0f9ff6fd5ebd..a6dd13a12b527 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -210,7 +210,9 @@ export function createPluginStartContext( capabilities: { resolveCapabilities: deps.capabilities.resolveCapabilities, }, - elasticsearch: deps.elasticsearch, + elasticsearch: { + legacy: deps.elasticsearch.legacy, + }, http: { auth: deps.http.auth, basePath: deps.http.basePath, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ea95329bf8fa4..107edf11bb6f4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -4,6 +4,7 @@ ```ts +import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; import { BulkIndexDocumentsParams } from 'elasticsearch'; import { CatAliasesParams } from 'elasticsearch'; @@ -21,6 +22,8 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; +import { Client as Client_2 } from '@elastic/elasticsearch'; +import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; import { ClusterHealthParams } from 'elasticsearch'; @@ -138,6 +141,8 @@ import { TasksCancelParams } from 'elasticsearch'; import { TasksGetParams } from 'elasticsearch'; import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; +import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh index 3832ec9b4076a..915759d4214f9 100755 --- a/src/dev/ci_setup/checkout_sibling_es.sh +++ b/src/dev/ci_setup/checkout_sibling_es.sh @@ -7,11 +7,10 @@ function checkout_sibling { targetDir=$2 useExistingParamName=$3 useExisting="$(eval "echo "\$$useExistingParamName"")" - repoAddress="https://github.com/" if [ -z ${useExisting:+x} ]; then if [ -d "$targetDir" ]; then - echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$WORKSPACE]!" + echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!" echo echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling." exit 1 @@ -22,9 +21,8 @@ function checkout_sibling { cloneBranch="" function clone_target_is_valid { - echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}" - if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then + if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then return 0 else return 1 @@ -73,7 +71,7 @@ function checkout_sibling { fi echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..." - git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1 + git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1 echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)" echo } @@ -89,12 +87,12 @@ function checkout_sibling { fi } -checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES" +checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES" export TEST_ES_FROM=${TEST_ES_FROM:-snapshot} # Set the JAVA_HOME based on the Java property file in the ES repo # This assumes the naming convention used on CI (ex: ~/.java/java10) -ES_DIR="$WORKSPACE/elasticsearch" +ES_DIR="$PARENT_DIR/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index f96a2240917e2..343ff47199375 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -53,8 +53,6 @@ export PARENT_DIR="$parentDir" kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")" export KIBANA_PKG_BRANCH="$kbnBranch" -export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" - ### ### download node ### @@ -163,7 +161,7 @@ export -f checks-reporter-with-killswitch source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh" -ES_DIR="$WORKSPACE/elasticsearch" +ES_DIR="$PARENT_DIR/elasticsearch" ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index a2b05c6dc8a4e..fb74bed0f26f4 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -49,10 +49,8 @@ export async function generateNoticeFromSource({ productName, directory, log }: ignore: [ '{node_modules,build,target,dist,data,built_assets}/**', 'packages/*/{node_modules,build,target,dist}/**', - 'src/plugins/*/{node_modules,build,target,dist}/**', 'x-pack/{node_modules,build,target,dist,data}/**', 'x-pack/packages/*/{node_modules,build,target,dist}/**', - 'x-pack/plugins/*/{node_modules,build,target,dist}/**', ], }; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 85bfd4a7a4d26..9d9f5616b5a33 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -26,4 +26,5 @@ export const storybookAliases = { infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js', ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js', + observability: 'x-pack/plugins/observability/scripts/storybook.js', }; diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts index d70a247c61e70..d58a89db97d74 100644 --- a/src/fixtures/telemetry_collectors/working_collector.ts +++ b/src/fixtures/telemetry_collectors/working_collector.ts @@ -33,6 +33,8 @@ interface Usage { flat?: string; my_str?: string; my_objects: MyObject; + my_array?: MyObject[]; + my_str_array?: string[]; } const SOME_NUMBER: number = 123; @@ -54,6 +56,13 @@ export const myCollector = makeUsageCollector({ total: SOME_NUMBER, type: true, }, + my_array: [ + { + total: SOME_NUMBER, + type: true, + }, + ], + my_str_array: ['hello', 'world'], }; } catch (err) { return { @@ -77,5 +86,12 @@ export const myCollector = makeUsageCollector({ }, type: { type: 'boolean' }, }, + my_array: { + total: { + type: 'number', + }, + type: { type: 'boolean' }, + }, + my_str_array: { type: 'keyword' }, }, }); diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 8ec72dc1f9a74..22db1552e4303 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -44,7 +44,8 @@ export const UI_SETTINGS = { FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale', TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults', TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges', + TIMEPICKER_TIME_DEFAULTS: 'timepicker:timeDefaults', INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', -}; +} as const; diff --git a/src/plugins/data/common/search/aggs/index.ts b/src/plugins/data/common/search/aggs/index.ts index 09ea958ccaa85..7a5b7d509c940 100644 --- a/src/plugins/data/common/search/aggs/index.ts +++ b/src/plugins/data/common/search/aggs/index.ts @@ -18,3 +18,4 @@ */ export * from './date_interval_utils'; +export * from './ipv4_address'; diff --git a/src/plugins/kibana_utils/public/parse/ipv4_address.test.ts b/src/plugins/data/common/search/aggs/ipv4_address.test.ts similarity index 99% rename from src/plugins/kibana_utils/public/parse/ipv4_address.test.ts rename to src/plugins/data/common/search/aggs/ipv4_address.test.ts index 1318e11ca6f07..15082b750016e 100644 --- a/src/plugins/kibana_utils/public/parse/ipv4_address.test.ts +++ b/src/plugins/data/common/search/aggs/ipv4_address.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -// @ts-ignore + import expect from '@kbn/expect'; import { Ipv4Address } from './ipv4_address'; diff --git a/src/plugins/kibana_utils/public/parse/ipv4_address.ts b/src/plugins/data/common/search/aggs/ipv4_address.ts similarity index 97% rename from src/plugins/kibana_utils/public/parse/ipv4_address.ts rename to src/plugins/data/common/search/aggs/ipv4_address.ts index d214fb6e0e158..da555d8c422ce 100644 --- a/src/plugins/kibana_utils/public/parse/ipv4_address.ts +++ b/src/plugins/data/common/search/aggs/ipv4_address.ts @@ -30,7 +30,6 @@ function isIntegerInRange(integer: number, min: number, max: number) { ); } -// eslint-disable-next-line import/no-default-export export class Ipv4Address { private value: number; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index 5d21b395b994f..aa96d77d873d3 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -23,8 +23,7 @@ import { createFiltersFromRangeSelectAction } from './create_filters_from_range_ import { IndexPatternsContract, RangeFilter } from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { setIndexPatterns, setSearchService } from '../../../public/services'; import { TriggerContextMapping } from '../../../../ui_actions/public'; describe('brushEvent', () => { @@ -51,9 +50,10 @@ describe('brushEvent', () => { ]; beforeEach(() => { - mockDataServices(); + const dataStart = dataPluginMock.createStartContract(); + setSearchService(dataStart.search); setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, + ...dataStart.indexPatterns, get: async () => indexPattern, } as unknown) as IndexPatternsContract); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 3e38477a908b8..a3b9b0b344823 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -24,8 +24,7 @@ import { IndexPatternsContract, } from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { setIndexPatterns, setSearchService } from '../../../public/services'; import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; import { ValueClickContext } from '../../../../embeddable/public'; @@ -72,9 +71,10 @@ describe('createFiltersFromValueClick', () => { }, ]; - mockDataServices(); + const dataStart = dataPluginMock.createStartContract(); + setSearchService(dataStart.search); setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, + ...dataStart.indexPatterns, get: async () => ({ id: 'logstash-*', fields: { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 84ee65ed12e3e..abec908b41c0f 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -306,6 +306,7 @@ import { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, + Ipv4Address, isValidEsInterval, isValidInterval, parseEsInterval, @@ -373,6 +374,7 @@ export const search = { intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, + Ipv4Address, isDateHistogramBucketAggConfig, // TODO: remove in build_pipeline refactor isNumberType, isStringType, @@ -417,7 +419,6 @@ export { connectToQueryState, syncQueryStateWithUrl, QueryState, - getQueryLog, getDefaultQuery, FilterManager, SavedQuery, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index ec71794fde87d..4040781bb2f01 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -188,7 +188,11 @@ export class DataPublicPlugin implements Plugin({ timefilter: { timefil // Warning: (ae-missing-release-tag) "createSavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract_3) => SavedQueryService; +export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract) => SavedQueryService; // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -750,12 +751,6 @@ export const getIndexPatternFieldListCreator: ({ fieldFormats, onNotification, } // @public export const getKbnTypeNames: () => string[]; -// Warning: (ae-forgotten-export) The symbol "PersistedLog" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "getQueryLog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function getQueryLog(uiSettings: IUiSettingsClient, storage: IStorageWrapper, appName: string, language: string): PersistedLog; - // Warning: (ae-missing-release-tag) "getSearchErrorType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1486,7 +1481,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1675,6 +1670,7 @@ export const search: { })[]; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; + Ipv4Address: typeof Ipv4Address; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; isNumberType: (agg: import("./search").AggConfig) => boolean; isStringType: (agg: import("./search").AggConfig) => boolean; @@ -1910,33 +1906,34 @@ export interface TimeRange { // // @public (undocumented) export const UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; }; @@ -1979,20 +1976,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:369:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:372:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:381:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:370:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:370:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:370:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:370:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:372:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:40:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts b/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts new file mode 100644 index 0000000000000..c95a943be7713 --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/filter_manager.mock.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { FilterManager } from './filter_manager'; + +export const createFilterManagerMock = () => { + const filterManager = ({ + mergeIncomingFilters: jest.fn(), + handleStateUpdate: jest.fn(), + getFilters: jest.fn(), + getAppFilters: jest.fn(), + getGlobalFilters: jest.fn(), + getPartitionedFilters: jest.fn(), + getUpdates$: jest.fn(() => new Observable()), + getFetches$: jest.fn(() => new Observable()), + addFilters: jest.fn(), + setFilters: jest.fn(), + setGlobalFilters: jest.fn(), + setAppFilters: jest.fn(), + removeFilter: jest.fn(), + removeAll: jest.fn(), + } as unknown) as jest.Mocked; + + return filterManager; +}; diff --git a/src/plugins/data/public/query/lib/add_to_query_log.ts b/src/plugins/data/public/query/lib/add_to_query_log.ts new file mode 100644 index 0000000000000..d1e9a800418a2 --- /dev/null +++ b/src/plugins/data/public/query/lib/add_to_query_log.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IUiSettingsClient } from 'src/core/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { Query } from '../../../common'; +import { getQueryLog } from './get_query_log'; + +interface AddToQueryLogDependencies { + uiSettings: IUiSettingsClient; + storage: IStorageWrapper; +} + +export function createAddToQueryLog({ storage, uiSettings }: AddToQueryLogDependencies) { + /** + * This function is to be used in conjunction with ``. + * It provides a way for external editors to add new filter entries to the + * persisted query log which lives in `localStorage`. These entries are then + * read by `` and provided in the autocomplete options. + * + * @param appName Name of the app where this filter is added from. + * @param query Filter value to add. + */ + return function addToQueryLog(appName: string, { language, query }: Query) { + const persistedLog = getQueryLog(uiSettings, storage, appName, language); + persistedLog.add(query); + }; +} diff --git a/src/plugins/data/public/query/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts index b7827d2c8de02..8256c3eabd7d1 100644 --- a/src/plugins/data/public/query/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -22,6 +22,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { PersistedLog } from '../persisted_log'; import { UI_SETTINGS } from '../../../common'; +/** @internal */ export function getQueryLog( uiSettings: IUiSettingsClient, storage: IStorageWrapper, diff --git a/src/plugins/data/public/query/lib/index.ts b/src/plugins/data/public/query/lib/index.ts index 284fb6458a535..a0625902fea7b 100644 --- a/src/plugins/data/public/query/lib/index.ts +++ b/src/plugins/data/public/query/lib/index.ts @@ -17,9 +17,10 @@ * under the License. */ -export * from './match_pairs'; +export * from './add_to_query_log'; export * from './from_user'; +export * from './get_default_query'; +export * from './get_query_log'; +export * from './match_pairs'; export * from './to_user'; export * from './to_user'; -export * from './get_query_log'; -export * from './get_default_query'; diff --git a/src/plugins/data/public/query/mocks.ts b/src/plugins/data/public/query/mocks.ts index 47b0a5b871ce2..41896107bb868 100644 --- a/src/plugins/data/public/query/mocks.ts +++ b/src/plugins/data/public/query/mocks.ts @@ -20,12 +20,13 @@ import { Observable } from 'rxjs'; import { QueryService, QuerySetup, QueryStart } from '.'; import { timefilterServiceMock } from './timefilter/timefilter_service.mock'; +import { createFilterManagerMock } from './filter_manager/filter_manager.mock'; type QueryServiceClientContract = PublicMethodsOf; const createSetupContractMock = () => { const setupContract: jest.Mocked = { - filterManager: jest.fn() as any, + filterManager: createFilterManagerMock(), timefilter: timefilterServiceMock.createSetupContract(), state$: new Observable(), }; @@ -35,10 +36,11 @@ const createSetupContractMock = () => { const createStartContractMock = () => { const startContract: jest.Mocked = { - filterManager: jest.fn() as any, - timefilter: timefilterServiceMock.createStartContract(), + addToQueryLog: jest.fn(), + filterManager: createFilterManagerMock(), savedQueries: jest.fn() as any, state$: new Observable(), + timefilter: timefilterServiceMock.createStartContract(), }; return startContract; diff --git a/src/plugins/data/public/query/query_service.ts b/src/plugins/data/public/query/query_service.ts index c885d596f1943..eb1f985fa51db 100644 --- a/src/plugins/data/public/query/query_service.ts +++ b/src/plugins/data/public/query/query_service.ts @@ -18,9 +18,10 @@ */ import { share } from 'rxjs/operators'; -import { CoreStart } from 'src/core/public'; +import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { FilterManager } from './filter_manager'; +import { createAddToQueryLog } from './lib'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { createSavedQueryService } from './saved_query/saved_query_service'; import { createQueryStateObservable } from './state_sync/create_global_query_observable'; @@ -30,17 +31,24 @@ import { createQueryStateObservable } from './state_sync/create_global_query_obs * @internal */ -export interface QueryServiceDependencies { +interface QueryServiceSetupDependencies { storage: IStorageWrapper; - uiSettings: CoreStart['uiSettings']; + uiSettings: IUiSettingsClient; } + +interface QueryServiceStartDependencies { + savedObjectsClient: SavedObjectsClientContract; + storage: IStorageWrapper; + uiSettings: IUiSettingsClient; +} + export class QueryService { filterManager!: FilterManager; timefilter!: TimefilterSetup; state$!: ReturnType; - public setup({ uiSettings, storage }: QueryServiceDependencies) { + public setup({ storage, uiSettings }: QueryServiceSetupDependencies) { this.filterManager = new FilterManager(uiSettings); const timefilterService = new TimefilterService(); @@ -61,12 +69,16 @@ export class QueryService { }; } - public start(savedObjects: CoreStart['savedObjects']) { + public start({ savedObjectsClient, storage, uiSettings }: QueryServiceStartDependencies) { return { + addToQueryLog: createAddToQueryLog({ + storage, + uiSettings, + }), filterManager: this.filterManager, - timefilter: this.timefilter, + savedQueries: createSavedQueryService(savedObjectsClient), state$: this.state$, - savedQueries: createSavedQueryService(savedObjects.client), + timefilter: this.timefilter, }; } diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 4e394445b75ae..cf98c87b18267 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -78,7 +78,11 @@ describe('connect_to_global_state', () => { uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), }); - queryServiceStart = queryService.start(startMock.savedObjects); + queryServiceStart = queryService.start({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + savedObjectsClient: startMock.savedObjects.client, + }); filterManager = queryServiceStart.filterManager; timeFilter = queryServiceStart.timefilter.timefilter; @@ -307,7 +311,11 @@ describe('connect_to_app_state', () => { uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), }); - queryServiceStart = queryService.start(startMock.savedObjects); + queryServiceStart = queryService.start({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + savedObjectsClient: startMock.savedObjects.client, + }); filterManager = queryServiceStart.filterManager; appState = createStateContainer({}); @@ -481,7 +489,11 @@ describe('filters with different state', () => { uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), }); - queryServiceStart = queryService.start(startMock.savedObjects); + queryServiceStart = queryService.start({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + savedObjectsClient: startMock.savedObjects.client, + }); filterManager = queryServiceStart.filterManager; state = createStateContainer({}); diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts index 7727153537257..122eb2ff6a343 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts @@ -72,7 +72,11 @@ describe('sync_query_state_with_url', () => { uiSettings: setupMock.uiSettings, storage: new Storage(new StubBrowserStorage()), }); - queryServiceStart = queryService.start(startMock.savedObjects); + queryServiceStart = queryService.start({ + uiSettings: startMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + savedObjectsClient: startMock.savedObjects.client, + }); filterManager = queryServiceStart.filterManager; timefilter = queryServiceStart.timefilter.timefilter; diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 18a86d767c2de..f9279e06d14a9 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -23,19 +23,50 @@ import { AggConfig, IAggConfig } from './agg_config'; import { AggConfigs, CreateAggConfigParams } from './agg_configs'; import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; -import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; import { IndexPattern, IIndexPatternFieldList } from '../../index_patterns'; -import { stubIndexPatternWithFields } from '../../../public/stubs'; describe('AggConfig', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; + const fields = [ + { + name: '@timestamp', + type: 'date', + aggregatable: true, + format: { + toJSON: () => ({}), + }, + }, + { + name: 'bytes', + type: 'number', + aggregatable: true, + format: { + toJSON: () => ({}), + }, + }, + { + name: 'machine.os.keyword', + type: 'string', + aggregatable: true, + format: { + toJSON: () => ({}), + }, + }, + ]; beforeEach(() => { jest.restoreAllMocks(); - mockDataServices(); - indexPattern = stubIndexPatternWithFields as IndexPattern; + indexPattern = { + id: '1234', + title: 'logstash-*', + fields: ({ + getByName: (name: string) => fields.find((f) => f.name === name), + filter: () => fields, + } as unknown) as IndexPattern['fields'], + } as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -488,6 +519,9 @@ describe('AggConfig', () => { "enabled": Array [ true, ], + "field": Array [ + "machine.os.keyword", + ], "id": Array [ "1", ], @@ -548,6 +582,9 @@ describe('AggConfig', () => { "enabled": Array [ true, ], + "field": Array [ + "bytes", + ], "id": Array [ "1-orderAgg", ], diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index de49e9ab6f66e..31618eac18e98 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -24,7 +24,7 @@ import { ExpressionAstFunction, ExpressionAstArgument, SerializedFieldFormat, -} from 'src/plugins/expressions/public'; +} from 'src/plugins/expressions/common'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; diff --git a/src/plugins/data/public/search/aggs/agg_configs.test.ts b/src/plugins/data/public/search/aggs/agg_configs.test.ts index f3efeb028665b..df4a5420ae0db 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.test.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.test.ts @@ -21,7 +21,7 @@ import { keyBy } from 'lodash'; import { AggConfig } from './agg_config'; import { AggConfigs } from './agg_configs'; import { AggTypesRegistryStart } from './agg_types_registry'; -import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { mockAggTypesRegistry } from './test_helpers'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPattern, stubIndexPatternWithFields } from '../../../public/stubs'; @@ -30,7 +30,6 @@ describe('AggConfigs', () => { let typesRegistry: AggTypesRegistryStart; beforeEach(() => { - mockDataServices(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); diff --git a/src/plugins/data/public/search/aggs/agg_params.test.ts b/src/plugins/data/public/search/aggs/agg_params.test.ts index 997027fe88e82..ac3f86d5128a1 100644 --- a/src/plugins/data/public/search/aggs/agg_params.test.ts +++ b/src/plugins/data/public/search/aggs/agg_params.test.ts @@ -22,24 +22,12 @@ import { BaseParamType } from './param_types/base'; import { FieldParamType } from './param_types/field'; import { OptionedParamType } from './param_types/optioned'; import { AggParamType } from '../aggs/param_types/agg'; -import { fieldFormatsServiceMock } from '../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; -import { AggTypeDependencies } from './agg_type'; -import { InternalStartServices } from '../../types'; describe('AggParams class', () => { - const aggTypesDependencies: AggTypeDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - describe('constructor args', () => { it('accepts an array of param defs', () => { const params = [{ name: 'one' }, { name: 'two' }] as AggParamType[]; - const aggParams = initParams(params, aggTypesDependencies); + const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); expect(Array.isArray(aggParams)).toBeTruthy(); @@ -49,7 +37,7 @@ describe('AggParams class', () => { describe('AggParam creation', () => { it('Uses the FieldParamType class for params with the name "field"', () => { const params = [{ name: 'field', type: 'field' }] as AggParamType[]; - const aggParams = initParams(params, aggTypesDependencies); + const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); expect(aggParams[0] instanceof FieldParamType).toBeTruthy(); @@ -62,7 +50,7 @@ describe('AggParams class', () => { type: 'optioned', }, ] as AggParamType[]; - const aggParams = initParams(params, aggTypesDependencies); + const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); expect(aggParams[0] instanceof OptionedParamType).toBeTruthy(); @@ -84,7 +72,7 @@ describe('AggParams class', () => { }, ] as AggParamType[]; - const aggParams = initParams(params, aggTypesDependencies); + const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); diff --git a/src/plugins/data/public/search/aggs/agg_params.ts b/src/plugins/data/public/search/aggs/agg_params.ts index 4f7bec30fa27d..b6222698a12a6 100644 --- a/src/plugins/data/public/search/aggs/agg_params.ts +++ b/src/plugins/data/public/search/aggs/agg_params.ts @@ -26,7 +26,6 @@ import { BaseParamType } from './param_types/base'; import { AggConfig } from './agg_config'; import { IAggConfigs } from './agg_configs'; -import { AggTypeDependencies } from './agg_type'; const paramTypeMap = { field: FieldParamType, @@ -46,13 +45,12 @@ export interface AggParamOption { } export const initParams = ( - params: TAggParam[], - { getInternalStartServices }: AggTypeDependencies + params: TAggParam[] ): TAggParam[] => params.map((config: TAggParam) => { const Class = paramTypeMap[config.type] || paramTypeMap._default; - return new Class(config, { getInternalStartServices }); + return new Class(config); }) as TAggParam[]; /** diff --git a/src/plugins/data/public/search/aggs/agg_type.test.ts b/src/plugins/data/public/search/aggs/agg_type.test.ts index 99b1c4d861219..2fcc6b97b1cc6 100644 --- a/src/plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/plugins/data/public/search/aggs/agg_type.test.ts @@ -17,33 +17,15 @@ * under the License. */ -import { AggType, AggTypeConfig, AggTypeDependencies } from './agg_type'; +import { AggType, AggTypeConfig } from './agg_type'; import { IAggConfig } from './agg_config'; -import { fieldFormatsServiceMock } from '../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../types'; describe('AggType Class', () => { - let dependencies: AggTypeDependencies; - - beforeEach(() => { - dependencies = { - getInternalStartServices: () => - (({ - fieldFormats: { - ...fieldFormatsServiceMock.createStartContract(), - getDefaultInstance: jest.fn(() => 'default') as any, - }, - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - }); - describe('constructor', () => { test("requires a valid config object as it's first param", () => { expect(() => { const aggConfig: AggTypeConfig = (undefined as unknown) as AggTypeConfig; - new AggType(aggConfig, dependencies); + new AggType(aggConfig); }).toThrowError(); }); @@ -54,7 +36,7 @@ describe('AggType Class', () => { title: 'title', }; - const aggType = new AggType(config, dependencies); + const aggType = new AggType(config); expect(aggType.name).toBe('name'); expect(aggType.title).toBe('title'); @@ -70,7 +52,7 @@ describe('AggType Class', () => { makeLabel, }; - const aggType = new AggType(config, dependencies); + const aggType = new AggType(config); expect(aggType.makeLabel).toBe(makeLabel); expect(aggType.makeLabel(aggConfig)).toBe('label'); @@ -81,15 +63,12 @@ describe('AggType Class', () => { test('copies the value', () => { const testConfig = (aggConfig: IAggConfig) => [aggConfig]; - const aggType = new AggType( - { - name: 'name', - title: 'title', - getResponseAggs: testConfig, - getRequestAggs: testConfig, - }, - dependencies - ); + const aggType = new AggType({ + name: 'name', + title: 'title', + getResponseAggs: testConfig, + getRequestAggs: testConfig, + }); expect(aggType.getResponseAggs).toBe(testConfig); expect(aggType.getResponseAggs).toBe(testConfig); @@ -97,13 +76,10 @@ describe('AggType Class', () => { test('defaults to noop', () => { const aggConfig = {} as IAggConfig; - const aggType = new AggType( - { - name: 'name', - title: 'title', - }, - dependencies - ); + const aggType = new AggType({ + name: 'name', + title: 'title', + }); const responseAggs = aggType.getRequestAggs(aggConfig); expect(responseAggs).toBe(undefined); @@ -112,13 +88,10 @@ describe('AggType Class', () => { describe('params', () => { test('defaults to AggParams object with JSON param', () => { - const aggType = new AggType( - { - name: 'smart agg', - title: 'title', - }, - dependencies - ); + const aggType = new AggType({ + name: 'smart agg', + title: 'title', + }); expect(Array.isArray(aggType.params)).toBeTruthy(); expect(aggType.params.length).toBe(2); @@ -127,14 +100,11 @@ describe('AggType Class', () => { }); test('can disable customLabel', () => { - const aggType = new AggType( - { - name: 'smart agg', - title: 'title', - customLabels: false, - }, - dependencies - ); + const aggType = new AggType({ + name: 'smart agg', + title: 'title', + customLabels: false, + }); expect(aggType.params.length).toBe(1); expect(aggType.params[0].name).toBe('json'); @@ -144,14 +114,11 @@ describe('AggType Class', () => { const params = [{ name: 'one' }, { name: 'two' }]; const paramLength = params.length + 2; // json and custom label are always appended - const aggType = new AggType( - { - name: 'bucketeer', - title: 'title', - params, - }, - dependencies - ); + const aggType = new AggType({ + name: 'bucketeer', + title: 'title', + params, + }); expect(Array.isArray(aggType.params)).toBeTruthy(); expect(aggType.params.length).toBe(paramLength); @@ -170,13 +137,10 @@ describe('AggType Class', () => { }, }, } as unknown) as IAggConfig; - const aggType = new AggType( - { - name: 'name', - title: 'title', - }, - dependencies - ); + const aggType = new AggType({ + name: 'name', + title: 'title', + }); expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(` Object { "id": "format", @@ -188,13 +152,10 @@ describe('AggType Class', () => { const aggConfig = ({ params: {}, } as unknown) as IAggConfig; - const aggType = new AggType( - { - name: 'name', - title: 'title', - }, - dependencies - ); + const aggType = new AggType({ + name: 'name', + title: 'title', + }); expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(`Object {}`); }); @@ -209,14 +170,11 @@ describe('AggType Class', () => { }, } as unknown) as IAggConfig; const getSerializedFormat = jest.fn().mockReturnValue({ id: 'hello' }); - const aggType = new AggType( - { - name: 'name', - title: 'title', - getSerializedFormat, - }, - dependencies - ); + const aggType = new AggType({ + name: 'name', + title: 'title', + getSerializedFormat, + }); const serialized = aggType.getSerializedFormat(aggConfig); expect(getSerializedFormat).toHaveBeenCalledWith(aggConfig); expect(serialized).toMatchInlineSnapshot(` diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index 13655afdf469e..de7ca48e71d57 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -20,16 +20,15 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SerializedFieldFormat } from 'src/plugins/expressions/public'; +import { SerializedFieldFormat } from 'src/plugins/expressions/common'; +import type { RequestAdapter } from 'src/plugins/inspector/common'; import { initParams } from './agg_params'; import { AggConfig } from './agg_config'; import { IAggConfigs } from './agg_configs'; -import { Adapters } from '../../../../../plugins/inspector/public'; import { BaseParamType } from './param_types/base'; import { AggParamType } from './param_types/agg'; import { ISearchSource } from '../search_source'; -import { GetInternalStartServicesFn } from '../../types'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, @@ -54,7 +53,7 @@ export interface AggTypeConfig< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorAdapters: Adapters, + inspectorRequestAdapter: RequestAdapter, abortSignal?: AbortSignal ) => Promise; getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; @@ -65,10 +64,6 @@ export interface AggTypeConfig< // TODO need to make a more explicit interface for this export type IAggType = AggType; -export interface AggTypeDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export class AggType< TAggConfig extends AggConfig = AggConfig, TParam extends AggParamType = AggParamType @@ -192,7 +187,7 @@ export class AggType< aggConfigs: IAggConfigs, aggConfig: TAggConfig, searchSource: ISearchSource, - inspectorAdapters: Adapters, + inspectorRequestAdapter: RequestAdapter, abortSignal?: AbortSignal ) => Promise; /** @@ -223,10 +218,7 @@ export class AggType< * @private * @param {object} config - used to set the properties of the AggType */ - constructor( - config: AggTypeConfig, - { getInternalStartServices }: AggTypeDependencies - ) { + constructor(config: AggTypeConfig) { this.name = config.name; this.type = config.type || 'metrics'; this.dslName = config.dslName || config.name; @@ -263,7 +255,7 @@ export class AggType< }); } - this.params = initParams(params, { getInternalStartServices }); + this.params = initParams(params); } this.getRequestAggs = config.getRequestAggs || noop; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 68542b66e6c35..2820ae495f318 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -67,40 +67,40 @@ export const getAggTypes = ({ uiSettings, }: AggTypesDependencies) => ({ metrics: [ - getCountMetricAgg({ getInternalStartServices }), - getAvgMetricAgg({ getInternalStartServices }), - getSumMetricAgg({ getInternalStartServices }), - getMedianMetricAgg({ getInternalStartServices }), - getMinMetricAgg({ getInternalStartServices }), - getMaxMetricAgg({ getInternalStartServices }), - getStdDeviationMetricAgg({ getInternalStartServices }), - getCardinalityMetricAgg({ getInternalStartServices }), - getPercentilesMetricAgg({ getInternalStartServices }), + getCountMetricAgg(), + getAvgMetricAgg(), + getSumMetricAgg(), + getMedianMetricAgg(), + getMinMetricAgg(), + getMaxMetricAgg(), + getStdDeviationMetricAgg(), + getCardinalityMetricAgg(), + getPercentilesMetricAgg(), getPercentileRanksMetricAgg({ getInternalStartServices }), - getTopHitMetricAgg({ getInternalStartServices }), - getDerivativeMetricAgg({ getInternalStartServices }), - getCumulativeSumMetricAgg({ getInternalStartServices }), - getMovingAvgMetricAgg({ getInternalStartServices }), - getSerialDiffMetricAgg({ getInternalStartServices }), - getBucketAvgMetricAgg({ getInternalStartServices }), - getBucketSumMetricAgg({ getInternalStartServices }), - getBucketMinMetricAgg({ getInternalStartServices }), - getBucketMaxMetricAgg({ getInternalStartServices }), - getGeoBoundsMetricAgg({ getInternalStartServices }), - getGeoCentroidMetricAgg({ getInternalStartServices }), + getTopHitMetricAgg(), + getDerivativeMetricAgg(), + getCumulativeSumMetricAgg(), + getMovingAvgMetricAgg(), + getSerialDiffMetricAgg(), + getBucketAvgMetricAgg(), + getBucketSumMetricAgg(), + getBucketMinMetricAgg(), + getBucketMaxMetricAgg(), + getGeoBoundsMetricAgg(), + getGeoCentroidMetricAgg(), ], buckets: [ - getDateHistogramBucketAgg({ calculateBounds, uiSettings, getInternalStartServices }), + getDateHistogramBucketAgg({ calculateBounds, uiSettings }), getHistogramBucketAgg({ uiSettings, getInternalStartServices }), getRangeBucketAgg({ getInternalStartServices }), - getDateRangeBucketAgg({ uiSettings, getInternalStartServices }), - getIpRangeBucketAgg({ getInternalStartServices }), - getTermsBucketAgg({ getInternalStartServices }), - getFilterBucketAgg({ getInternalStartServices }), - getFiltersBucketAgg({ uiSettings, getInternalStartServices }), - getSignificantTermsBucketAgg({ getInternalStartServices }), - getGeoHashBucketAgg({ getInternalStartServices }), - getGeoTitleBucketAgg({ getInternalStartServices }), + getDateRangeBucketAgg({ uiSettings }), + getIpRangeBucketAgg(), + getTermsBucketAgg(), + getFilterBucketAgg(), + getFiltersBucketAgg({ uiSettings }), + getSignificantTermsBucketAgg(), + getGeoHashBucketAgg(), + getGeoTitleBucketAgg(), ], }); diff --git a/src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts b/src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts index f3c95b444dee9..03629c3189cbb 100644 --- a/src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts +++ b/src/plugins/data/public/search/aggs/buckets/bucket_agg_type.ts @@ -21,7 +21,6 @@ import { IAggConfig } from '../agg_config'; import { KBN_FIELD_TYPES } from '../../../../common'; import { AggType, AggTypeConfig } from '../agg_type'; import { AggParamType } from '../param_types/agg'; -import { GetInternalStartServicesFn } from '../../../types'; export interface IBucketAggConfig extends IAggConfig { type: InstanceType; @@ -40,10 +39,6 @@ interface BucketAggTypeConfig getKey?: (bucket: any, key: any, agg: IAggConfig) => any; } -interface BucketAggTypeDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export class BucketAggType extends AggType< TBucketAggConfig, BucketAggParam @@ -51,11 +46,8 @@ export class BucketAggType any; type = bucketType; - constructor( - config: BucketAggTypeConfig, - dependencies: BucketAggTypeDependencies - ) { - super(config, dependencies); + constructor(config: BucketAggTypeConfig) { + super(config); this.getKey = config.getKey || diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 518bdbfe0c135..24a17b60566cc 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -21,7 +21,7 @@ import moment from 'moment'; import { createFilterDateHistogram } from './date_histogram'; import { intervalOptions } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; +import { mockAggTypesRegistry } from '../../test_helpers'; import { getDateHistogramBucketAgg, DateHistogramBucketAggDependencies, @@ -29,9 +29,7 @@ import { } from '../date_histogram'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { RangeFilter } from '../../../../../common'; -import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { InternalStartServices } from '../../../../types'; +import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('date_histogram', () => { @@ -46,15 +44,8 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { calculateBounds: jest.fn(), - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), uiSettings, }; - - mockDataServices(); }); const init = (interval: string = 'auto', duration: any = moment.duration(15, 'minutes')) => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index 08f39e10b6006..c272c037c5927 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -26,9 +26,7 @@ import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { InternalStartServices } from '../../../../types'; +import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('Date range', () => { @@ -37,14 +35,7 @@ describe('AggConfig Filters', () => { beforeEach(() => { const { uiSettings } = coreMock.createSetup(); - aggTypesDependencies = { - uiSettings, - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + aggTypesDependencies = { uiSettings }; }); const getConfig = (() => {}) as FieldFormatsGetConfigFn; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index c5ad1a61e6676..ff66d80c6d8d0 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -22,9 +22,7 @@ import { createFilterFilters } from './filters'; import { AggConfigs } from '../../agg_configs'; import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { InternalStartServices } from '../../../../types'; +import { coreMock } from '../../../../../../../core/public/mocks'; describe('AggConfig Filters', () => { describe('filters', () => { @@ -33,14 +31,7 @@ describe('AggConfig Filters', () => { beforeEach(() => { const { uiSettings } = coreMock.createSetup(); - aggTypesDependencies = { - uiSettings, - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + aggTypesDependencies = { uiSettings }; }); const getAggConfigs = () => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index 89a5fd6577891..852685a505afd 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -24,22 +24,10 @@ import { mockAggTypesRegistry } from '../../test_helpers'; import { IpFormat } from '../../../../../common'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../core/public/mocks'; -import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('IP range', () => { - const fieldFormats = fieldFormatsServiceMock.createStartContract(); - const typesRegistry = mockAggTypesRegistry([ - getIpRangeBucketAgg({ - getInternalStartServices: () => - (({ - fieldFormats, - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }), - ]); + const typesRegistry = mockAggTypesRegistry([getIpRangeBucketAgg()]); const getAggConfigs = (aggs: CreateAggConfigParams[]) => { const field = { name: 'ip', diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index 238e5737d8f47..faffad3beb8c1 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -17,21 +17,19 @@ * under the License. */ -import { getRangeBucketAgg, RangeBucketAggDependencies } from '../range'; +import { getRangeBucketAgg } from '../range'; import { createFilterRange } from './range'; import { BytesFormat, FieldFormatsGetConfigFn } from '../../../../../common'; import { AggConfigs } from '../../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../../test_helpers'; +import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { FieldFormatsStart } from '../../../../field_formats'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../core/public/mocks'; import { GetInternalStartServicesFn, InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('range', () => { - let aggTypesDependencies: RangeBucketAggDependencies; let getInternalStartServices: GetInternalStartServicesFn; let fieldFormats: FieldFormatsStart; @@ -41,17 +39,7 @@ describe('AggConfig Filters', () => { getInternalStartServices = () => (({ fieldFormats, - notifications: notificationServiceMock.createStartContract(), } as unknown) as InternalStartServices); - - aggTypesDependencies = { - getInternalStartServices: () => - (({ - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - - mockDataServices(); }); const getConfig = (() => {}) as FieldFormatsGetConfigFn; @@ -84,7 +72,7 @@ describe('AggConfig Filters', () => { }, ], { - typesRegistry: mockAggTypesRegistry([getRangeBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry([getRangeBucketAgg({ getInternalStartServices })]), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index a45087916a395..1c165f0d29ab6 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -24,25 +24,9 @@ import { mockAggTypesRegistry } from '../../test_helpers'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { Filter, ExistsFilter } from '../../../../../common'; -import { RangeBucketAggDependencies } from '../range'; -import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../core/public/mocks'; -import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('terms', () => { - let aggTypesDependencies: RangeBucketAggDependencies; - - beforeEach(() => { - aggTypesDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - }); - const getAggConfigs = (aggs: CreateAggConfigParams[]) => { const indexPattern = { id: '1234', @@ -59,7 +43,7 @@ describe('AggConfig Filters', () => { }; return new AggConfigs(indexPattern, aggs, { - typesRegistry: mockAggTypesRegistry([getTermsBucketAgg(aggTypesDependencies)]), + typesRegistry: mockAggTypesRegistry([getTermsBucketAgg()]), }); }; diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index e4c4bc0cedc3c..fa1725eccbd28 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -37,7 +37,6 @@ import { TimeRangeBounds, UI_SETTINGS, } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; import { ExtendedBounds } from './lib/extended_bounds'; @@ -59,7 +58,6 @@ const updateTimeBuckets = ( export interface DateHistogramBucketAggDependencies { calculateBounds: CalculateBoundsFn; - getInternalStartServices: GetInternalStartServicesFn; uiSettings: IUiSettingsClient; } @@ -86,219 +84,213 @@ export interface AggParamsDateHistogram extends BaseAggParams { export const getDateHistogramBucketAgg = ({ calculateBounds, - getInternalStartServices, uiSettings, }: DateHistogramBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.DATE_HISTOGRAM, - title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', { - defaultMessage: 'Date Histogram', - }), - ordered: { - date: true, - }, - makeLabel(agg) { - let output: Record = {}; + new BucketAggType({ + name: BUCKET_TYPES.DATE_HISTOGRAM, + title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', { + defaultMessage: 'Date Histogram', + }), + ordered: { + date: true, + }, + makeLabel(agg) { + let output: Record = {}; - if (this.params) { - output = writeParams(this.params, agg); - } + if (this.params) { + output = writeParams(this.params, agg); + } - const field = agg.getFieldDisplayName(); + const field = agg.getFieldDisplayName(); - return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', { - defaultMessage: '{fieldName} per {intervalDescription}', - values: { - fieldName: field, - intervalDescription: output.metricScaleText || output.bucketInterval.description, - }, - }); - }, - createFilter: createFilterDateHistogram, - decorateAggConfig() { - let buckets: any; + return i18n.translate('data.search.aggs.buckets.dateHistogramLabel', { + defaultMessage: '{fieldName} per {intervalDescription}', + values: { + fieldName: field, + intervalDescription: output.metricScaleText || output.bucketInterval.description, + }, + }); + }, + createFilter: createFilterDateHistogram, + decorateAggConfig() { + let buckets: any; - return { - buckets: { - configurable: true, - get() { - if (buckets) return buckets; + return { + buckets: { + configurable: true, + get() { + if (buckets) return buckets; - buckets = new TimeBuckets({ - 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), - 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), - dateFormat: uiSettings.get('dateFormat'), - 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), - }); - updateTimeBuckets(this, calculateBounds, buckets); + buckets = new TimeBuckets({ + 'histogram:maxBars': uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), + 'histogram:barTarget': uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET), + dateFormat: uiSettings.get('dateFormat'), + 'dateFormat:scaled': uiSettings.get('dateFormat:scaled'), + }); + updateTimeBuckets(this, calculateBounds, buckets); - return buckets; - }, - } as any, - }; - }, - getSerializedFormat(agg) { - return { - id: 'date', - params: { - pattern: agg.buckets.getScaledDateFormat(), - }, - }; - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.DATE, - default(agg: IBucketDateHistogramAggConfig) { - return agg.getIndexPattern().timeFieldName; - }, - onChange(agg: IBucketDateHistogramAggConfig) { - if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) { - delete agg.params.interval; - } + return buckets; }, + } as any, + }; + }, + getSerializedFormat(agg) { + return { + id: 'date', + params: { + pattern: agg.buckets.getScaledDateFormat(), }, - { - name: 'timeRange', - default: null, - write: noop, - }, - { - name: 'useNormalizedEsInterval', - default: true, - write: noop, + }; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.DATE, + default(agg: IBucketDateHistogramAggConfig) { + return agg.getIndexPattern().timeFieldName; }, - { - name: 'scaleMetricValues', - default: false, - write: noop, - advanced: true, + onChange(agg: IBucketDateHistogramAggConfig) { + if (get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) { + delete agg.params.interval; + } }, - { - name: 'interval', - deserialize(state: any, agg) { - // For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value - if (state === 'custom') { - return get(agg, 'params.customInterval'); - } + }, + { + name: 'timeRange', + default: null, + write: noop, + }, + { + name: 'useNormalizedEsInterval', + default: true, + write: noop, + }, + { + name: 'scaleMetricValues', + default: false, + write: noop, + advanced: true, + }, + { + name: 'interval', + deserialize(state: any, agg) { + // For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value + if (state === 'custom') { + return get(agg, 'params.customInterval'); + } - const interval = find(intervalOptions, { val: state }); + const interval = find(intervalOptions, { val: state }); - // For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year', - // but this maps the old values to the new values - if (!interval && state === 'year') { - return 'y'; - } - return state; - }, - default: 'auto', - options: intervalOptions, - write(agg, output, aggs) { - updateTimeBuckets(agg, calculateBounds); + // For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year', + // but this maps the old values to the new values + if (!interval && state === 'year') { + return 'y'; + } + return state; + }, + default: 'auto', + options: intervalOptions, + write(agg, output, aggs) { + updateTimeBuckets(agg, calculateBounds); - const { useNormalizedEsInterval, scaleMetricValues } = agg.params; - const interval = agg.buckets.getInterval(useNormalizedEsInterval); - output.bucketInterval = interval; - if (interval.expression === '0ms') { - // We are hitting this code a couple of times while configuring in editor - // with an interval of 0ms because the overall time range has not yet been - // set. Since 0ms is not a valid ES interval, we cannot pass it through dateHistogramInterval - // below, since it would throw an exception. So in the cases we still have an interval of 0ms - // here we simply skip the rest of the method and never write an interval into the DSL, since - // this DSL will anyway not be used before we're passing this code with an actual interval. - return; - } - output.params = { - ...output.params, - ...dateHistogramInterval(interval.expression), - }; + const { useNormalizedEsInterval, scaleMetricValues } = agg.params; + const interval = agg.buckets.getInterval(useNormalizedEsInterval); + output.bucketInterval = interval; + if (interval.expression === '0ms') { + // We are hitting this code a couple of times while configuring in editor + // with an interval of 0ms because the overall time range has not yet been + // set. Since 0ms is not a valid ES interval, we cannot pass it through dateHistogramInterval + // below, since it would throw an exception. So in the cases we still have an interval of 0ms + // here we simply skip the rest of the method and never write an interval into the DSL, since + // this DSL will anyway not be used before we're passing this code with an actual interval. + return; + } + output.params = { + ...output.params, + ...dateHistogramInterval(interval.expression), + }; - const scaleMetrics = - scaleMetricValues && interval.scaled && interval.scale && interval.scale < 1; - if (scaleMetrics && aggs) { - const metrics = aggs.aggs.filter((a) => isMetricAggType(a.type)); - const all = every(metrics, (a: IBucketAggConfig) => { - const { type } = a; + const scaleMetrics = + scaleMetricValues && interval.scaled && interval.scale && interval.scale < 1; + if (scaleMetrics && aggs) { + const metrics = aggs.aggs.filter((a) => isMetricAggType(a.type)); + const all = every(metrics, (a: IBucketAggConfig) => { + const { type } = a; - if (isMetricAggType(type)) { - return type.isScalable(); - } - }); - if (all) { - output.metricScale = interval.scale; - output.metricScaleText = interval.preScaled?.description || ''; + if (isMetricAggType(type)) { + return type.isScalable(); } + }); + if (all) { + output.metricScale = interval.scale; + output.metricScaleText = interval.preScaled?.description || ''; } - }, - }, - { - name: 'time_zone', - default: undefined, - // We don't ever want this parameter to be serialized out (when saving or to URLs) - // since we do all the logic handling it "on the fly" in the `write` method, to prevent - // time_zones being persisted into saved_objects - serialize: noop, - write(agg, output) { - // If a time_zone has been set explicitly always prefer this. - let tz = agg.params.time_zone; - if (!tz && agg.params.field) { - // If a field has been configured check the index pattern's typeMeta if a date_histogram on that - // field requires a specific time_zone - tz = get(agg.getIndexPattern(), [ - 'typeMeta', - 'aggs', - 'date_histogram', - agg.params.field.name, - 'time_zone', - ]); - } - if (!tz) { - // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz - const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - tz = isDefaultTimezone - ? detectedTimezone || tzOffset - : uiSettings.get('dateFormat:tz'); - } - output.params.time_zone = tz; - }, - }, - { - name: 'drop_partials', - default: false, - write: noop, - shouldShow: (agg) => { - const field = agg.params.field; - return field && field.name && field.name === agg.getIndexPattern().timeFieldName; - }, + } }, - { - name: 'format', + }, + { + name: 'time_zone', + default: undefined, + // We don't ever want this parameter to be serialized out (when saving or to URLs) + // since we do all the logic handling it "on the fly" in the `write` method, to prevent + // time_zones being persisted into saved_objects + serialize: noop, + write(agg, output) { + // If a time_zone has been set explicitly always prefer this. + let tz = agg.params.time_zone; + if (!tz && agg.params.field) { + // If a field has been configured check the index pattern's typeMeta if a date_histogram on that + // field requires a specific time_zone + tz = get(agg.getIndexPattern(), [ + 'typeMeta', + 'aggs', + 'date_histogram', + agg.params.field.name, + 'time_zone', + ]); + } + if (!tz) { + // If the index pattern typeMeta data, didn't had a time zone assigned for the selected field use the configured tz + const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); + const detectedTimezone = moment.tz.guess(); + const tzOffset = moment().format('Z'); + tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz'); + } + output.params.time_zone = tz; }, - { - name: 'min_doc_count', - default: 1, + }, + { + name: 'drop_partials', + default: false, + write: noop, + shouldShow: (agg) => { + const field = agg.params.field; + return field && field.name && field.name === agg.getIndexPattern().timeFieldName; }, - { - name: 'extended_bounds', - default: {}, - write(agg, output) { - const val = agg.params.extended_bounds; + }, + { + name: 'format', + }, + { + name: 'min_doc_count', + default: 1, + }, + { + name: 'extended_bounds', + default: {}, + write(agg, output) { + const val = agg.params.extended_bounds; - if (val.min != null || val.max != null) { - output.params.extended_bounds = { - min: moment(val.min).valueOf(), - max: moment(val.max).valueOf(), - }; + if (val.min != null || val.max != null) { + output.params.extended_bounds = { + min: moment(val.min).valueOf(), + max: moment(val.max).valueOf(), + }; - return; - } - }, + return; + } }, - ], - }, - { getInternalStartServices } - ); + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts index 033b44da0880f..1cc5b41fa6bb3 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts index 0829d6e9cdc9f..69515dfee87fe 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts @@ -17,13 +17,11 @@ * under the License. */ -import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { getDateRangeBucketAgg, DateRangeBucketAggDependencies } from './date_range'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; describe('date_range params', () => { let aggTypesDependencies: DateRangeBucketAggDependencies; @@ -31,14 +29,7 @@ describe('date_range params', () => { beforeEach(() => { const { uiSettings } = coreMock.createSetup(); - aggTypesDependencies = { - uiSettings, - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + aggTypesDependencies = { uiSettings }; }); const getAggConfigs = (params: Record = {}, hasIncludeTypeMeta: boolean = true) => { diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.ts b/src/plugins/data/public/search/aggs/buckets/date_range.ts index dbcf5517fb08f..8c576023f0239 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.ts @@ -28,7 +28,6 @@ import { createFilterDateRange } from './create_filter/date_range'; import { DateRangeKey } from './lib/date_range'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', { @@ -37,7 +36,6 @@ const dateRangeTitle = i18n.translate('data.search.aggs.buckets.dateRangeTitle', export interface DateRangeBucketAggDependencies { uiSettings: IUiSettingsClient; - getInternalStartServices: GetInternalStartServicesFn; } export interface AggParamsDateRange extends BaseAggParams { @@ -46,76 +44,68 @@ export interface AggParamsDateRange extends BaseAggParams { time_zone?: string; } -export const getDateRangeBucketAgg = ({ - uiSettings, - getInternalStartServices, -}: DateRangeBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.DATE_RANGE, - title: dateRangeTitle, - createFilter: createFilterDateRange, - getKey({ from, to }): DateRangeKey { - return { from, to }; - }, - getSerializedFormat(agg) { - return { - id: 'date_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, - }; - }, - makeLabel(aggConfig) { - return aggConfig.getFieldDisplayName() + ' date ranges'; +export const getDateRangeBucketAgg = ({ uiSettings }: DateRangeBucketAggDependencies) => + new BucketAggType({ + name: BUCKET_TYPES.DATE_RANGE, + title: dateRangeTitle, + createFilter: createFilterDateRange, + getKey({ from, to }): DateRangeKey { + return { from, to }; + }, + getSerializedFormat(agg) { + return { + id: 'date_range', + params: agg.params.field ? agg.params.field.format.toJSON() : {}, + }; + }, + makeLabel(aggConfig) { + return aggConfig.getFieldDisplayName() + ' date ranges'; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.DATE, + default(agg: IBucketAggConfig) { + return agg.getIndexPattern().timeFieldName; + }, }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.DATE, - default(agg: IBucketAggConfig) { - return agg.getIndexPattern().timeFieldName; + { + name: 'ranges', + default: [ + { + from: 'now-1w/w', + to: 'now', }, - }, - { - name: 'ranges', - default: [ - { - from: 'now-1w/w', - to: 'now', - }, - ], - }, - { - name: 'time_zone', - default: undefined, - // Implimentation method is the same as that of date_histogram - serialize: () => undefined, - write: (agg, output) => { - const field = agg.getParam('field'); - let tz = agg.getParam('time_zone'); + ], + }, + { + name: 'time_zone', + default: undefined, + // Implimentation method is the same as that of date_histogram + serialize: () => undefined, + write: (agg, output) => { + const field = agg.getParam('field'); + let tz = agg.getParam('time_zone'); - if (!tz && field) { - tz = get(agg.getIndexPattern(), [ - 'typeMeta', - 'aggs', - 'date_range', - field.name, - 'time_zone', - ]); - } - if (!tz) { - const detectedTimezone = moment.tz.guess(); - const tzOffset = moment().format('Z'); - const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); + if (!tz && field) { + tz = get(agg.getIndexPattern(), [ + 'typeMeta', + 'aggs', + 'date_range', + field.name, + 'time_zone', + ]); + } + if (!tz) { + const detectedTimezone = moment.tz.guess(); + const tzOffset = moment().format('Z'); + const isDefaultTimezone = uiSettings.isDefault('dateFormat:tz'); - tz = isDefaultTimezone - ? detectedTimezone || tzOffset - : uiSettings.get('dateFormat:tz'); - } - output.params.time_zone = tz; - }, + tz = isDefaultTimezone ? detectedTimezone || tzOffset : uiSettings.get('dateFormat:tz'); + } + output.params.time_zone = tz; }, - ], - }, - { getInternalStartServices } - ); + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts b/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts index 1fe42ce63d815..5027aadbb7331 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/filter.ts b/src/plugins/data/public/search/aggs/buckets/filter.ts index d048df9f0e43e..5d146e125b996 100644 --- a/src/plugins/data/public/search/aggs/buckets/filter.ts +++ b/src/plugins/data/public/search/aggs/buckets/filter.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { GetInternalStartServicesFn } from '../../../types'; import { GeoBoundingBox } from './lib/geo_point'; import { BaseAggParams } from '../types'; @@ -28,25 +27,18 @@ const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { defaultMessage: 'Filter', }); -export interface FilterBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export interface AggParamsFilter extends BaseAggParams { geo_bounding_box?: GeoBoundingBox; } -export const getFilterBucketAgg = ({ getInternalStartServices }: FilterBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.FILTER, - title: filterTitle, - makeLabel: () => filterTitle, - params: [ - { - name: 'geo_bounding_box', - }, - ], - }, - { getInternalStartServices } - ); +export const getFilterBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.FILTER, + title: filterTitle, + makeLabel: () => filterTitle, + params: [ + { + name: 'geo_bounding_box', + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/filter_fn.ts b/src/plugins/data/public/search/aggs/buckets/filter_fn.ts index 4a7180fc86c71..ae60da3e8a47c 100644 --- a/src/plugins/data/public/search/aggs/buckets/filter_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/filter_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/filters.test.ts index 7554e4081726c..bcb82b5f99649 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.test.ts @@ -18,13 +18,11 @@ */ import { Query } from '../../../../common'; -import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { getFiltersBucketAgg, FiltersBucketAggDependencies } from './filters'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { InternalStartServices } from '../../../types'; describe('Filters Agg', () => { let aggTypesDependencies: FiltersBucketAggDependencies; @@ -33,14 +31,7 @@ describe('Filters Agg', () => { jest.resetAllMocks(); const { uiSettings } = coreMock.createSetup(); - aggTypesDependencies = { - uiSettings, - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + aggTypesDependencies = { uiSettings }; }); describe('order agg editor UI', () => { diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index cb17ef07a930b..cd4ed721fda77 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -25,10 +25,7 @@ import { createFilterFilters } from './create_filter/filters'; import { toAngularJSON } from '../utils'; import { BucketAggType } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common'; -import { getQueryLog } from '../../../query'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const filtersTitle = i18n.translate('data.search.aggs.buckets.filtersTitle', { @@ -45,7 +42,6 @@ interface FilterValue { export interface FiltersBucketAggDependencies { uiSettings: IUiSettingsClient; - getInternalStartServices: GetInternalStartServicesFn; } export interface AggParamsFilters extends Omit { @@ -55,81 +51,60 @@ export interface AggParamsFilters extends Omit { }>; } -export const getFiltersBucketAgg = ({ - uiSettings, - getInternalStartServices, -}: FiltersBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.FILTERS, - title: filtersTitle, - createFilter: createFilterFilters, - customLabels: false, - params: [ - { - name: 'filters', - default: [ - { - input: { query: '', language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, - label: '', - }, - ], - write(aggConfig, output) { - const inFilters: FilterValue[] = aggConfig.params.filters; - if (!size(inFilters)) return; - - inFilters.forEach((filter) => { - const persistedLog = getQueryLog( - uiSettings, - new Storage(window.localStorage), - 'vis_default_editor', - filter.input.language - ); - persistedLog.add(filter.input.query); - }); +export const getFiltersBucketAgg = ({ uiSettings }: FiltersBucketAggDependencies) => + new BucketAggType({ + name: BUCKET_TYPES.FILTERS, + title: filtersTitle, + createFilter: createFilterFilters, + customLabels: false, + params: [ + { + name: 'filters', + default: [ + { + input: { query: '', language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) }, + label: '', + }, + ], + write(aggConfig, output) { + const inFilters: FilterValue[] = aggConfig.params.filters; + if (!size(inFilters)) return; - const outFilters = transform( - inFilters, - function (filters: any, filter) { - const input = cloneDeep(filter.input); + const outFilters = transform( + inFilters, + function (filters: Record, filter) { + const input = cloneDeep(filter.input); - if (!input) { - console.log('malformed filter agg params, missing "input" query'); // eslint-disable-line no-console - return; - } + if (!input) { + console.log('malformed filter agg params, missing "input" query'); // eslint-disable-line no-console + return; + } - const esQueryConfigs = getEsQueryConfig(uiSettings); - const query = buildEsQuery( - aggConfig.getIndexPattern(), - [input], - [], - esQueryConfigs - ); + const esQueryConfigs = getEsQueryConfig(uiSettings); + const query = buildEsQuery(aggConfig.getIndexPattern(), [input], [], esQueryConfigs); - if (!query) { - console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console - return; - } + if (!query) { + console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console + return; + } - const matchAllLabel = filter.input.query === '' ? '*' : ''; - const label = - filter.label || - matchAllLabel || - (typeof filter.input.query === 'string' - ? filter.input.query - : toAngularJSON(filter.input.query)); - filters[label] = query; - }, - {} - ); + const matchAllLabel = filter.input.query === '' ? '*' : ''; + const label = + filter.label || + matchAllLabel || + (typeof filter.input.query === 'string' + ? filter.input.query + : toAngularJSON(filter.input.query)); + filters[label] = query; + }, + {} + ); - if (!size(outFilters)) return; + if (!size(outFilters)) return; - const params = output.params || (output.params = {}); - params.filters = outFilters; - }, + const params = output.params || (output.params = {}); + params.filters = outFilters; }, - ], - }, - { getInternalStartServices } - ); + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/filters_fn.ts b/src/plugins/data/public/search/aggs/buckets/filters_fn.ts index 6ffd5369d7087..55380ea815315 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts index d4c0a5b328844..e77d2bf1eaf5f 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -17,29 +17,17 @@ * under the License. */ -import { getGeoHashBucketAgg, GeoHashBucketAggDependencies } from './geo_hash'; +import { getGeoHashBucketAgg } from './geo_hash'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; -import { InternalStartServices } from '../../../types'; describe('Geohash Agg', () => { - let aggTypesDependencies: GeoHashBucketAggDependencies; let geoHashBucketAgg: BucketAggType; beforeEach(() => { - aggTypesDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - - geoHashBucketAgg = getGeoHashBucketAgg(aggTypesDependencies); + geoHashBucketAgg = getGeoHashBucketAgg(); }); const getAggConfigs = (params?: Record) => { diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.ts index fbe7c76681f81..a0ef8a27b0d1e 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { GetInternalStartServicesFn } from '../../../types'; import { GeoBoundingBox } from './lib/geo_point'; import { BaseAggParams } from '../types'; @@ -36,10 +35,6 @@ const geohashGridTitle = i18n.translate('data.search.aggs.buckets.geohashGridTit defaultMessage: 'Geohash', }); -export interface GeoHashBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export interface AggParamsGeoHash extends BaseAggParams { field: string; autoPrecision?: boolean; @@ -49,88 +44,85 @@ export interface AggParamsGeoHash extends BaseAggParams { boundingBox?: GeoBoundingBox; } -export const getGeoHashBucketAgg = ({ getInternalStartServices }: GeoHashBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.GEOHASH_GRID, - title: geohashGridTitle, - makeLabel: () => geohashGridTitle, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, - }, - { - name: 'autoPrecision', - default: true, - write: () => {}, - }, - { - name: 'precision', - default: defaultPrecision, - write(aggConfig, output) { - output.params.precision = aggConfig.params.precision; - }, - }, - { - name: 'useGeocentroid', - default: true, - write: () => {}, - }, - { - name: 'isFilteredByCollar', - default: true, - write: () => {}, - }, - { - name: 'boundingBox', - default: null, - write: () => {}, +export const getGeoHashBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.GEOHASH_GRID, + title: geohashGridTitle, + makeLabel: () => geohashGridTitle, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, + }, + { + name: 'autoPrecision', + default: true, + write: () => {}, + }, + { + name: 'precision', + default: defaultPrecision, + write(aggConfig, output) { + output.params.precision = aggConfig.params.precision; }, - ], - getRequestAggs(agg) { - const aggs = []; - const params = agg.params; + }, + { + name: 'useGeocentroid', + default: true, + write: () => {}, + }, + { + name: 'isFilteredByCollar', + default: true, + write: () => {}, + }, + { + name: 'boundingBox', + default: null, + write: () => {}, + }, + ], + getRequestAggs(agg) { + const aggs = []; + const params = agg.params; - if (params.isFilteredByCollar && agg.getField()) { - aggs.push( - agg.aggConfigs.createAggConfig( - { - type: 'filter', - id: 'filter_agg', - enabled: true, - params: { - geo_bounding_box: { - ignore_unmapped: true, - [agg.getField().name]: params.boundingBox || defaultBoundingBox, - }, + if (params.isFilteredByCollar && agg.getField()) { + aggs.push( + agg.aggConfigs.createAggConfig( + { + type: 'filter', + id: 'filter_agg', + enabled: true, + params: { + geo_bounding_box: { + ignore_unmapped: true, + [agg.getField().name]: params.boundingBox || defaultBoundingBox, }, - } as any, - { addToAggConfigs: false } - ) - ); - } + }, + } as any, + { addToAggConfigs: false } + ) + ); + } - aggs.push(agg); + aggs.push(agg); - if (params.useGeocentroid) { - aggs.push( - agg.aggConfigs.createAggConfig( - { - type: 'geo_centroid', - enabled: true, - params: { - field: agg.getField(), - }, + if (params.useGeocentroid) { + aggs.push( + agg.aggConfigs.createAggConfig( + { + type: 'geo_centroid', + enabled: true, + params: { + field: agg.getField(), }, - { addToAggConfigs: false } - ) - ); - } + }, + { addToAggConfigs: false } + ) + ); + } - return aggs; - }, + return aggs; }, - { getInternalStartServices } - ); + }); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts index bbfa8575d486c..5152804bf8122 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile.ts b/src/plugins/data/public/search/aggs/buckets/geo_tile.ts index 1212bba23a93a..e6eff1e1a5d8e 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_tile.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_tile.ts @@ -24,13 +24,8 @@ import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; import { METRIC_TYPES } from '../metrics/metric_agg_types'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; -export interface GeoTitleBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const geotileGridTitle = i18n.translate('data.search.aggs.buckets.geotileGridTitle', { defaultMessage: 'Geotile', }); @@ -41,47 +36,44 @@ export interface AggParamsGeoTile extends BaseAggParams { precision?: number; } -export const getGeoTitleBucketAgg = ({ getInternalStartServices }: GeoTitleBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.GEOTILE_GRID, - title: geotileGridTitle, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, - }, - { - name: 'useGeocentroid', - default: true, - write: noop, - }, - { - name: 'precision', - default: 0, - }, - ], - getRequestAggs(agg) { - const aggs = []; - const useGeocentroid = agg.getParam('useGeocentroid'); +export const getGeoTitleBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.GEOTILE_GRID, + title: geotileGridTitle, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, + }, + { + name: 'useGeocentroid', + default: true, + write: noop, + }, + { + name: 'precision', + default: 0, + }, + ], + getRequestAggs(agg) { + const aggs = []; + const useGeocentroid = agg.getParam('useGeocentroid'); - aggs.push(agg); + aggs.push(agg); - if (useGeocentroid) { - const aggConfig = { - type: METRIC_TYPES.GEO_CENTROID, - enabled: true, - params: { - field: agg.getField(), - }, - }; + if (useGeocentroid) { + const aggConfig = { + type: METRIC_TYPES.GEO_CENTROID, + enabled: true, + params: { + field: agg.getField(), + }, + }; - aggs.push(agg.aggConfigs.createAggConfig(aggConfig, { addToAggConfigs: false })); - } + aggs.push(agg.aggConfigs.createAggConfig(aggConfig, { addToAggConfigs: false })); + } - return aggs as IBucketAggConfig[]; - }, + return aggs as IBucketAggConfig[]; }, - { getInternalStartServices } - ); + }); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts index 9c33ef45762af..ed3142408892a 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_tile_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts index 787603ee14361..6ac77f207d9ce 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -42,7 +42,6 @@ describe('Histogram Agg', () => { getInternalStartServices: () => (({ fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.ts b/src/plugins/data/public/search/aggs/buckets/histogram.ts index ce275e2dc1639..500b6eab75d77 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.ts @@ -57,165 +57,162 @@ export const getHistogramBucketAgg = ({ uiSettings, getInternalStartServices, }: HistogramBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.HISTOGRAM, - title: i18n.translate('data.search.aggs.buckets.histogramTitle', { - defaultMessage: 'Histogram', - }), - ordered: {}, - makeLabel(aggConfig) { - return aggConfig.getFieldDisplayName(); - }, - createFilter: createFilterHistogram(getInternalStartServices), - decorateAggConfig() { - let autoBounds: AutoBounds; - - return { - setAutoBounds: { - configurable: true, - value(newValue: AutoBounds) { - autoBounds = newValue; - }, - }, - getAutoBounds: { - configurable: true, - value() { - return autoBounds; - }, + new BucketAggType({ + name: BUCKET_TYPES.HISTOGRAM, + title: i18n.translate('data.search.aggs.buckets.histogramTitle', { + defaultMessage: 'Histogram', + }), + ordered: {}, + makeLabel(aggConfig) { + return aggConfig.getFieldDisplayName(); + }, + createFilter: createFilterHistogram(getInternalStartServices), + decorateAggConfig() { + let autoBounds: AutoBounds; + + return { + setAutoBounds: { + configurable: true, + value(newValue: AutoBounds) { + autoBounds = newValue; }, - }; - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.NUMBER, }, - { - /* - * This parameter can be set if you want the auto scaled interval to always - * be a multiple of a specific base. - */ - name: 'intervalBase', - default: null, - write: () => {}, + getAutoBounds: { + configurable: true, + value() { + return autoBounds; + }, }, - { - name: 'interval', - modifyAggConfigOnSearchRequestStart( - aggConfig: IBucketHistogramAggConfig, - searchSource: any, - options: any - ) { - const field = aggConfig.getField(); - const aggBody = field.scripted - ? { script: { source: field.script, lang: field.lang } } - : { field: field.name }; - - const childSearchSource = searchSource - .createChild() - .setField('size', 0) - .setField('aggs', { - maxAgg: { - max: aggBody, - }, - minAgg: { - min: aggBody, - }, - }); - - return childSearchSource - .fetch(options) - .then((resp: any) => { - aggConfig.setAutoBounds({ - min: get(resp, 'aggregations.minAgg.value'), - max: get(resp, 'aggregations.maxAgg.value'), - }); - }) - .catch((e: Error) => { - if (e.name === 'AbortError') return; - getInternalStartServices().notifications.toasts.addWarning( - i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', { - defaultMessage: - 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.', - }) - ); + }; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.NUMBER, + }, + { + /* + * This parameter can be set if you want the auto scaled interval to always + * be a multiple of a specific base. + */ + name: 'intervalBase', + default: null, + write: () => {}, + }, + { + name: 'interval', + modifyAggConfigOnSearchRequestStart( + aggConfig: IBucketHistogramAggConfig, + searchSource: any, + options: any + ) { + const field = aggConfig.getField(); + const aggBody = field.scripted + ? { script: { source: field.script, lang: field.lang } } + : { field: field.name }; + + const childSearchSource = searchSource + .createChild() + .setField('size', 0) + .setField('aggs', { + maxAgg: { + max: aggBody, + }, + minAgg: { + min: aggBody, + }, + }); + + return childSearchSource + .fetch(options) + .then((resp: any) => { + aggConfig.setAutoBounds({ + min: get(resp, 'aggregations.minAgg.value'), + max: get(resp, 'aggregations.maxAgg.value'), }); - }, - write(aggConfig, output) { - let interval = parseFloat(aggConfig.params.interval); - if (interval <= 0) { - interval = 1; - } - const autoBounds = aggConfig.getAutoBounds(); - - // ensure interval does not create too many buckets and crash browser - if (autoBounds) { - const range = autoBounds.max - autoBounds.min; - const bars = range / interval; - - if (bars > uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { - const minInterval = range / uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); - - // Round interval by order of magnitude to provide clean intervals - // Always round interval up so there will always be less buckets than histogram:maxBars - const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); - let roundInterval = orderOfMagnitude; - - while (roundInterval < minInterval) { - roundInterval += orderOfMagnitude; - } - interval = roundInterval; + }) + .catch((e: Error) => { + if (e.name === 'AbortError') return; + throw new Error( + i18n.translate('data.search.aggs.histogram.missingMaxMinValuesWarning', { + defaultMessage: + 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.', + }) + ); + }); + }, + write(aggConfig, output) { + let interval = parseFloat(aggConfig.params.interval); + if (interval <= 0) { + interval = 1; + } + const autoBounds = aggConfig.getAutoBounds(); + + // ensure interval does not create too many buckets and crash browser + if (autoBounds) { + const range = autoBounds.max - autoBounds.min; + const bars = range / interval; + + if (bars > uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS)) { + const minInterval = range / uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + + // Round interval by order of magnitude to provide clean intervals + // Always round interval up so there will always be less buckets than histogram:maxBars + const orderOfMagnitude = Math.pow(10, Math.floor(Math.log10(minInterval))); + let roundInterval = orderOfMagnitude; + + while (roundInterval < minInterval) { + roundInterval += orderOfMagnitude; } + interval = roundInterval; } - const base = aggConfig.params.intervalBase; - - if (base) { - if (interval < base) { - // In case the specified interval is below the base, just increase it to it's base - interval = base; - } else if (interval % base !== 0) { - // In case the interval is not a multiple of the base round it to the next base - interval = Math.round(interval / base) * base; - } + } + const base = aggConfig.params.intervalBase; + + if (base) { + if (interval < base) { + // In case the specified interval is below the base, just increase it to it's base + interval = base; + } else if (interval % base !== 0) { + // In case the interval is not a multiple of the base round it to the next base + interval = Math.round(interval / base) * base; } + } - output.params.interval = interval; - }, + output.params.interval = interval; }, - { - name: 'min_doc_count', - default: false, - write(aggConfig, output) { - if (aggConfig.params.min_doc_count) { - output.params.min_doc_count = 0; - } else { - output.params.min_doc_count = 1; - } - }, + }, + { + name: 'min_doc_count', + default: false, + write(aggConfig, output) { + if (aggConfig.params.min_doc_count) { + output.params.min_doc_count = 0; + } else { + output.params.min_doc_count = 1; + } }, - { - name: 'has_extended_bounds', - default: false, - write: () => {}, + }, + { + name: 'has_extended_bounds', + default: false, + write: () => {}, + }, + { + name: 'extended_bounds', + default: { + min: '', + max: '', }, - { - name: 'extended_bounds', - default: { - min: '', - max: '', - }, - write(aggConfig, output) { - const { min, max } = aggConfig.params.extended_bounds; + write(aggConfig, output) { + const { min, max } = aggConfig.params.extended_bounds; - if (aggConfig.params.has_extended_bounds && (min || min === 0) && (max || max === 0)) { - output.params.extended_bounds = { min, max }; - } - }, - shouldShow: (aggConfig: IBucketAggConfig) => aggConfig.params.has_extended_bounds, + if (aggConfig.params.has_extended_bounds && (min || min === 0) && (max || max === 0)) { + output.params.extended_bounds = { min, max }; + } }, - ], - }, - { getInternalStartServices } - ); + shouldShow: (aggConfig: IBucketAggConfig) => aggConfig.params.has_extended_bounds, + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts index 1e5a5a72c0ecb..877fd13e59f87 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range.ts b/src/plugins/data/public/search/aggs/buckets/ip_range.ts index ed9bc5e0462f1..46e0b62d0f8d7 100644 --- a/src/plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/ip_range.ts @@ -25,7 +25,6 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterIpRange } from './create_filter/ip_range'; import { IpRangeKey, RangeIpRangeAggKey, CidrMaskIpRangeAggKey } from './lib/ip_range'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const ipRangeTitle = i18n.translate('data.search.aggs.buckets.ipRangeTitle', { @@ -37,10 +36,6 @@ export enum IP_RANGE_TYPES { MASK = 'mask', } -export interface IpRangeBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export interface AggParamsIpRange extends BaseAggParams { field: string; ipRangeType?: IP_RANGE_TYPES; @@ -50,64 +45,61 @@ export interface AggParamsIpRange extends BaseAggParams { }>; } -export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.IP_RANGE, - title: ipRangeTitle, - createFilter: createFilterIpRange, - getKey(bucket, key, agg): IpRangeKey { - if (agg.params.ipRangeType === IP_RANGE_TYPES.MASK) { - return { type: 'mask', mask: key }; - } - return { type: 'range', from: bucket.from, to: bucket.to }; - }, - getSerializedFormat(agg) { - return { - id: 'ip_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, - }; +export const getIpRangeBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.IP_RANGE, + title: ipRangeTitle, + createFilter: createFilterIpRange, + getKey(bucket, key, agg): IpRangeKey { + if (agg.params.ipRangeType === IP_RANGE_TYPES.MASK) { + return { type: 'mask', mask: key }; + } + return { type: 'range', from: bucket.from, to: bucket.to }; + }, + getSerializedFormat(agg) { + return { + id: 'ip_range', + params: agg.params.field ? agg.params.field.format.toJSON() : {}, + }; + }, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.buckets.ipRangeLabel', { + defaultMessage: '{fieldName} IP ranges', + values: { + fieldName: aggConfig.getFieldDisplayName(), + }, + }); + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.IP, }, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.buckets.ipRangeLabel', { - defaultMessage: '{fieldName} IP ranges', - values: { - fieldName: aggConfig.getFieldDisplayName(), - }, - }); + { + name: 'ipRangeType', + default: IP_RANGE_TYPES.FROM_TO, + write: noop, }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.IP, - }, - { - name: 'ipRangeType', - default: IP_RANGE_TYPES.FROM_TO, - write: noop, + { + name: 'ranges', + default: { + fromTo: [ + { from: '0.0.0.0', to: '127.255.255.255' }, + { from: '128.0.0.0', to: '191.255.255.255' }, + ], + mask: [{ mask: '0.0.0.0/1' }, { mask: '128.0.0.0/2' }], }, - { - name: 'ranges', - default: { - fromTo: [ - { from: '0.0.0.0', to: '127.255.255.255' }, - { from: '128.0.0.0', to: '191.255.255.255' }, - ], - mask: [{ mask: '0.0.0.0/1' }, { mask: '128.0.0.0/2' }], - }, - write(aggConfig, output) { - const ipRangeType = aggConfig.params.ipRangeType; - let ranges = aggConfig.params.ranges[ipRangeType]; + write(aggConfig, output) { + const ipRangeType = aggConfig.params.ipRangeType; + let ranges = aggConfig.params.ranges[ipRangeType]; - if (ipRangeType === IP_RANGE_TYPES.FROM_TO) { - ranges = map(ranges, (range: any) => omitBy(range, isNull)); - } + if (ipRangeType === IP_RANGE_TYPES.FROM_TO) { + ranges = map(ranges, (range: any) => omitBy(range, isNull)); + } - output.params.ranges = ranges; - }, + output.params.ranges = ranges; }, - ], - }, - { getInternalStartServices } - ); + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts index 554a8708d9164..15b763fd42d6b 100644 --- a/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/ip_range_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts b/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts index 4535b5f5c5dd2..57a7b378f305f 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/cidr_mask.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Ipv4Address } from '../../../../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../common'; const NUM_BITS = 32; diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts index af3c15167295c..ae7630ecd3dac 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import moment from 'moment'; import { TimeBuckets, TimeBucketsConfig } from './time_buckets'; diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/public/search/aggs/buckets/range.test.ts index fea5572b75795..f7c61a638158c 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.test.ts @@ -19,10 +19,10 @@ import { getRangeBucketAgg, RangeBucketAggDependencies } from './range'; import { AggConfigs } from '../agg_configs'; -import { mockDataServices, mockAggTypesRegistry } from '../test_helpers'; +import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { InternalStartServices } from '../../../types'; describe('Range Agg', () => { @@ -32,11 +32,9 @@ describe('Range Agg', () => { aggTypesDependencies = { getInternalStartServices: () => (({ - notifications: notificationServiceMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), } as unknown) as InternalStartServices), }; - - mockDataServices(); }); const getConfig = (() => {}) as FieldFormatsGetConfigFn; diff --git a/src/plugins/data/public/search/aggs/buckets/range.ts b/src/plugins/data/public/search/aggs/buckets/range.ts index 92c5193d55a20..9f54f9fd0704e 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.ts @@ -45,66 +45,63 @@ export interface AggParamsRange extends BaseAggParams { export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDependencies) => { const keyCaches = new WeakMap(); - return new BucketAggType( - { - name: BUCKET_TYPES.RANGE, - title: rangeTitle, - createFilter: createFilterRange(getInternalStartServices), - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.aggTypesLabel', { - defaultMessage: '{fieldName} ranges', - values: { - fieldName: aggConfig.getFieldDisplayName(), - }, - }); - }, - getKey(bucket, key, agg) { - let keys = keyCaches.get(agg); + return new BucketAggType({ + name: BUCKET_TYPES.RANGE, + title: rangeTitle, + createFilter: createFilterRange(getInternalStartServices), + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.aggTypesLabel', { + defaultMessage: '{fieldName} ranges', + values: { + fieldName: aggConfig.getFieldDisplayName(), + }, + }); + }, + getKey(bucket, key, agg) { + let keys = keyCaches.get(agg); - if (!keys) { - keys = new Map(); - keyCaches.set(agg, keys); - } + if (!keys) { + keys = new Map(); + keyCaches.set(agg, keys); + } - const id = RangeKey.idBucket(bucket); + const id = RangeKey.idBucket(bucket); - key = keys.get(id); - if (!key) { - key = new RangeKey(bucket); - keys.set(id, key); - } + key = keys.get(id); + if (!key) { + key = new RangeKey(bucket); + keys.set(id, key); + } - return key; - }, - getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; - return { - id: 'range', - params: { - id: format.id, - params: format.params, - }, - }; - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER], - }, - { - name: 'ranges', - default: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 }, - ], - write(aggConfig, output) { - output.params.ranges = aggConfig.params.ranges; - output.params.keyed = true; - }, + return key; + }, + getSerializedFormat(agg) { + const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + return { + id: 'range', + params: { + id: format.id, + params: format.params, }, - ], + }; }, - { getInternalStartServices } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER], + }, + { + name: 'ranges', + default: [ + { from: 0, to: 1000 }, + { from: 1000, to: 2000 }, + ], + write(aggConfig, output) { + output.params.ranges = aggConfig.params.ranges; + output.params.keyed = true; + }, + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/buckets/range_fn.ts b/src/plugins/data/public/search/aggs/buckets/range_fn.ts index 48686e7061de9..6806125a10f6d 100644 --- a/src/plugins/data/public/search/aggs/buckets/range_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/range_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts index 8fe833aa99cb2..f13fafc2b17e6 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -20,29 +20,11 @@ import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { - getSignificantTermsBucketAgg, - SignificantTermsBucketAggDependencies, -} from './significant_terms'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; +import { getSignificantTermsBucketAgg } from './significant_terms'; describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { describe('convert include/exclude from old format', () => { - let aggTypesDependencies: SignificantTermsBucketAggDependencies; - - beforeEach(() => { - aggTypesDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - }); - const getAggConfigs = (params: Record = {}) => { const indexPattern = { id: '1234', @@ -69,9 +51,7 @@ describe('Significant Terms Agg', () => { }, ], { - typesRegistry: mockAggTypesRegistry([ - getSignificantTermsBucketAgg(aggTypesDependencies), - ]), + typesRegistry: mockAggTypesRegistry([getSignificantTermsBucketAgg()]), } ); }; diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.ts index e6afc56dfd31c..4dc8aafd8a7a7 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.ts @@ -23,17 +23,12 @@ import { createFilterTerms } from './create_filter/terms'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const significantTermsTitle = i18n.translate('data.search.aggs.buckets.significantTermsTitle', { defaultMessage: 'Significant Terms', }); -export interface SignificantTermsBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export interface AggParamsSignificantTerms extends BaseAggParams { field: string; size?: number; @@ -41,57 +36,50 @@ export interface AggParamsSignificantTerms extends BaseAggParams { include?: string; } -export const getSignificantTermsBucketAgg = ({ - getInternalStartServices, -}: SignificantTermsBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.SIGNIFICANT_TERMS, - title: significantTermsTitle, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.buckets.significantTermsLabel', { - defaultMessage: 'Top {size} unusual terms in {fieldName}', - values: { - size: aggConfig.params.size, - fieldName: aggConfig.getFieldDisplayName(), - }, - }); - }, - createFilter: createFilterTerms, - params: [ - { - name: 'field', - type: 'field', - scriptable: false, - filterFieldTypes: KBN_FIELD_TYPES.STRING, - }, - { - name: 'size', - default: '', - }, - { - name: 'exclude', - displayName: i18n.translate('data.search.aggs.buckets.significantTerms.excludeLabel', { - defaultMessage: 'Exclude', - }), - type: 'string', - advanced: true, - shouldShow: isStringType, - ...migrateIncludeExcludeFormat, +export const getSignificantTermsBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.SIGNIFICANT_TERMS, + title: significantTermsTitle, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.buckets.significantTermsLabel', { + defaultMessage: 'Top {size} unusual terms in {fieldName}', + values: { + size: aggConfig.params.size, + fieldName: aggConfig.getFieldDisplayName(), }, - { - name: 'include', - displayName: i18n.translate('data.search.aggs.buckets.significantTerms.includeLabel', { - defaultMessage: 'Include', - }), - type: 'string', - advanced: true, - shouldShow: isStringType, - ...migrateIncludeExcludeFormat, - }, - ], + }); }, - { - getInternalStartServices, - } - ); + createFilter: createFilterTerms, + params: [ + { + name: 'field', + type: 'field', + scriptable: false, + filterFieldTypes: KBN_FIELD_TYPES.STRING, + }, + { + name: 'size', + default: '', + }, + { + name: 'exclude', + displayName: i18n.translate('data.search.aggs.buckets.significantTerms.excludeLabel', { + defaultMessage: 'Exclude', + }), + type: 'string', + advanced: true, + shouldShow: isStringType, + ...migrateIncludeExcludeFormat, + }, + { + name: 'include', + displayName: i18n.translate('data.search.aggs.buckets.significantTerms.includeLabel', { + defaultMessage: 'Include', + }), + type: 'string', + advanced: true, + shouldShow: isStringType, + ...migrateIncludeExcludeFormat, + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts index 83583070bddfe..1fecfcc914313 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 57a5ebf72316c..d3acd33d73d01 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -19,6 +19,7 @@ import { noop } from 'lodash'; import { i18n } from '@kbn/i18n'; + import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterTerms } from './create_filter/terms'; @@ -26,10 +27,8 @@ import { isStringOrNumberType, migrateIncludeExcludeFormat, } from './migrate_include_exclude_format'; -import { AggConfigSerialized, BaseAggParams, IAggConfigs } from '../types'; +import { AggConfigSerialized, BaseAggParams } from '../types'; -import { Adapters } from '../../../../../inspector/public'; -import { ISearchSource } from '../../search_source'; import { KBN_FIELD_TYPES } from '../../../../common'; import { getRequestInspectorStats, getResponseInspectorStats } from '../../expressions'; @@ -38,7 +37,6 @@ import { mergeOtherBucketAggResponse, updateMissingBucket, } from './_terms_other_bucket_helper'; -import { GetInternalStartServicesFn } from '../../../types'; export const termsAggFilter = [ '!top_hits', @@ -59,10 +57,6 @@ const termsTitle = i18n.translate('data.search.aggs.buckets.termsTitle', { defaultMessage: 'Terms', }); -export interface TermsBucketAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export interface AggParamsTerms extends BaseAggParams { field: string; orderBy: string; @@ -78,222 +72,219 @@ export interface AggParamsTerms extends BaseAggParams { include?: string; } -export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => - new BucketAggType( - { - name: BUCKET_TYPES.TERMS, - expressionName: 'aggTerms', - title: termsTitle, - makeLabel(agg) { - const params = agg.params; - return agg.getFieldDisplayName() + ': ' + params.order.text; - }, - getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; - return { - id: 'terms', - params: { - id: format.id, - otherBucketLabel: agg.params.otherBucketLabel, - missingBucketLabel: agg.params.missingBucketLabel, - ...format.params, - }, - }; - }, - createFilter: createFilterTerms, - postFlightRequest: async ( - resp: any, - aggConfigs: IAggConfigs, - aggConfig: IBucketAggConfig, - searchSource: ISearchSource, - inspectorAdapters: Adapters, - abortSignal?: AbortSignal - ) => { - if (!resp.aggregations) return resp; - const nestedSearchSource = searchSource.createChild(); - if (aggConfig.params.otherBucket) { - const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp); - if (!filterAgg) return resp; +export const getTermsBucketAgg = () => + new BucketAggType({ + name: BUCKET_TYPES.TERMS, + expressionName: 'aggTerms', + title: termsTitle, + makeLabel(agg) { + const params = agg.params; + return agg.getFieldDisplayName() + ': ' + params.order.text; + }, + getSerializedFormat(agg) { + const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + return { + id: 'terms', + params: { + id: format.id, + otherBucketLabel: agg.params.otherBucketLabel, + missingBucketLabel: agg.params.missingBucketLabel, + ...format.params, + }, + }; + }, + createFilter: createFilterTerms, + postFlightRequest: async ( + resp, + aggConfigs, + aggConfig, + searchSource, + inspectorRequestAdapter, + abortSignal + ) => { + if (!resp.aggregations) return resp; + const nestedSearchSource = searchSource.createChild(); + if (aggConfig.params.otherBucket) { + const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp); + if (!filterAgg) return resp; - nestedSearchSource.setField('aggs', filterAgg); + nestedSearchSource.setField('aggs', filterAgg); - const request = inspectorAdapters.requests.start( - i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { - defaultMessage: 'Other bucket', + const request = inspectorRequestAdapter.start( + i18n.translate('data.search.aggs.buckets.terms.otherBucketTitle', { + defaultMessage: 'Other bucket', + }), + { + description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { + defaultMessage: + 'This request counts the number of documents that fall ' + + 'outside the criterion of the data buckets.', }), - { - description: i18n.translate('data.search.aggs.buckets.terms.otherBucketDescription', { - defaultMessage: - 'This request counts the number of documents that fall ' + - 'outside the criterion of the data buckets.', - }), - } - ); - nestedSearchSource.getSearchRequestBody().then((body: string) => { - request.json(body); - }); - request.stats(getRequestInspectorStats(nestedSearchSource)); + } + ); + nestedSearchSource.getSearchRequestBody().then((body) => { + request.json(body); + }); + request.stats(getRequestInspectorStats(nestedSearchSource)); - const response = await nestedSearchSource.fetch({ abortSignal }); - request - .stats(getResponseInspectorStats(nestedSearchSource, response)) - .ok({ json: response }); - resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); - } - if (aggConfig.params.missingBucket) { - resp = updateMissingBucket(resp, aggConfigs, aggConfig); - } - return resp; + const response = await nestedSearchSource.fetch({ abortSignal }); + request + .stats(getResponseInspectorStats(nestedSearchSource, response)) + .ok({ json: response }); + resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); + } + if (aggConfig.params.missingBucket) { + resp = updateMissingBucket(resp, aggConfigs, aggConfig); + } + return resp; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [ + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.DATE, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.STRING, + ], }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [ - KBN_FIELD_TYPES.NUMBER, - KBN_FIELD_TYPES.BOOLEAN, - KBN_FIELD_TYPES.DATE, - KBN_FIELD_TYPES.IP, - KBN_FIELD_TYPES.STRING, - ], - }, - { - name: 'orderBy', - write: noop, // prevent default write, it's handled by orderAgg - }, - { - name: 'orderAgg', - type: 'agg', - allowedAggs: termsAggFilter, - default: null, - makeAgg(termsAgg, state = { type: 'count' }) { - state.schema = 'orderAgg'; - const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { - addToAggConfigs: false, - }); - orderAgg.id = termsAgg.id + '-orderAgg'; + { + name: 'orderBy', + write: noop, // prevent default write, it's handled by orderAgg + }, + { + name: 'orderAgg', + type: 'agg', + allowedAggs: termsAggFilter, + default: null, + makeAgg(termsAgg, state = { type: 'count' }) { + state.schema = 'orderAgg'; + const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { + addToAggConfigs: false, + }); + orderAgg.id = termsAgg.id + '-orderAgg'; - return orderAgg; - }, - write(agg, output, aggs) { - const dir = agg.params.order.value; - const order: Record = (output.params.order = {}); + return orderAgg; + }, + write(agg, output, aggs) { + const dir = agg.params.order.value; + const order: Record = (output.params.order = {}); - let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy); + let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy); - // TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings - // thus causing issues with filtering. This probably causes other issues since float might not - // be able to contain the number on the elasticsearch side - if (output.params.script) { - output.params.value_type = - agg.getField().type === 'number' ? 'float' : agg.getField().type; - } + // TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings + // thus causing issues with filtering. This probably causes other issues since float might not + // be able to contain the number on the elasticsearch side + if (output.params.script) { + output.params.value_type = + agg.getField().type === 'number' ? 'float' : agg.getField().type; + } - if (agg.params.missingBucket && agg.params.field.type === 'string') { - output.params.missing = '__missing__'; - } + if (agg.params.missingBucket && agg.params.field.type === 'string') { + output.params.missing = '__missing__'; + } - if (!orderAgg) { - order[agg.params.orderBy || '_count'] = dir; - return; - } + if (!orderAgg) { + order[agg.params.orderBy || '_count'] = dir; + return; + } - if (orderAgg.type.name === 'count') { - order._count = dir; - return; - } + if (orderAgg.type.name === 'count') { + order._count = dir; + return; + } - const orderAggId = orderAgg.id; + const orderAggId = orderAgg.id; - if (orderAgg.parentId && aggs) { - orderAgg = aggs.byId(orderAgg.parentId); - } + if (orderAgg.parentId && aggs) { + orderAgg = aggs.byId(orderAgg.parentId); + } - output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAggId] = dir; - }, - }, - { - name: 'order', - type: 'optioned', - default: 'desc', - options: [ - { - text: i18n.translate('data.search.aggs.buckets.terms.orderDescendingTitle', { - defaultMessage: 'Descending', - }), - value: 'desc', - }, - { - text: i18n.translate('data.search.aggs.buckets.terms.orderAscendingTitle', { - defaultMessage: 'Ascending', - }), - value: 'asc', - }, - ], - write: noop, // prevent default write, it's handled by orderAgg - }, - { - name: 'size', - default: 5, + output.subAggs = (output.subAggs || []).concat(orderAgg); + order[orderAggId] = dir; }, - { - name: 'otherBucket', - default: false, - write: noop, - }, - { - name: 'otherBucketLabel', - type: 'string', - default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { - defaultMessage: 'Other', - }), - displayName: i18n.translate('data.search.aggs.otherBucket.labelForOtherBucketLabel', { - defaultMessage: 'Label for other bucket', - }), - shouldShow: (agg) => agg.getParam('otherBucket'), - write: noop, - }, - { - name: 'missingBucket', - default: false, - write: noop, - }, - { - name: 'missingBucketLabel', - default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { - defaultMessage: 'Missing', - description: `Default label used in charts when documents are missing a field. + }, + { + name: 'order', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.buckets.terms.orderDescendingTitle', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.buckets.terms.orderAscendingTitle', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write: noop, // prevent default write, it's handled by orderAgg + }, + { + name: 'size', + default: 5, + }, + { + name: 'otherBucket', + default: false, + write: noop, + }, + { + name: 'otherBucketLabel', + type: 'string', + default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { + defaultMessage: 'Other', + }), + displayName: i18n.translate('data.search.aggs.otherBucket.labelForOtherBucketLabel', { + defaultMessage: 'Label for other bucket', + }), + shouldShow: (agg) => agg.getParam('otherBucket'), + write: noop, + }, + { + name: 'missingBucket', + default: false, + write: noop, + }, + { + name: 'missingBucketLabel', + default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { + defaultMessage: 'Missing', + description: `Default label used in charts when documents are missing a field. Visible when you create a chart with a terms aggregation and enable "Show missing values"`, - }), - type: 'string', - displayName: i18n.translate('data.search.aggs.otherBucket.labelForMissingValuesLabel', { - defaultMessage: 'Label for missing values', - }), - shouldShow: (agg) => agg.getParam('missingBucket'), - write: noop, - }, - { - name: 'exclude', - displayName: i18n.translate('data.search.aggs.buckets.terms.excludeLabel', { - defaultMessage: 'Exclude', - }), - type: 'string', - advanced: true, - shouldShow: isStringOrNumberType, - ...migrateIncludeExcludeFormat, - }, - { - name: 'include', - displayName: i18n.translate('data.search.aggs.buckets.terms.includeLabel', { - defaultMessage: 'Include', - }), - type: 'string', - advanced: true, - shouldShow: isStringOrNumberType, - ...migrateIncludeExcludeFormat, - }, - ], - }, - { getInternalStartServices } - ); + }), + type: 'string', + displayName: i18n.translate('data.search.aggs.otherBucket.labelForMissingValuesLabel', { + defaultMessage: 'Label for missing values', + }), + shouldShow: (agg) => agg.getParam('missingBucket'), + write: noop, + }, + { + name: 'exclude', + displayName: i18n.translate('data.search.aggs.buckets.terms.excludeLabel', { + defaultMessage: 'Exclude', + }), + type: 'string', + advanced: true, + shouldShow: isStringOrNumberType, + ...migrateIncludeExcludeFormat, + }, + { + name: 'include', + displayName: i18n.translate('data.search.aggs.buckets.terms.includeLabel', { + defaultMessage: 'Include', + }), + type: 'string', + advanced: true, + shouldShow: isStringOrNumberType, + ...migrateIncludeExcludeFormat, + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts index 49520863fe1cc..975941506da4e 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/avg.ts b/src/plugins/data/public/search/aggs/metrics/avg.ts index 1aa39ccd2aad9..651aaf857c757 100644 --- a/src/plugins/data/public/search/aggs/metrics/avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/avg.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const averageTitle = i18n.translate('data.search.aggs.metrics.averageTitle', { @@ -32,31 +31,22 @@ export interface AggParamsAvg extends BaseAggParams { field: string; } -export interface AvgMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getAvgMetricAgg = ({ getInternalStartServices }: AvgMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.AVG, - title: averageTitle, - makeLabel: (aggConfig) => { - return i18n.translate('data.search.aggs.metrics.averageLabel', { - defaultMessage: 'Average {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], - }, - ], +export const getAvgMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.AVG, + title: averageTitle, + makeLabel: (aggConfig) => { + return i18n.translate('data.search.aggs.metrics.averageLabel', { + defaultMessage: 'Average {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); }, - { - getInternalStartServices, - } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/avg_fn.ts index c370623b2752a..18629927d7814 100644 --- a/src/plugins/data/public/search/aggs/metrics/avg_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/avg_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts index a30e426dd2390..92fa675ac2d38 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts @@ -24,17 +24,12 @@ import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsBucketAvg extends BaseAggParams { customMetric?: AggConfigSerialized; customBucket?: AggConfigSerialized; } -export interface BucketAvgMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const overallAverageLabel = i18n.translate('data.search.aggs.metrics.overallAverageLabel', { defaultMessage: 'overall average', }); @@ -43,36 +38,29 @@ const averageBucketTitle = i18n.translate('data.search.aggs.metrics.averageBucke defaultMessage: 'Average Bucket', }); -export const getBucketAvgMetricAgg = ({ - getInternalStartServices, -}: BucketAvgMetricAggDependencies) => { +export const getBucketAvgMetricAgg = () => { const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.AVG_BUCKET, - title: averageBucketTitle, - makeLabel: (agg) => makeNestedLabel(agg, overallAverageLabel), - subtype, - params: [...params()], - getSerializedFormat, - getValue(agg, bucket) { - const customMetric = agg.getParam('customMetric'); - const customBucket = agg.getParam('customBucket'); - const scaleMetrics = customMetric.type && customMetric.type.isScalable(); + return new MetricAggType({ + name: METRIC_TYPES.AVG_BUCKET, + title: averageBucketTitle, + makeLabel: (agg) => makeNestedLabel(agg, overallAverageLabel), + subtype, + params: [...params()], + getSerializedFormat, + getValue(agg, bucket) { + const customMetric = agg.getParam('customMetric'); + const customBucket = agg.getParam('customBucket'); + const scaleMetrics = customMetric.type && customMetric.type.isScalable(); - let value = bucket[agg.id] && bucket[agg.id].value; + let value = bucket[agg.id] && bucket[agg.id].value; - if (scaleMetrics && customBucket.type.name === 'date_histogram') { - const aggInfo = customBucket.write(); + if (scaleMetrics && customBucket.type.name === 'date_histogram') { + const aggInfo = customBucket.write(); - value *= get(aggInfo, 'bucketInterval.scale', 1); - } - return value; - }, + value *= get(aggInfo, 'bucketInterval.scale', 1); + } + return value; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts index 56643a2df54bd..4e0c1d7311cd6 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts index 307c7ed3f9d38..8e2606676ec33 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts @@ -23,17 +23,12 @@ import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsBucketMax extends BaseAggParams { customMetric?: AggConfigSerialized; customBucket?: AggConfigSerialized; } -export interface BucketMaxMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const overallMaxLabel = i18n.translate('data.search.aggs.metrics.overallMaxLabel', { defaultMessage: 'overall max', }); @@ -42,22 +37,15 @@ const maxBucketTitle = i18n.translate('data.search.aggs.metrics.maxBucketTitle', defaultMessage: 'Max Bucket', }); -export const getBucketMaxMetricAgg = ({ - getInternalStartServices, -}: BucketMaxMetricAggDependencies) => { +export const getBucketMaxMetricAgg = () => { const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.MAX_BUCKET, - title: maxBucketTitle, - makeLabel: (agg) => makeNestedLabel(agg, overallMaxLabel), - subtype, - params: [...params()], - getSerializedFormat, - }, - { - getInternalStartServices, - } - ); + return new MetricAggType({ + name: METRIC_TYPES.MAX_BUCKET, + title: maxBucketTitle, + makeLabel: (agg) => makeNestedLabel(agg, overallMaxLabel), + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts index 896e9cf839605..66ae7601470fb 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts index bb4ed9d44b0b3..dedc3a9de3dd1 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts @@ -23,17 +23,12 @@ import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsBucketMin extends BaseAggParams { customMetric?: AggConfigSerialized; customBucket?: AggConfigSerialized; } -export interface BucketMinMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const overallMinLabel = i18n.translate('data.search.aggs.metrics.overallMinLabel', { defaultMessage: 'overall min', }); @@ -42,22 +37,15 @@ const minBucketTitle = i18n.translate('data.search.aggs.metrics.minBucketTitle', defaultMessage: 'Min Bucket', }); -export const getBucketMinMetricAgg = ({ - getInternalStartServices, -}: BucketMinMetricAggDependencies) => { +export const getBucketMinMetricAgg = () => { const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.MIN_BUCKET, - title: minBucketTitle, - makeLabel: (agg) => makeNestedLabel(agg, overallMinLabel), - subtype, - params: [...params()], - getSerializedFormat, - }, - { - getInternalStartServices, - } - ); + return new MetricAggType({ + name: METRIC_TYPES.MIN_BUCKET, + title: minBucketTitle, + makeLabel: (agg) => makeNestedLabel(agg, overallMinLabel), + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts index 2ae3d9211227a..009cc0102b05d 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts index dd065b52acd12..c6ccd498a0eb9 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts @@ -23,17 +23,12 @@ import { makeNestedLabel } from './lib/make_nested_label'; import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsBucketSum extends BaseAggParams { customMetric?: AggConfigSerialized; customBucket?: AggConfigSerialized; } -export interface BucketSumMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const overallSumLabel = i18n.translate('data.search.aggs.metrics.overallSumLabel', { defaultMessage: 'overall sum', }); @@ -42,22 +37,15 @@ const sumBucketTitle = i18n.translate('data.search.aggs.metrics.sumBucketTitle', defaultMessage: 'Sum Bucket', }); -export const getBucketSumMetricAgg = ({ - getInternalStartServices, -}: BucketSumMetricAggDependencies) => { +export const getBucketSumMetricAgg = () => { const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.SUM_BUCKET, - title: sumBucketTitle, - makeLabel: (agg) => makeNestedLabel(agg, overallSumLabel), - subtype, - params: [...params()], - getSerializedFormat, - }, - { - getInternalStartServices, - } - ); + return new MetricAggType({ + name: METRIC_TYPES.SUM_BUCKET, + title: sumBucketTitle, + makeLabel: (agg) => makeNestedLabel(agg, overallSumLabel), + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts index eceb11a90f293..920285e89e8f4 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality.ts b/src/plugins/data/public/search/aggs/metrics/cardinality.ts index efc79a8559566..777cb833849f4 100644 --- a/src/plugins/data/public/search/aggs/metrics/cardinality.ts +++ b/src/plugins/data/public/search/aggs/metrics/cardinality.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType, IMetricAggConfig } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const uniqueCountTitle = i18n.translate('data.search.aggs.metrics.uniqueCountTitle', { @@ -32,39 +31,28 @@ export interface AggParamsCardinality extends BaseAggParams { field: string; } -export interface CardinalityMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getCardinalityMetricAgg = ({ - getInternalStartServices, -}: CardinalityMetricAggDependencies) => - new MetricAggType( - { - name: METRIC_TYPES.CARDINALITY, - title: uniqueCountTitle, - makeLabel(aggConfig: IMetricAggConfig) { - return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', { - defaultMessage: 'Unique count of {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - getSerializedFormat(agg) { - return { - id: 'number', - }; - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter( - (type) => type !== KBN_FIELD_TYPES.HISTOGRAM - ), - }, - ], +export const getCardinalityMetricAgg = () => + new MetricAggType({ + name: METRIC_TYPES.CARDINALITY, + title: uniqueCountTitle, + makeLabel(aggConfig: IMetricAggConfig) { + return i18n.translate('data.search.aggs.metrics.uniqueCountLabel', { + defaultMessage: 'Unique count of {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); + }, + getSerializedFormat(agg) { + return { + id: 'number', + }; }, - { - getInternalStartServices, - } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter( + (type) => type !== KBN_FIELD_TYPES.HISTOGRAM + ), + }, + ], + }); diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts index f30429993638f..2542c76e7be57 100644 --- a/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/cardinality_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/count.ts b/src/plugins/data/public/search/aggs/metrics/count.ts index 86faca053a9cf..d990599586e81 100644 --- a/src/plugins/data/public/search/aggs/metrics/count.ts +++ b/src/plugins/data/public/search/aggs/metrics/count.ts @@ -20,38 +20,28 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; -import { GetInternalStartServicesFn } from '../../../types'; -export interface CountMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getCountMetricAgg = ({ getInternalStartServices }: CountMetricAggDependencies) => - new MetricAggType( - { - name: METRIC_TYPES.COUNT, - title: i18n.translate('data.search.aggs.metrics.countTitle', { +export const getCountMetricAgg = () => + new MetricAggType({ + name: METRIC_TYPES.COUNT, + title: i18n.translate('data.search.aggs.metrics.countTitle', { + defaultMessage: 'Count', + }), + hasNoDsl: true, + makeLabel() { + return i18n.translate('data.search.aggs.metrics.countLabel', { defaultMessage: 'Count', - }), - hasNoDsl: true, - makeLabel() { - return i18n.translate('data.search.aggs.metrics.countLabel', { - defaultMessage: 'Count', - }); - }, - getSerializedFormat(agg) { - return { - id: 'number', - }; - }, - getValue(agg, bucket) { - return bucket.doc_count; - }, - isScalable() { - return true; - }, + }); + }, + getSerializedFormat(agg) { + return { + id: 'number', + }; + }, + getValue(agg, bucket) { + return bucket.doc_count; + }, + isScalable() { + return true; }, - { - getInternalStartServices, - } - ); + }); diff --git a/src/plugins/data/public/search/aggs/metrics/count_fn.ts b/src/plugins/data/public/search/aggs/metrics/count_fn.ts index f4c7e8e854230..338ca18209299 100644 --- a/src/plugins/data/public/search/aggs/metrics/count_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/count_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts index 3de88bb71740f..b10bdd31a5817 100644 --- a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts @@ -23,7 +23,6 @@ import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsCumulativeSum extends BaseAggParams { buckets_path: string; @@ -31,10 +30,6 @@ export interface AggParamsCumulativeSum extends BaseAggParams { metricAgg?: string; } -export interface CumulativeSumMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const cumulativeSumLabel = i18n.translate('data.search.aggs.metrics.cumulativeSumLabel', { defaultMessage: 'cumulative sum', }); @@ -43,22 +38,15 @@ const cumulativeSumTitle = i18n.translate('data.search.aggs.metrics.cumulativeSu defaultMessage: 'Cumulative Sum', }); -export const getCumulativeSumMetricAgg = ({ - getInternalStartServices, -}: CumulativeSumMetricAggDependencies) => { +export const getCumulativeSumMetricAgg = () => { const { subtype, params, getSerializedFormat } = parentPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.CUMULATIVE_SUM, - title: cumulativeSumTitle, - makeLabel: (agg) => makeNestedLabel(agg, cumulativeSumLabel), - subtype, - params: [...params()], - getSerializedFormat, - }, - { - getInternalStartServices, - } - ); + return new MetricAggType({ + name: METRIC_TYPES.CUMULATIVE_SUM, + title: cumulativeSumTitle, + makeLabel: (agg) => makeNestedLabel(agg, cumulativeSumLabel), + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts index 950df03b10134..411cbd256c37e 100644 --- a/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/derivative.ts b/src/plugins/data/public/search/aggs/metrics/derivative.ts index c9507029080e6..c03c33ba80710 100644 --- a/src/plugins/data/public/search/aggs/metrics/derivative.ts +++ b/src/plugins/data/public/search/aggs/metrics/derivative.ts @@ -23,7 +23,6 @@ import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsDerivative extends BaseAggParams { buckets_path: string; @@ -31,10 +30,6 @@ export interface AggParamsDerivative extends BaseAggParams { metricAgg?: string; } -export interface DerivativeMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const derivativeLabel = i18n.translate('data.search.aggs.metrics.derivativeLabel', { defaultMessage: 'derivative', }); @@ -43,24 +38,17 @@ const derivativeTitle = i18n.translate('data.search.aggs.metrics.derivativeTitle defaultMessage: 'Derivative', }); -export const getDerivativeMetricAgg = ({ - getInternalStartServices, -}: DerivativeMetricAggDependencies) => { +export const getDerivativeMetricAgg = () => { const { subtype, params, getSerializedFormat } = parentPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.DERIVATIVE, - title: derivativeTitle, - makeLabel(agg) { - return makeNestedLabel(agg, derivativeLabel); - }, - subtype, - params: [...params()], - getSerializedFormat, + return new MetricAggType({ + name: METRIC_TYPES.DERIVATIVE, + title: derivativeTitle, + makeLabel(agg) { + return makeNestedLabel(agg, derivativeLabel); }, - { - getInternalStartServices, - } - ); + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts b/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts index 90b88b4de2712..1d87dfdac6da3 100644 --- a/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/derivative_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts b/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts index 864e97ca8dfe7..c86f42f066bdf 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_bounds.ts @@ -21,17 +21,12 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; export interface AggParamsGeoBounds extends BaseAggParams { field: string; } -export interface GeoBoundsMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const geoBoundsTitle = i18n.translate('data.search.aggs.metrics.geoBoundsTitle', { defaultMessage: 'Geo Bounds', }); @@ -40,24 +35,17 @@ const geoBoundsLabel = i18n.translate('data.search.aggs.metrics.geoBoundsLabel', defaultMessage: 'Geo Bounds', }); -export const getGeoBoundsMetricAgg = ({ - getInternalStartServices, -}: GeoBoundsMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.GEO_BOUNDS, - title: geoBoundsTitle, - makeLabel: () => geoBoundsLabel, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, - }, - ], - }, - { - getInternalStartServices, - } - ); +export const getGeoBoundsMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.GEO_BOUNDS, + title: geoBoundsTitle, + makeLabel: () => geoBoundsLabel, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts index 8ba71a098fc70..927f7f42d0f50 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_bounds_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts b/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts index 2bbb6b2de8d87..b98ce45d35229 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_centroid.ts @@ -21,17 +21,12 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; export interface AggParamsGeoCentroid extends BaseAggParams { field: string; } -export interface GeoCentroidMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const geoCentroidTitle = i18n.translate('data.search.aggs.metrics.geoCentroidTitle', { defaultMessage: 'Geo Centroid', }); @@ -40,27 +35,20 @@ const geoCentroidLabel = i18n.translate('data.search.aggs.metrics.geoCentroidLab defaultMessage: 'Geo Centroid', }); -export const getGeoCentroidMetricAgg = ({ - getInternalStartServices, -}: GeoCentroidMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.GEO_CENTROID, - title: geoCentroidTitle, - makeLabel: () => geoCentroidLabel, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, - }, - ], - getValue(agg, bucket) { - return bucket[agg.id] && bucket[agg.id].location; +export const getGeoCentroidMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.GEO_CENTROID, + title: geoCentroidTitle, + makeLabel: () => geoCentroidLabel, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.GEO_POINT, }, + ], + getValue(agg, bucket) { + return bucket[agg.id] && bucket[agg.id].location; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts index 464f9b535cd8b..98bd7365f8b3f 100644 --- a/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/geo_centroid_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/max.ts b/src/plugins/data/public/search/aggs/metrics/max.ts index 49cbfba5a269d..5b2f08c5b0260 100644 --- a/src/plugins/data/public/search/aggs/metrics/max.ts +++ b/src/plugins/data/public/search/aggs/metrics/max.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const maxTitle = i18n.translate('data.search.aggs.metrics.maxTitle', { @@ -32,31 +31,22 @@ export interface AggParamsMax extends BaseAggParams { field: string; } -export interface MaxMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getMaxMetricAgg = ({ getInternalStartServices }: MaxMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.MAX, - title: maxTitle, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.metrics.maxLabel', { - defaultMessage: 'Max {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], - }, - ], +export const getMaxMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.MAX, + title: maxTitle, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.maxLabel', { + defaultMessage: 'Max {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); }, - { - getInternalStartServices, - } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/max_fn.ts b/src/plugins/data/public/search/aggs/metrics/max_fn.ts index 1d68c8919fca8..d1bccd08982f8 100644 --- a/src/plugins/data/public/search/aggs/metrics/max_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/max_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/median.test.ts b/src/plugins/data/public/search/aggs/metrics/median.test.ts index b3721e2c1e679..22d907330e2a3 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.test.ts @@ -17,26 +17,16 @@ * under the License. */ -import { getMedianMetricAgg, MedianMetricAggDependencies } from './median'; +import { getMedianMetricAgg } from './median'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; - const aggTypesDependencies: MedianMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([getMedianMetricAgg(aggTypesDependencies)]); + const typesRegistry = mockAggTypesRegistry([getMedianMetricAgg()]); const field = { name: 'bytes', }; diff --git a/src/plugins/data/public/search/aggs/metrics/median.ts b/src/plugins/data/public/search/aggs/metrics/median.ts index 725fdcb2400d1..7b48a664b5fb9 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const medianTitle = i18n.translate('data.search.aggs.metrics.medianTitle', { @@ -32,43 +31,30 @@ export interface AggParamsMedian extends BaseAggParams { field: string; } -export interface MedianMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getMedianMetricAgg = ({ getInternalStartServices }: MedianMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.MEDIAN, - dslName: 'percentiles', - title: medianTitle, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.metrics.medianLabel', { - defaultMessage: 'Median {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [ - KBN_FIELD_TYPES.NUMBER, - KBN_FIELD_TYPES.DATE, - KBN_FIELD_TYPES.HISTOGRAM, - ], - write(agg, output) { - output.params.field = agg.getParam('field').name; - output.params.percents = [50]; - }, +export const getMedianMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.MEDIAN, + dslName: 'percentiles', + title: medianTitle, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.medianLabel', { + defaultMessage: 'Median {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], + write(agg, output) { + output.params.field = agg.getParam('field').name; + output.params.percents = [50]; }, - ], - getValue(agg, bucket) { - return bucket[agg.id].values['50.0']; }, + ], + getValue(agg, bucket) { + return bucket[agg.id].values['50.0']; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/median_fn.ts b/src/plugins/data/public/search/aggs/metrics/median_fn.ts index 2e8e89992136e..c5e9edb86e81c 100644 --- a/src/plugins/data/public/search/aggs/metrics/median_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/median_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts b/src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts index 5c4ff91258fb0..23dfd976aa846 100644 --- a/src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts +++ b/src/plugins/data/public/search/aggs/metrics/metric_agg_type.ts @@ -23,7 +23,6 @@ import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../agg_config'; import { METRIC_TYPES } from './metric_agg_types'; import { FieldTypes } from '../param_types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface IMetricAggConfig extends AggConfig { type: InstanceType; @@ -43,10 +42,6 @@ interface MetricAggTypeConfig subtype?: string; } -interface MetricAggTypeDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - // TODO need to make a more explicit interface for this export type IMetricAggType = MetricAggType; @@ -60,11 +55,8 @@ export class MetricAggType {}; - constructor( - config: MetricAggTypeConfig, - dependencies: MetricAggTypeDependencies - ) { - super(config, dependencies); + constructor(config: MetricAggTypeConfig) { + super(config); this.getValue = config.getValue || diff --git a/src/plugins/data/public/search/aggs/metrics/min.ts b/src/plugins/data/public/search/aggs/metrics/min.ts index 0f52aa8a4f788..6472c3ae12990 100644 --- a/src/plugins/data/public/search/aggs/metrics/min.ts +++ b/src/plugins/data/public/search/aggs/metrics/min.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const minTitle = i18n.translate('data.search.aggs.metrics.minTitle', { @@ -32,31 +31,22 @@ export interface AggParamsMin extends BaseAggParams { field: string; } -export interface MinMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getMinMetricAgg = ({ getInternalStartServices }: MinMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.MIN, - title: minTitle, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.metrics.minLabel', { - defaultMessage: 'Min {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], - }, - ], +export const getMinMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.MIN, + title: minTitle, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.minLabel', { + defaultMessage: 'Min {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); }, - { - getInternalStartServices, - } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE], + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/min_fn.ts b/src/plugins/data/public/search/aggs/metrics/min_fn.ts index b51da46a137b0..7a57c79a350fa 100644 --- a/src/plugins/data/public/search/aggs/metrics/min_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/min_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts index 61384ef1dc106..1791d49b98437 100644 --- a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts @@ -23,7 +23,6 @@ import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsMovingAvg extends BaseAggParams { buckets_path: string; @@ -33,10 +32,6 @@ export interface AggParamsMovingAvg extends BaseAggParams { metricAgg?: string; } -export interface MovingAvgMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const movingAvgTitle = i18n.translate('data.search.aggs.metrics.movingAvgTitle', { defaultMessage: 'Moving Avg', }); @@ -45,45 +40,38 @@ const movingAvgLabel = i18n.translate('data.search.aggs.metrics.movingAvgLabel', defaultMessage: 'moving avg', }); -export const getMovingAvgMetricAgg = ({ - getInternalStartServices, -}: MovingAvgMetricAggDependencies) => { +export const getMovingAvgMetricAgg = () => { const { subtype, params, getSerializedFormat } = parentPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.MOVING_FN, - dslName: 'moving_fn', - title: movingAvgTitle, - makeLabel: (agg) => makeNestedLabel(agg, movingAvgLabel), - subtype, - getSerializedFormat, - params: [ - ...params(), - { - name: 'window', - default: 5, - }, - { - name: 'script', - default: 'MovingFunctions.unweightedAvg(values)', - }, - ], - getValue(agg, bucket) { - /** - * The previous implementation using `moving_avg` did not - * return any bucket in case there are no documents or empty window. - * The `moving_fn` aggregation returns buckets with the value null if the - * window is empty or doesn't return any value if the sibiling metric - * is null. Since our generic MetricAggType.getValue implementation - * would return the value 0 for null buckets, we need a specific - * implementation here, that preserves the null value. - */ - return bucket[agg.id] ? bucket[agg.id].value : null; + return new MetricAggType({ + name: METRIC_TYPES.MOVING_FN, + dslName: 'moving_fn', + title: movingAvgTitle, + makeLabel: (agg) => makeNestedLabel(agg, movingAvgLabel), + subtype, + getSerializedFormat, + params: [ + ...params(), + { + name: 'window', + default: 5, + }, + { + name: 'script', + default: 'MovingFunctions.unweightedAvg(values)', }, + ], + getValue(agg, bucket) { + /** + * The previous implementation using `moving_avg` did not + * return any bucket in case there are no documents or empty window. + * The `moving_fn` aggregation returns buckets with the value null if the + * window is empty or doesn't return any value if the sibiling metric + * is null. Since our generic MetricAggType.getValue implementation + * would return the value 0 for null buckets, we need a specific + * implementation here, that preserves the null value. + */ + return bucket[agg.id] ? bucket[agg.id].value : null; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts index 54a3fa176385b..e1c1637d3ad1d 100644 --- a/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 91149a3f29f94..c6bba56f73ec7 100644 --- a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -24,38 +24,31 @@ import { getSerialDiffMetricAgg } from './serial_diff'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; -import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('parent pipeline aggs', function () { - const getInternalStartServices: GetInternalStartServicesFn = () => - (({ - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices); - const typesRegistry = mockAggTypesRegistry(); const metrics = [ { name: 'derivative', title: 'Derivative', - provider: getDerivativeMetricAgg({ getInternalStartServices }), + provider: getDerivativeMetricAgg(), }, { name: 'cumulative_sum', title: 'Cumulative Sum', - provider: getCumulativeSumMetricAgg({ getInternalStartServices }), + provider: getCumulativeSumMetricAgg(), }, { name: 'moving_avg', title: 'Moving Avg', - provider: getMovingAvgMetricAgg({ getInternalStartServices }), + provider: getMovingAvgMetricAgg(), dslName: 'moving_fn', }, { name: 'serial_diff', title: 'Serial Diff', - provider: getSerialDiffMetricAgg({ getInternalStartServices }), + provider: getSerialDiffMetricAgg(), }, ]; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 39e371763ed4b..348aecc23243a 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -27,7 +27,6 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { FieldFormatsStart } from '../../../field_formats'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function () { @@ -44,7 +43,6 @@ describe('AggTypesMetricsPercentileRanksProvider class', function () { getInternalStartServices: () => (({ fieldFormats, - notifications: notificationServiceMock.createStartContract(), } as unknown) as InternalStartServices), }; const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts index 1953c81a99874..3c0be229f1bbd 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts @@ -59,54 +59,49 @@ const getValueProps = (getInternalStartServices: GetInternalStartServicesFn) => export const getPercentileRanksMetricAgg = ({ getInternalStartServices, }: PercentileRanksMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.PERCENTILE_RANKS, - title: i18n.translate('data.search.aggs.metrics.percentileRanksTitle', { - defaultMessage: 'Percentile Ranks', - }), - makeLabel(agg) { - return i18n.translate('data.search.aggs.metrics.percentileRanksLabel', { - defaultMessage: 'Percentile ranks of {field}', - values: { field: agg.getFieldDisplayName() }, - }); - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], - }, - { - name: 'values', - default: [], - }, - { - write(agg, output) { - output.params.keyed = false; - }, - }, - ], - getResponseAggs(agg) { - const ValueAggConfig = getResponseAggConfigClass( - agg, - getValueProps(getInternalStartServices) - ); - const values = agg.getParam('values'); - - return values.map((value: any) => new ValueAggConfig(value)); + return new MetricAggType({ + name: METRIC_TYPES.PERCENTILE_RANKS, + title: i18n.translate('data.search.aggs.metrics.percentileRanksTitle', { + defaultMessage: 'Percentile Ranks', + }), + makeLabel(agg) { + return i18n.translate('data.search.aggs.metrics.percentileRanksLabel', { + defaultMessage: 'Percentile ranks of {field}', + values: { field: agg.getFieldDisplayName() }, + }); + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], }, - getSerializedFormat(agg) { - return { - id: 'percent', - }; + { + name: 'values', + default: [], }, - getValue(agg, bucket) { - return getPercentileValue(agg, bucket) / 100; + { + write(agg, output) { + output.params.keyed = false; + }, }, + ], + getResponseAggs(agg) { + const ValueAggConfig = getResponseAggConfigClass( + agg, + getValueProps(getInternalStartServices) + ); + const values = agg.getParam('values'); + + return values.map((value: any) => new ValueAggConfig(value)); + }, + getSerializedFormat(agg) { + return { + id: 'percent', + }; + }, + getValue(agg, bucket) { + return getPercentileValue(agg, bucket) / 100; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts index 851e938f28c1c..08e1489a856dd 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 52ab325ac5806..a44c0e5075ef9 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -17,30 +17,16 @@ * under the License. */ -import { - IPercentileAggConfig, - getPercentilesMetricAgg, - PercentilesMetricAggDependencies, -} from './percentiles'; +import { IPercentileAggConfig, getPercentilesMetricAgg } from './percentiles'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; - const aggTypesDependencies: PercentilesMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; beforeEach(() => { - const typesRegistry = mockAggTypesRegistry([getPercentilesMetricAgg(aggTypesDependencies)]); + const typesRegistry = mockAggTypesRegistry([getPercentilesMetricAgg()]); const field = { name: 'bytes', }; @@ -72,7 +58,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }); it('uses the custom label if it is set', () => { - const responseAggs: any = getPercentilesMetricAgg(aggTypesDependencies).getResponseAggs( + const responseAggs: any = getPercentilesMetricAgg().getResponseAggs( aggConfigs.aggs[0] as IPercentileAggConfig ); diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.ts index ad3c19cfaffcc..8ea493f324811 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.ts @@ -24,7 +24,6 @@ import { KBN_FIELD_TYPES } from '../../../../common'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; import { ordinalSuffix } from './lib/ordinal_suffix'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; export interface AggParamsPercentiles extends BaseAggParams { @@ -34,10 +33,6 @@ export interface AggParamsPercentiles extends BaseAggParams { export type IPercentileAggConfig = IResponseAggConfig; -export interface PercentilesMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const valueProps = { makeLabel(this: IPercentileAggConfig) { const customLabel = this.getParam('customLabel'); @@ -50,51 +45,40 @@ const valueProps = { }, }; -export const getPercentilesMetricAgg = ({ - getInternalStartServices, -}: PercentilesMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.PERCENTILES, - title: i18n.translate('data.search.aggs.metrics.percentilesTitle', { - defaultMessage: 'Percentiles', - }), - makeLabel(agg) { - return i18n.translate('data.search.aggs.metrics.percentilesLabel', { - defaultMessage: 'Percentiles of {field}', - values: { field: agg.getFieldDisplayName() }, - }); +export const getPercentilesMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.PERCENTILES, + title: i18n.translate('data.search.aggs.metrics.percentilesTitle', { + defaultMessage: 'Percentiles', + }), + makeLabel(agg) { + return i18n.translate('data.search.aggs.metrics.percentilesLabel', { + defaultMessage: 'Percentiles of {field}', + values: { field: agg.getFieldDisplayName() }, + }); + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [ - KBN_FIELD_TYPES.NUMBER, - KBN_FIELD_TYPES.DATE, - KBN_FIELD_TYPES.HISTOGRAM, - ], - }, - { - name: 'percents', - default: [1, 5, 25, 50, 75, 95, 99], - }, - { - write(agg, output) { - output.params.keyed = false; - }, + { + name: 'percents', + default: [1, 5, 25, 50, 75, 95, 99], + }, + { + write(agg, output) { + output.params.keyed = false; }, - ], - getResponseAggs(agg) { - const ValueAggConfig = getResponseAggConfigClass(agg, valueProps); - - return agg.getParam('percents').map((percent: any) => new ValueAggConfig(percent)); }, + ], + getResponseAggs(agg) { + const ValueAggConfig = getResponseAggConfigClass(agg, valueProps); - getValue: getPercentileValue, + return agg.getParam('percents').map((percent: any) => new ValueAggConfig(percent)); }, - { - getInternalStartServices, - } - ); + + getValue: getPercentileValue, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts index b799be07925fa..eb8952267f5ea 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts index 9ea01be11fe8f..a4e4d7a8990fa 100644 --- a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts @@ -23,7 +23,6 @@ import { parentPipelineAggHelper } from './lib/parent_pipeline_agg_helper'; import { makeNestedLabel } from './lib/make_nested_label'; import { METRIC_TYPES } from './metric_agg_types'; import { AggConfigSerialized, BaseAggParams } from '../types'; -import { GetInternalStartServicesFn } from '../../../types'; export interface AggParamsSerialDiff extends BaseAggParams { buckets_path: string; @@ -31,10 +30,6 @@ export interface AggParamsSerialDiff extends BaseAggParams { metricAgg?: string; } -export interface SerialDiffMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const serialDiffTitle = i18n.translate('data.search.aggs.metrics.serialDiffTitle', { defaultMessage: 'Serial Diff', }); @@ -43,22 +38,15 @@ const serialDiffLabel = i18n.translate('data.search.aggs.metrics.serialDiffLabel defaultMessage: 'serial diff', }); -export const getSerialDiffMetricAgg = ({ - getInternalStartServices, -}: SerialDiffMetricAggDependencies) => { +export const getSerialDiffMetricAgg = () => { const { subtype, params, getSerializedFormat } = parentPipelineAggHelper; - return new MetricAggType( - { - name: METRIC_TYPES.SERIAL_DIFF, - title: serialDiffTitle, - makeLabel: (agg) => makeNestedLabel(agg, serialDiffLabel), - subtype, - params: [...params()], - getSerializedFormat, - }, - { - getInternalStartServices, - } - ); + return new MetricAggType({ + name: METRIC_TYPES.SERIAL_DIFF, + title: serialDiffTitle, + makeLabel: (agg) => makeNestedLabel(agg, serialDiffLabel), + subtype, + params: [...params()], + getSerializedFormat, + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts index 9ba313aff7386..3cc1dacb87b3d 100644 --- a/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff_fn.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index f08e850caadce..a157d225c839c 100644 --- a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -25,37 +25,30 @@ import { getBucketMaxMetricAgg } from './bucket_max'; import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; -import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('sibling pipeline aggs', () => { - const getInternalStartServices: GetInternalStartServicesFn = () => - (({ - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices); - const typesRegistry = mockAggTypesRegistry(); const metrics = [ { name: 'sum_bucket', title: 'Overall Sum', - provider: getBucketSumMetricAgg({ getInternalStartServices }), + provider: getBucketSumMetricAgg(), }, { name: 'avg_bucket', title: 'Overall Average', - provider: getBucketAvgMetricAgg({ getInternalStartServices }), + provider: getBucketAvgMetricAgg(), }, { name: 'min_bucket', title: 'Overall Min', - provider: getBucketMinMetricAgg({ getInternalStartServices }), + provider: getBucketMinMetricAgg(), }, { name: 'max_bucket', title: 'Overall Max', - provider: getBucketMaxMetricAgg({ getInternalStartServices }), + provider: getBucketMaxMetricAgg(), }, ]; diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts index 2fa207e62771e..c3efe95f44a56 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -17,27 +17,13 @@ * under the License. */ -import { - IStdDevAggConfig, - getStdDeviationMetricAgg, - StdDeviationMetricAggDependencies, -} from './std_deviation'; +import { IStdDevAggConfig, getStdDeviationMetricAgg } from './std_deviation'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; describe('AggTypeMetricStandardDeviationProvider class', () => { - const aggTypesDependencies: StdDeviationMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg(aggTypesDependencies)]); + const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg()]); const getAggConfigs = (customLabel?: string) => { const field = { name: 'memory', @@ -72,7 +58,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { it('uses the custom label if it is set', () => { const aggConfigs = getAggConfigs('custom label'); - const responseAggs: any = getStdDeviationMetricAgg(aggTypesDependencies).getResponseAggs( + const responseAggs: any = getStdDeviationMetricAgg().getResponseAggs( aggConfigs.aggs[0] as IStdDevAggConfig ); @@ -86,7 +72,7 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { it('uses the default labels if custom label is not set', () => { const aggConfigs = getAggConfigs(); - const responseAggs: any = getStdDeviationMetricAgg(aggTypesDependencies).getResponseAggs( + const responseAggs: any = getStdDeviationMetricAgg().getResponseAggs( aggConfigs.aggs[0] as IStdDevAggConfig ); diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.ts index 5d987dc2cbbe9..9aba063776252 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.ts @@ -23,7 +23,6 @@ import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; export interface AggParamsStdDeviation extends BaseAggParams { @@ -36,12 +35,8 @@ interface ValProp { } export interface IStdDevAggConfig extends IResponseAggConfig { - keyedDetails: (customLabel: string, fieldDisplayName?: string) => { [key: string]: ValProp }; - valProp: () => ValProp; -} - -export interface StdDeviationMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; + keyedDetails: (customLabel: string, fieldDisplayName?: string) => Record; + valProp: () => string[]; } const responseAggConfigProps = { @@ -85,42 +80,35 @@ const responseAggConfigProps = { }, }; -export const getStdDeviationMetricAgg = ({ - getInternalStartServices, -}: StdDeviationMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.STD_DEV, - dslName: 'extended_stats', - title: i18n.translate('data.search.aggs.metrics.standardDeviationTitle', { - defaultMessage: 'Standard Deviation', - }), - makeLabel(agg) { - return i18n.translate('data.search.aggs.metrics.standardDeviationLabel', { - defaultMessage: 'Standard Deviation of {field}', - values: { field: agg.getFieldDisplayName() }, - }); +export const getStdDeviationMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.STD_DEV, + dslName: 'extended_stats', + title: i18n.translate('data.search.aggs.metrics.standardDeviationTitle', { + defaultMessage: 'Standard Deviation', + }), + makeLabel(agg) { + return i18n.translate('data.search.aggs.metrics.standardDeviationLabel', { + defaultMessage: 'Standard Deviation of {field}', + values: { field: agg.getFieldDisplayName() }, + }); + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: KBN_FIELD_TYPES.NUMBER, }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: KBN_FIELD_TYPES.NUMBER, - }, - ], + ], - getResponseAggs(agg) { - const ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps); + getResponseAggs(agg) { + const ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps); - return [new ValueAggConfig('std_lower'), new ValueAggConfig('std_upper')]; - }, + return [new ValueAggConfig('std_lower'), new ValueAggConfig('std_upper')]; + }, - getValue(agg, bucket) { - return get(bucket[agg.parentId], agg.valProp() as any); - }, + getValue(agg, bucket) { + return get(bucket[agg.parentId], agg.valProp()); }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts index 70623e2e48041..61b8a6f28f088 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/sum.ts b/src/plugins/data/public/search/aggs/metrics/sum.ts index 66fad89316613..fa44af98554da 100644 --- a/src/plugins/data/public/search/aggs/metrics/sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/sum.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; const sumTitle = i18n.translate('data.search.aggs.metrics.sumTitle', { @@ -32,34 +31,25 @@ export interface AggParamsSum extends BaseAggParams { field: string; } -export interface SumMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - -export const getSumMetricAgg = ({ getInternalStartServices }: SumMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.SUM, - title: sumTitle, - makeLabel(aggConfig) { - return i18n.translate('data.search.aggs.metrics.sumLabel', { - defaultMessage: 'Sum of {field}', - values: { field: aggConfig.getFieldDisplayName() }, - }); - }, - isScalable() { - return true; - }, - params: [ - { - name: 'field', - type: 'field', - filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], - }, - ], +export const getSumMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.SUM, + title: sumTitle, + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.sumLabel', { + defaultMessage: 'Sum of {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); + }, + isScalable() { + return true; }, - { - getInternalStartServices, - } - ); + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM], + }, + ], + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/sum_fn.ts b/src/plugins/data/public/search/aggs/metrics/sum_fn.ts index a277aef02693f..e625befc8f1d9 100644 --- a/src/plugins/data/public/search/aggs/metrics/sum_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/sum_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts index 94a970a72a46f..cd5b4a2f724bd 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -18,25 +18,15 @@ */ import { dropRight, last } from 'lodash'; -import { getTopHitMetricAgg, TopHitMetricAggDependencies } from './top_hit'; +import { getTopHitMetricAgg } from './top_hit'; import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig } from './metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; describe('Top hit metric', () => { let aggDsl: Record; let aggConfig: IMetricAggConfig; - const aggTypesDependencies: TopHitMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; const init = ({ fieldName = 'field', @@ -46,7 +36,7 @@ describe('Top hit metric', () => { fieldType = KBN_FIELD_TYPES.NUMBER, size = 1, }: any) => { - const typesRegistry = mockAggTypesRegistry([getTopHitMetricAgg(aggTypesDependencies)]); + const typesRegistry = mockAggTypesRegistry([getTopHitMetricAgg()]); const field = { name: fieldName, displayName: fieldName, @@ -101,7 +91,7 @@ describe('Top hit metric', () => { it('should return a label prefixed with Last if sorting in descending order', () => { init({ fieldName: 'bytes' }); - expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual('Last bytes'); + expect(getTopHitMetricAgg().makeLabel(aggConfig)).toEqual('Last bytes'); }); it('should return a label prefixed with First if sorting in ascending order', () => { @@ -109,7 +99,7 @@ describe('Top hit metric', () => { fieldName: 'bytes', sortOrder: 'asc', }); - expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual('First bytes'); + expect(getTopHitMetricAgg().makeLabel(aggConfig)).toEqual('First bytes'); }); it('should request the _source field', () => { @@ -150,7 +140,7 @@ describe('Top hit metric', () => { }; init({ fieldName: '@tags' }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(null); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toBe(null); }); // it('should return undefined if the field does not appear in the source', () => { @@ -169,7 +159,7 @@ describe('Top hit metric', () => { }; init({ fieldName: '@tags' }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(undefined); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toBe(undefined); }); it('should return the field value from the top hit', () => { @@ -188,7 +178,7 @@ describe('Top hit metric', () => { }; init({ fieldName: '@tags' }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe('aaa'); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toBe('aaa'); }); it('should return the object if the field value is an object', () => { @@ -210,7 +200,7 @@ describe('Top hit metric', () => { init({ fieldName: '@tags' }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual({ + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toEqual({ label: 'aaa', }); }); @@ -231,10 +221,7 @@ describe('Top hit metric', () => { }; init({ fieldName: '@tags' }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual([ - 'aaa', - 'bbb', - ]); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toEqual(['aaa', 'bbb']); }); it('should return undefined if the field is not in the source nor in the doc_values field', () => { @@ -256,7 +243,7 @@ describe('Top hit metric', () => { }; init({ fieldName: 'machine.os.raw', readFromDocValues: true }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toBe(undefined); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toBe(undefined); }); describe('Multivalued field and first/last X docs', () => { @@ -265,9 +252,7 @@ describe('Top hit metric', () => { fieldName: 'bytes', size: 2, }); - expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual( - 'Last 2 bytes' - ); + expect(getTopHitMetricAgg().makeLabel(aggConfig)).toEqual('Last 2 bytes'); }); it('should return a label prefixed with First X docs if sorting in ascending order', () => { @@ -276,9 +261,7 @@ describe('Top hit metric', () => { size: 2, sortOrder: 'asc', }); - expect(getTopHitMetricAgg(aggTypesDependencies).makeLabel(aggConfig)).toEqual( - 'First 2 bytes' - ); + expect(getTopHitMetricAgg().makeLabel(aggConfig)).toEqual('First 2 bytes'); }); [ @@ -353,9 +336,7 @@ describe('Top hit metric', () => { }; init({ fieldName: 'bytes', aggregate: agg.type }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual( - agg.result - ); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toEqual(agg.result); }); it(`should return the result of the ${agg.type} aggregation over the last X docs - ${agg.description}`, () => { @@ -379,9 +360,7 @@ describe('Top hit metric', () => { }; init({ fieldName: 'bytes', aggregate: agg.type }); - expect(getTopHitMetricAgg(aggTypesDependencies).getValue(aggConfig, bucket)).toEqual( - agg.result - ); + expect(getTopHitMetricAgg().getValue(aggConfig, bucket)).toEqual(agg.result); }); }); }); diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.ts index c6890f98b20e4..5ca883e60afd3 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.ts @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../common'; -import { GetInternalStartServicesFn } from '../../../types'; import { BaseAggParams } from '../types'; export interface AggParamsTopHit extends BaseAggParams { @@ -33,235 +32,223 @@ export interface AggParamsTopHit extends BaseAggParams { sortOrder?: 'desc' | 'asc'; } -export interface TopHitMetricAggDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - const isNumericFieldSelected = (agg: IMetricAggConfig) => { const field = agg.getParam('field'); return field && field.type && field.type === KBN_FIELD_TYPES.NUMBER; }; -export const getTopHitMetricAgg = ({ getInternalStartServices }: TopHitMetricAggDependencies) => { - return new MetricAggType( - { - name: METRIC_TYPES.TOP_HITS, - title: i18n.translate('data.search.aggs.metrics.topHitTitle', { - defaultMessage: 'Top Hit', - }), - makeLabel(aggConfig) { - const lastPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.lastPrefixLabel', { - defaultMessage: 'Last', - }); - const firstPrefixLabel = i18n.translate( - 'data.search.aggs.metrics.topHit.firstPrefixLabel', - { - defaultMessage: 'First', - } - ); +export const getTopHitMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.TOP_HITS, + title: i18n.translate('data.search.aggs.metrics.topHitTitle', { + defaultMessage: 'Top Hit', + }), + makeLabel(aggConfig) { + const lastPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.lastPrefixLabel', { + defaultMessage: 'Last', + }); + const firstPrefixLabel = i18n.translate('data.search.aggs.metrics.topHit.firstPrefixLabel', { + defaultMessage: 'First', + }); - let prefix = - aggConfig.getParam('sortOrder').value === 'desc' ? lastPrefixLabel : firstPrefixLabel; + let prefix = + aggConfig.getParam('sortOrder').value === 'desc' ? lastPrefixLabel : firstPrefixLabel; - const size = aggConfig.getParam('size'); + const size = aggConfig.getParam('size'); - if (size !== 1) { - prefix += ` ${size}`; - } - - const field = aggConfig.getParam('field'); + if (size !== 1) { + prefix += ` ${size}`; + } - return `${prefix} ${field ? field.displayName : ''}`; - }, - params: [ - { - name: 'field', - type: 'field', - onlyAggregatable: false, - filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter( - (type) => type !== KBN_FIELD_TYPES.HISTOGRAM - ), - write(agg, output) { - const field = agg.getParam('field'); - output.params = {}; + const field = aggConfig.getParam('field'); - if (field.scripted) { - output.params.script_fields = { - [field.name]: { - script: { - source: field.script, - lang: field.lang, - }, + return `${prefix} ${field ? field.displayName : ''}`; + }, + params: [ + { + name: 'field', + type: 'field', + onlyAggregatable: false, + filterFieldTypes: Object.values(KBN_FIELD_TYPES).filter( + (type) => type !== KBN_FIELD_TYPES.HISTOGRAM + ), + write(agg, output) { + const field = agg.getParam('field'); + output.params = {}; + + if (field.scripted) { + output.params.script_fields = { + [field.name]: { + script: { + source: field.script, + lang: field.lang, }, - }; - } else { - if (field.readFromDocValues) { - // always format date fields as date_time to avoid - // displaying unformatted dates like epoch_millis - // or other not-accepted momentjs formats - const format = - field.type === KBN_FIELD_TYPES.DATE ? 'date_time' : 'use_field_mapping'; - output.params.docvalue_fields = [{ field: field.name, format }]; - } - output.params._source = field.name === '_source' ? true : field.name; + }, + }; + } else { + if (field.readFromDocValues) { + // always format date fields as date_time to avoid + // displaying unformatted dates like epoch_millis + // or other not-accepted momentjs formats + const format = + field.type === KBN_FIELD_TYPES.DATE ? 'date_time' : 'use_field_mapping'; + output.params.docvalue_fields = [{ field: field.name, format }]; } - }, + output.params._source = field.name === '_source' ? true : field.name; + } }, - { - name: 'aggregate', - type: 'optioned', - options: [ - { - text: i18n.translate('data.search.aggs.metrics.topHit.minLabel', { - defaultMessage: 'Min', - }), - isCompatible: isNumericFieldSelected, - disabled: true, - value: 'min', - }, - { - text: i18n.translate('data.search.aggs.metrics.topHit.maxLabel', { - defaultMessage: 'Max', - }), - isCompatible: isNumericFieldSelected, - disabled: true, - value: 'max', - }, - { - text: i18n.translate('data.search.aggs.metrics.topHit.sumLabel', { - defaultMessage: 'Sum', - }), - isCompatible: isNumericFieldSelected, - disabled: true, - value: 'sum', - }, - { - text: i18n.translate('data.search.aggs.metrics.topHit.averageLabel', { - defaultMessage: 'Average', - }), - isCompatible: isNumericFieldSelected, - disabled: true, - value: 'average', - }, - { - text: i18n.translate('data.search.aggs.metrics.topHit.concatenateLabel', { - defaultMessage: 'Concatenate', - }), - isCompatible(aggConfig: IMetricAggConfig) { - return _.get(aggConfig.params, 'field.filterFieldTypes', '*') === '*'; - }, - disabled: true, - value: 'concat', + }, + { + name: 'aggregate', + type: 'optioned', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topHit.minLabel', { + defaultMessage: 'Min', + }), + isCompatible: isNumericFieldSelected, + disabled: true, + value: 'min', + }, + { + text: i18n.translate('data.search.aggs.metrics.topHit.maxLabel', { + defaultMessage: 'Max', + }), + isCompatible: isNumericFieldSelected, + disabled: true, + value: 'max', + }, + { + text: i18n.translate('data.search.aggs.metrics.topHit.sumLabel', { + defaultMessage: 'Sum', + }), + isCompatible: isNumericFieldSelected, + disabled: true, + value: 'sum', + }, + { + text: i18n.translate('data.search.aggs.metrics.topHit.averageLabel', { + defaultMessage: 'Average', + }), + isCompatible: isNumericFieldSelected, + disabled: true, + value: 'average', + }, + { + text: i18n.translate('data.search.aggs.metrics.topHit.concatenateLabel', { + defaultMessage: 'Concatenate', + }), + isCompatible(aggConfig: IMetricAggConfig) { + return _.get(aggConfig.params, 'field.filterFieldTypes', '*') === '*'; }, - ], - write: _.noop, - }, - { - name: 'size', - default: 1, - }, - { - name: 'sortField', - type: 'field', - filterFieldTypes: [ - KBN_FIELD_TYPES.NUMBER, - KBN_FIELD_TYPES.DATE, - KBN_FIELD_TYPES.IP, - KBN_FIELD_TYPES.STRING, - ], - default(agg: IMetricAggConfig) { - return agg.getIndexPattern().timeFieldName; + disabled: true, + value: 'concat', }, - write: _.noop, // prevent default write, it is handled below + ], + write: _.noop, + }, + { + name: 'size', + default: 1, + }, + { + name: 'sortField', + type: 'field', + filterFieldTypes: [ + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.DATE, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.STRING, + ], + default(agg: IMetricAggConfig) { + return agg.getIndexPattern().timeFieldName; }, - { - name: 'sortOrder', - type: 'optioned', - default: 'desc', - options: [ - { - text: i18n.translate('data.search.aggs.metrics.topHit.descendingLabel', { - defaultMessage: 'Descending', - }), - value: 'desc', - }, - { - text: i18n.translate('data.search.aggs.metrics.topHit.ascendingLabel', { - defaultMessage: 'Ascending', - }), - value: 'asc', - }, - ], - write(agg, output) { - const sortField = agg.params.sortField; - const sortOrder = agg.params.sortOrder; - - if (sortField.scripted) { - output.params.sort = [ - { - _script: { - script: { - source: sortField.script, - lang: sortField.lang, - }, - type: sortField.type, - order: sortOrder.value, + write: _.noop, // prevent default write, it is handled below + }, + { + name: 'sortOrder', + type: 'optioned', + default: 'desc', + options: [ + { + text: i18n.translate('data.search.aggs.metrics.topHit.descendingLabel', { + defaultMessage: 'Descending', + }), + value: 'desc', + }, + { + text: i18n.translate('data.search.aggs.metrics.topHit.ascendingLabel', { + defaultMessage: 'Ascending', + }), + value: 'asc', + }, + ], + write(agg, output) { + const sortField = agg.params.sortField; + const sortOrder = agg.params.sortOrder; + + if (sortField.scripted) { + output.params.sort = [ + { + _script: { + script: { + source: sortField.script, + lang: sortField.lang, }, + type: sortField.type, + order: sortOrder.value, }, - ]; - } else { - output.params.sort = [ - { - [sortField.name]: { - order: sortOrder.value, - }, + }, + ]; + } else { + output.params.sort = [ + { + [sortField.name]: { + order: sortOrder.value, }, - ]; - } - }, + }, + ]; + } }, - ], - getValue(agg, bucket) { - const hits: any[] = _.get(bucket, `${agg.id}.hits.hits`); - if (!hits || !hits.length) { + }, + ], + getValue(agg, bucket) { + const hits: any[] = _.get(bucket, `${agg.id}.hits.hits`); + if (!hits || !hits.length) { + return null; + } + const path = agg.getParam('field').name; + + let values = _.flatten( + hits.map((hit) => + path === '_source' ? hit._source : agg.getIndexPattern().flattenHit(hit, true)[path] + ) + ); + + if (values.length === 1) { + values = values[0]; + } + + if (Array.isArray(values)) { + if (!_.compact(values).length) { return null; } - const path = agg.getParam('field').name; - - let values = _.flatten( - hits.map((hit) => - path === '_source' ? hit._source : agg.getIndexPattern().flattenHit(hit, true)[path] - ) - ); - - if (values.length === 1) { - values = values[0]; - } - - if (Array.isArray(values)) { - if (!_.compact(values).length) { - return null; - } - const aggregate = agg.getParam('aggregate'); - - switch (aggregate.value) { - case 'max': - return _.max(values); - case 'min': - return _.min(values); - case 'sum': - return _.sum(values); - case 'average': - return _.sum(values) / values.length; - } + const aggregate = agg.getParam('aggregate'); + + switch (aggregate.value) { + case 'max': + return _.max(values); + case 'min': + return _.min(values); + case 'sum': + return _.sum(values); + case 'average': + return _.sum(values) / values.length; } - return values; - }, + } + return values; }, - { - getInternalStartServices, - } - ); + }); }; diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts index adfd22b540e06..e0c3fd0d070b2 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit_fn.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; import { getParsedValue } from '../utils/get_parsed_value'; diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts index 79e1cf2a540d1..1ba8a75e98cbe 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/public/search/aggs/param_types/base.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ExpressionAstFunction } from 'src/plugins/expressions/public'; +import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; import { FetchOptions } from '../../fetch'; diff --git a/src/plugins/data/public/search/aggs/param_types/field.test.ts b/src/plugins/data/public/search/aggs/param_types/field.test.ts index 2c51d9709f906..0182471392910 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.test.ts @@ -18,22 +18,11 @@ */ import { BaseParamType } from './base'; -import { FieldParamType, FieldParamTypeDependencies } from './field'; +import { FieldParamType } from './field'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../common'; import { IAggConfig } from '../agg_config'; -import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; -import { InternalStartServices } from '../../../types'; describe('Field', () => { - const fieldParamTypeDependencies: FieldParamTypeDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; - const indexPattern = { id: '1234', title: 'logstash-*', @@ -63,13 +52,10 @@ describe('Field', () => { describe('constructor', () => { it('it is an instance of BaseParamType', () => { - const aggParam = new FieldParamType( - { - name: 'field', - type: 'field', - }, - fieldParamTypeDependencies - ); + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + }); expect(aggParam instanceof BaseParamType).toBeTruthy(); }); @@ -77,13 +63,10 @@ describe('Field', () => { describe('getAvailableFields', () => { it('should return only aggregatable fields by default', () => { - const aggParam = new FieldParamType( - { - name: 'field', - type: 'field', - }, - fieldParamTypeDependencies - ); + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + }); const fields = aggParam.getAvailableFields(agg); @@ -95,13 +78,10 @@ describe('Field', () => { }); it('should return all fields if onlyAggregatable is false', () => { - const aggParam = new FieldParamType( - { - name: 'field', - type: 'field', - }, - fieldParamTypeDependencies - ); + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + }); aggParam.onlyAggregatable = false; @@ -111,13 +91,10 @@ describe('Field', () => { }); it('should return all fields if filterFieldTypes was not specified', () => { - const aggParam = new FieldParamType( - { - name: 'field', - type: 'field', - }, - fieldParamTypeDependencies - ); + const aggParam = new FieldParamType({ + name: 'field', + type: 'field', + }); indexPattern.fields[1].aggregatable = true; diff --git a/src/plugins/data/public/search/aggs/param_types/field.ts b/src/plugins/data/public/search/aggs/param_types/field.ts index 63dbed9cec612..cb3617b02e882 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.ts @@ -19,12 +19,11 @@ import { i18n } from '@kbn/i18n'; import { IAggConfig } from '../agg_config'; -import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/public'; +import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common'; import { BaseParamType } from './base'; import { propFilter } from '../utils'; import { isNestedField, KBN_FIELD_TYPES } from '../../../../common'; import { Field as IndexPatternField } from '../../../index_patterns'; -import { GetInternalStartServicesFn } from '../../../types'; const filterByType = propFilter('type'); @@ -32,20 +31,13 @@ export type FieldTypes = KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; // TODO need to make a more explicit interface for this export type IFieldParamType = FieldParamType; -export interface FieldParamTypeDependencies { - getInternalStartServices: GetInternalStartServicesFn; -} - export class FieldParamType extends BaseParamType { required = true; scriptable = true; filterFieldTypes: FieldTypes; onlyAggregatable: boolean; - constructor( - config: Record, - { getInternalStartServices }: FieldParamTypeDependencies - ) { + constructor(config: Record) { super(config); this.filterFieldTypes = config.filterFieldTypes || '*'; @@ -91,10 +83,9 @@ export class FieldParamType extends BaseParamType { throw new SavedObjectNotFound('index-pattern-field', fieldName); } - // @ts-ignore const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName); if (!validField) { - getInternalStartServices().notifications.toasts.addDanger( + throw new Error( i18n.translate( 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts index aa27bab8f4bd8..283e9107d3bb4 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts @@ -22,7 +22,7 @@ import { AnyExpressionFunctionDefinition, ExpressionFunctionDefinition, ExecutionContext, -} from '../../../../../../plugins/expressions/public'; +} from 'src/plugins/expressions/common'; /** * Takes a function spec and passes in default args, diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts index 63f8ae0ce5f58..d47317d8b4725 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts @@ -19,4 +19,3 @@ export { functionWrapper } from './function_wrapper'; export { mockAggTypesRegistry } from './mock_agg_types_registry'; -export { mockDataServices } from './mock_data_services'; diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts index 385d0cd6c6b39..4a0820c349b5f 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -17,7 +17,7 @@ * under the License. */ -import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; import { getAggTypes } from '../agg_types'; import { BucketAggType } from '../buckets/bucket_agg_type'; @@ -74,17 +74,11 @@ export function mockAggTypesRegistry | MetricAggTyp const coreSetup = coreMock.createSetup(); coreSetup.uiSettings.get = mockUiSettings; - const coreStart = coreMock.createStart(); - coreSetup.uiSettings.get = mockUiSettings; - const aggTypes = getAggTypes({ calculateBounds: jest.fn(), getInternalStartServices: () => (({ fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - uiSettings: coreStart.uiSettings, - injectedMetadata: coreStart.injectedMetadata, } as unknown) as InternalStartServices), uiSettings: coreSetup.uiSettings, }); diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_data_services.ts b/src/plugins/data/public/search/aggs/test_helpers/mock_data_services.ts deleted file mode 100644 index e807e084c683a..0000000000000 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_data_services.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { coreMock } from '../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../mocks'; -import { - setFieldFormats, - setIndexPatterns, - setInjectedMetadata, - setNotifications, - setOverlays, - setQueryService, - setSearchService, - setUiSettings, -} from '../../../services'; - -/** - * Testing helper which calls all of the service setters used in the - * data plugin. Services are added using their provided mocks. - * - * @internal - */ -export function mockDataServices() { - const core = coreMock.createStart(); - const data = dataPluginMock.createStartContract(); - - setFieldFormats(data.fieldFormats); - setIndexPatterns(data.indexPatterns); - setInjectedMetadata(core.injectedMetadata); - setNotifications(core.notifications); - setOverlays(core.overlays); - setQueryService(data.query); - setSearchService(data.search); - setUiSettings(core.uiSettings); - - return { - core, - data, - }; -} diff --git a/src/plugins/data/public/search/expressions/create_filter.test.ts b/src/plugins/data/public/search/expressions/create_filter.test.ts index 23da060cba203..a7fd67983cb92 100644 --- a/src/plugins/data/public/search/expressions/create_filter.test.ts +++ b/src/plugins/data/public/search/expressions/create_filter.test.ts @@ -21,7 +21,7 @@ import { createFilter } from './create_filter'; import { AggConfigs, IAggConfig } from '../aggs'; import { TabbedTable } from '../tabify'; import { isRangeFilter, BytesFormat, FieldFormatsGetConfigFn } from '../../../common'; -import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; +import { mockAggTypesRegistry } from '../aggs/test_helpers'; describe('createFilter', () => { let table: TabbedTable; @@ -83,7 +83,6 @@ describe('createFilter', () => { }, ], }; - mockDataServices(); }); test('ignores event when cell value is not provided', async () => { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index c2446e9cc5555..4ac6c823d2e3b 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -24,7 +24,7 @@ import { KibanaDatatable, ExpressionFunctionDefinition, KibanaDatatableColumn, -} from '../../../../../plugins/expressions/public'; +} from 'src/plugins/expressions/public'; import { calculateObjectHash } from '../../../../../plugins/kibana_utils/public'; import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -201,7 +201,7 @@ const handleCourierRequest = async ({ aggs, agg, requestSearchSource, - inspectorAdapters, + inspectorAdapters.requests, abortSignal ); } diff --git a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts index e54c20caacb26..96d0aaa16f6ba 100644 --- a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts @@ -26,12 +26,12 @@ import { i18n } from '@kbn/i18n'; import { SearchResponse } from 'elasticsearch'; -import { RequestInspectorStats } from './types'; +import { RequestStatistics } from 'src/plugins/inspector/common'; import { ISearchSource } from '../../search_source'; /** @public */ export function getRequestInspectorStats(searchSource: ISearchSource) { - const stats: RequestInspectorStats = {}; + const stats: RequestStatistics = {}; const index = searchSource.getField('index'); if (index) { @@ -65,7 +65,7 @@ export function getResponseInspectorStats( resp: SearchResponse ) { const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; - const stats: RequestInspectorStats = {}; + const stats: RequestStatistics = {}; if (resp && resp.took) { stats.queryTime = { diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts index 35f0181f63302..7df4f5c947583 100644 --- a/src/plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/plugins/data/public/search/tabify/get_columns.test.ts @@ -20,26 +20,27 @@ import { tabifyGetColumns } from './get_columns'; import { TabbedAggColumn } from './types'; import { AggConfigs } from '../aggs'; -import { mockAggTypesRegistry, mockDataServices } from '../aggs/test_helpers'; +import { mockAggTypesRegistry } from '../aggs/test_helpers'; describe('get columns', () => { - beforeEach(() => { - mockDataServices(); - }); - const typesRegistry = mockAggTypesRegistry(); const createAggConfigs = (aggs: any[] = []) => { - const field = { - name: '@timestamp', - }; + const fields = [ + { + name: '@timestamp', + }, + { + name: 'bytes', + }, + ]; const indexPattern = { id: '1234', title: 'logstash-*', fields: { - getByName: () => field, - filter: () => [field], + getByName: (name: string) => fields.find((f) => f.name === name), + filter: () => fields, }, } as any; diff --git a/src/plugins/data/public/search/tabify/response_writer.test.ts b/src/plugins/data/public/search/tabify/response_writer.test.ts index 94473d23ccc39..5a5ef8bd29524 100644 --- a/src/plugins/data/public/search/tabify/response_writer.test.ts +++ b/src/plugins/data/public/search/tabify/response_writer.test.ts @@ -19,14 +19,10 @@ import { TabbedAggResponseWriter } from './response_writer'; import { AggConfigs, BUCKET_TYPES } from '../aggs'; -import { mockDataServices, mockAggTypesRegistry } from '../aggs/test_helpers'; +import { mockAggTypesRegistry } from '../aggs/test_helpers'; import { TabbedResponseWriterOptions } from './types'; describe('TabbedAggResponseWriter class', () => { - beforeEach(() => { - mockDataServices(); - }); - let responseWriter: TabbedAggResponseWriter; const typesRegistry = mockAggTypesRegistry(); @@ -58,16 +54,21 @@ describe('TabbedAggResponseWriter class', () => { ]; const createResponseWritter = (aggs: any[] = [], opts?: Partial) => { - const field = { - name: 'geo.src', - }; + const fields = [ + { + name: 'geo.src', + }, + { + name: 'machine.os.raw', + }, + ]; const indexPattern = { id: '1234', title: 'logstash-*', fields: { - getByName: () => field, - filter: () => [field], + getByName: (name: string) => fields.find((f) => f.name === name), + filter: () => fields, }, } as any; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts index 01a664837e704..114be67e490cf 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -18,12 +18,12 @@ */ import dateMath from '@elastic/datemath'; -import { Ipv4Address } from '../../../../../../kibana_utils/public'; import { FILTER_OPERATORS, Operator } from './filter_operators'; import { isFilterable, IIndexPattern, IFieldType, + Ipv4Address, Filter, FieldFilter, } from '../../../../../common'; diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index f95fe748dfdae..007be9da63e49 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -1,3 +1,41 @@ +.kbnQueryBar__wrap { + max-width: 100%; + z-index: $euiZContentMenu; +} + +// Uses the append style, but no bordering +.kqlQueryBar__languageSwitcherButton { + border-right: none !important; +} + +.kbnQueryBar__textarea { + z-index: $euiZContentMenu; + resize: none !important; // When in the group, it will autosize + height: $euiSizeXXL; + // Unlike most inputs within layout control groups, the text area still needs a border. + // These adjusts help it sit above the control groups shadow to line up correctly. + padding-top: $euiSizeS + 3px !important; + transform: translateY(-2px); + padding: $euiSizeS - 1px; + + &:not(:focus) { + @include euiYScrollWithShadows; + white-space: nowrap; + overflow-y: hidden; + overflow-x: hidden; + border: none; + box-shadow: none; + } + + // When focused, let it scroll + &:focus { + overflow-x: auto; + overflow-y: auto; + width: calc(100% + 1px); // To overtake the group's fake border + white-space: normal; + } +} + @include euiBreakpoint('xs', 's') { .kbnQueryBar--withDatePicker { > :first-child { @@ -16,5 +54,11 @@ // sass-lint:disable-block no-important flex-grow: 0 !important; flex-basis: auto !important; + margin-right: -$euiSizeXS !important; + + &.kbnQueryBar__datePickerWrapper-isHidden { + width: 0; + overflow: hidden; + } } } diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index a4c93d0044c9a..4d51b173f6743 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -60,7 +60,7 @@ export function QueryLanguageSwitcher(props: Props) { setIsPopoverOpen(!isPopoverOpen)} - className="euiFormControlLayout__append" + className="euiFormControlLayout__append kqlQueryBar__languageSwitcherButton" data-test-subj={'switchQueryLanguageButton'} > {props.language === 'lucene' ? luceneLabel : kqlLabel} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 4b0dc579c39ce..86bf30ba0e374 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -69,6 +69,7 @@ interface Props { export function QueryBarTopRow(props: Props) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); + const [isQueryInputFocused, setIsQueryInputFocused] = useState(false); const kibana = useKibana(); const { uiSettings, notifications, storage, appName, docLinks } = kibana.services; @@ -107,6 +108,10 @@ export function QueryBarTopRow(props: Props) { }); } + function onChangeQueryInputFocus(isFocused: boolean) { + setIsQueryInputFocused(isFocused); + } + function onTimeChange({ start, end, @@ -182,6 +187,7 @@ export function QueryBarTopRow(props: Props) { query={props.query!} screenTitle={props.screenTitle} onChange={onQueryChange} + onChangeQueryInputFocus={onChangeQueryInputFocus} onSubmit={onInputSubmit} persistedLog={persistedLog} dataTestSubj={props.dataTestSubj} @@ -268,8 +274,12 @@ export function QueryBarTopRow(props: Props) { }; }); + const wrapperClasses = classNames('kbnQueryBar__datePickerWrapper', { + 'kbnQueryBar__datePickerWrapper-isHidden': isQueryInputFocused, + }); + return ( - + ); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 755716aee8f48..0397c34d0c2b8 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -23,7 +23,7 @@ import { mockPersistedLogFactory, } from './query_string_input.test.mocks'; -import { EuiFieldText } from '@elastic/eui'; +import { EuiTextArea } from '@elastic/eui'; import React from 'react'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryStringInput, QueryStringInputUI } from './query_string_input'; @@ -102,7 +102,7 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], }) ); - expect(component.find(EuiFieldText).props().value).toBe(kqlQuery.query); + expect(component.find(EuiTextArea).props().value).toBe(kqlQuery.query); expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(kqlQuery.language); }); @@ -117,7 +117,7 @@ describe('QueryStringInput', () => { expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); - it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { + it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { const component = mount( wrapQueryStringInputInContext({ query: kqlQuery, @@ -126,7 +126,7 @@ describe('QueryStringInput', () => { disableAutoFocus: true, }) ); - expect(component.find(EuiFieldText).prop('autoFocus')).toBeFalsy(); + expect(component.find(EuiTextArea).prop('autoFocus')).toBeFalsy(); }); it('Should create a unique PersistedLog based on the appName and query language', () => { @@ -179,7 +179,7 @@ describe('QueryStringInput', () => { const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI; const input = instance.inputRef; - const inputWrapper = component.find(EuiFieldText).find('input'); + const inputWrapper = component.find(EuiTextArea).find('textarea'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); expect(mockCallback).toHaveBeenCalledTimes(1); @@ -199,7 +199,7 @@ describe('QueryStringInput', () => { const instance = component.find('QueryStringInputUI').instance() as QueryStringInputUI; const input = instance.inputRef; - const inputWrapper = component.find(EuiFieldText).find('input'); + const inputWrapper = component.find(EuiTextArea).find('textarea'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); expect(mockPersistedLog.add).toHaveBeenCalledWith('response:200'); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 120bbf3b68f7b..6f72aa829d8f3 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -22,13 +22,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { - EuiFieldText, + EuiTextArea, EuiOutsideClickDetector, PopoverAnchorPosition, EuiFlexGroup, EuiFlexItem, EuiButton, EuiLink, + htmlIdGenerator, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -49,12 +50,14 @@ interface Props { query: Query; disableAutoFocus?: boolean; screenTitle?: string; - prepend?: React.ComponentProps['prepend']; + prepend?: any; persistedLog?: PersistedLog; bubbleSubmitEvent?: boolean; placeholder?: string; languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; + onBlur?: () => void; onChange?: (query: Query) => void; + onChangeQueryInputFocus?: (isFocused: boolean) => void; onSubmit?: (query: Query) => void; dataTestSubj?: string; } @@ -92,7 +95,7 @@ export class QueryStringInputUI extends Component { indexPatterns: [], }; - public inputRef: HTMLInputElement | null = null; + public inputRef: HTMLTextAreaElement | null = null; private persistedLog: PersistedLog | undefined; private abortController?: AbortController; @@ -222,27 +225,32 @@ export class QueryStringInputUI extends Component { this.onChange({ query: value, language: this.props.query.language }); }; - private onInputChange = (event: React.ChangeEvent) => { + private onInputChange = (event: React.ChangeEvent) => { this.onQueryStringChange(event.target.value); + if (event.target.value === '') { + this.handleRemoveHeight(); + } else { + this.handleAutoHeight(); + } }; - private onClickInput = (event: React.MouseEvent) => { - if (event.target instanceof HTMLInputElement) { + private onClickInput = (event: React.MouseEvent) => { + if (event.target instanceof HTMLTextAreaElement) { this.onQueryStringChange(event.target.value); } }; - private onKeyUp = (event: React.KeyboardEvent) => { + private onKeyUp = (event: React.KeyboardEvent) => { if ([KEY_CODES.LEFT, KEY_CODES.RIGHT, KEY_CODES.HOME, KEY_CODES.END].includes(event.keyCode)) { this.setState({ isSuggestionsVisible: true }); - if (event.target instanceof HTMLInputElement) { + if (event.target instanceof HTMLTextAreaElement) { this.onQueryStringChange(event.target.value); } } }; - private onKeyDown = (event: React.KeyboardEvent) => { - if (event.target instanceof HTMLInputElement) { + private onKeyDown = (event: React.KeyboardEvent) => { + if (event.target instanceof HTMLTextAreaElement) { const { isSuggestionsVisible, index } = this.state; const preventDefault = event.preventDefault.bind(event); const { target, key, metaKey } = event; @@ -257,16 +265,19 @@ export class QueryStringInputUI extends Component { switch (event.keyCode) { case KEY_CODES.DOWN: - event.preventDefault(); if (isSuggestionsVisible && index !== null) { + event.preventDefault(); this.incrementIndex(index); - } else { + // Note to engineers. `isSuggestionVisible` does not mean the suggestions are visible. + // This should likely be fixed, it's more that suggestions can be shown. + } else if ((isSuggestionsVisible && index == null) || this.getQueryString() === '') { + event.preventDefault(); this.setState({ isSuggestionsVisible: true, index: 0 }); } break; case KEY_CODES.UP: - event.preventDefault(); if (isSuggestionsVisible && index !== null) { + event.preventDefault(); this.decrementIndex(index); } break; @@ -438,6 +449,17 @@ export class QueryStringInputUI extends Component { if (this.state.isSuggestionsVisible) { this.setState({ isSuggestionsVisible: false, index: null }); } + this.handleBlurHeight(); + if (this.props.onChangeQueryInputFocus) { + this.props.onChangeQueryInputFocus(false); + } + }; + + private onInputBlur = () => { + this.handleBlurHeight(); + if (this.props.onChangeQueryInputFocus) { + this.props.onChangeQueryInputFocus(false); + } }; private onClickSuggestion = (suggestion: QuerySuggestion) => { @@ -459,6 +481,8 @@ export class QueryStringInputUI extends Component { this.setState({ index }); }; + textareaId = htmlIdGenerator()(); + public componentDidMount() { const parsedQuery = fromUser(toUser(this.props.query.query)); if (!isEqual(this.props.query.query, parsedQuery)) { @@ -467,6 +491,8 @@ export class QueryStringInputUI extends Component { this.initPersistedLog(); this.fetchIndexPatterns().then(this.updateSuggestions); + + window.addEventListener('resize', this.handleAutoHeight); } public componentDidUpdate(prevProps: Props) { @@ -484,15 +510,18 @@ export class QueryStringInputUI extends Component { } if (this.state.selectionStart !== null && this.state.selectionEnd !== null) { - if (this.inputRef) { - // For some reason the type guard above does not make the compiler happy - // @ts-ignore + if (this.inputRef != null) { this.inputRef.setSelectionRange(this.state.selectionStart, this.state.selectionEnd); } this.setState({ selectionStart: null, selectionEnd: null, }); + if (document.activeElement !== null && document.activeElement.id === this.textareaId) { + this.handleAutoHeight(); + } else { + this.handleRemoveHeight(); + } } } @@ -500,8 +529,37 @@ export class QueryStringInputUI extends Component { if (this.abortController) this.abortController.abort(); this.updateSuggestions.cancel(); this.componentIsUnmounting = true; + window.removeEventListener('resize', this.handleAutoHeight); } + handleAutoHeight = () => { + if (this.inputRef !== null && document.activeElement === this.inputRef) { + this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important'); + } + }; + + handleRemoveHeight = () => { + if (this.inputRef !== null) { + this.inputRef.style.removeProperty('height'); + } + }; + + handleBlurHeight = () => { + if (this.inputRef !== null) { + this.handleRemoveHeight(); + this.inputRef.scrollTop = 0; + } + }; + + handleOnFocus = () => { + if (this.props.onChangeQueryInputFocus) { + this.props.onChangeQueryInputFocus(true); + } + requestAnimationFrame(() => { + this.handleAutoHeight(); + }); + }; + public render() { const isSuggestionsVisible = this.state.isSuggestionsVisible && { 'aria-controls': 'kbnTypeahead__items', @@ -510,20 +568,24 @@ export class QueryStringInputUI extends Component { const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; return ( - -
-
-
- + {this.props.prepend} + +
+
+ { onKeyUp={this.onKeyUp} onChange={this.onInputChange} onClick={this.onClickInput} + onBlur={this.onInputBlur} + onFocus={this.handleOnFocus} + className="kbnQueryBar__textarea" fullWidth - autoFocus={!this.props.disableAutoFocus} - inputRef={(node) => { + rows={1} + id={this.textareaId} + autoFocus={ + this.props.onChangeQueryInputFocus ? false : !this.props.disableAutoFocus + } + inputRef={(node: any) => { if (node) { this.inputRef = node; } @@ -548,7 +617,6 @@ export class QueryStringInputUI extends Component { defaultMessage: 'Start typing to search and filter the {pageType} page', values: { pageType: this.services.appName }, })} - type="text" aria-autocomplete="list" aria-controls={this.state.isSuggestionsVisible ? 'kbnTypeahead__items' : undefined} aria-activedescendant={ @@ -557,29 +625,29 @@ export class QueryStringInputUI extends Component { : undefined } role="textbox" - prepend={this.props.prepend} - append={ - - } data-test-subj={this.props.dataTestSubj || 'queryInput'} - /> + > + {this.getQueryString()} +
-
- -
- + +
+ + + +
); } } diff --git a/src/plugins/data/public/ui/typeahead/_suggestion.scss b/src/plugins/data/public/ui/typeahead/_suggestion.scss index 3a215ceddcd00..81c05f1a8a78c 100644 --- a/src/plugins/data/public/ui/typeahead/_suggestion.scss +++ b/src/plugins/data/public/ui/typeahead/_suggestion.scss @@ -16,7 +16,7 @@ $kbnTypeaheadTypes: ( color: $euiTextColor; background-color: $euiColorEmptyShade; position: absolute; - top: -1px; + top: -2px; z-index: $euiZContentMenu; width: 100%; border-bottom-left-radius: $euiBorderRadius; @@ -56,7 +56,6 @@ $kbnTypeaheadTypes: ( .kbnTypeahead__item.active { background-color: $euiColorLightestShade; - .kbnSuggestionItem__callout { background: $euiColorEmptyShade; } @@ -130,7 +129,6 @@ $kbnTypeaheadTypes: ( align-items: center; } - .kbnSuggestionItem__text { flex-grow: 0; /* 2 */ flex-basis: auto; /* 2 */ @@ -142,16 +140,15 @@ $kbnTypeaheadTypes: ( color: $euiTextColor; } - .kbnSuggestionItem__description { color: $euiColorDarkShade; overflow: hidden; text-overflow: ellipsis; margin-left: $euiSizeXL; - + &:empty { flex-grow: 0; - margin-left:0; + margin-left: 0; } } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 6a4eb38b552ff..0dd0115add8ad 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -155,6 +155,7 @@ import { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, + Ipv4Address, isValidEsInterval, isValidInterval, parseEsInterval, @@ -184,6 +185,7 @@ export const search = { dateHistogramInterval, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, + Ipv4Address, isValidEsInterval, isValidInterval, parseEsInterval, diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f029609cbf7ec..6b62d942de688 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -732,6 +732,7 @@ export const search: { dateHistogramInterval: typeof dateHistogramInterval; InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; + Ipv4Address: typeof Ipv4Address; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parseEsInterval: typeof parseEsInterval; @@ -766,33 +767,34 @@ export type TStrategyTypes = typeof ES_SEARCH_STRATEGY | string; // // @public (undocumented) export const UI_SETTINGS: { - META_FIELDS: string; - DOC_HIGHLIGHT: string; - QUERY_STRING_OPTIONS: string; - QUERY_ALLOW_LEADING_WILDCARDS: string; - SEARCH_QUERY_LANGUAGE: string; - SORT_OPTIONS: string; - COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string; - COURIER_SET_REQUEST_PREFERENCE: string; - COURIER_CUSTOM_REQUEST_PREFERENCE: string; - COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string; - COURIER_BATCH_SEARCHES: string; - SEARCH_INCLUDE_FROZEN: string; - HISTOGRAM_BAR_TARGET: string; - HISTOGRAM_MAX_BARS: string; - HISTORY_LIMIT: string; - SHORT_DOTS_ENABLE: string; - FORMAT_DEFAULT_TYPE_MAP: string; - FORMAT_NUMBER_DEFAULT_PATTERN: string; - FORMAT_PERCENT_DEFAULT_PATTERN: string; - FORMAT_BYTES_DEFAULT_PATTERN: string; - FORMAT_CURRENCY_DEFAULT_PATTERN: string; - FORMAT_NUMBER_DEFAULT_LOCALE: string; - TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string; - TIMEPICKER_QUICK_RANGES: string; - INDEXPATTERN_PLACEHOLDER: string; - FILTERS_PINNED_BY_DEFAULT: string; - FILTERS_EDITOR_SUGGEST_VALUES: string; + readonly META_FIELDS: "metaFields"; + readonly DOC_HIGHLIGHT: "doc_table:highlight"; + readonly QUERY_STRING_OPTIONS: "query:queryString:options"; + readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards"; + readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage"; + readonly SORT_OPTIONS: "sort:options"; + readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex"; + readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference"; + readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference"; + readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; + readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; + readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; + readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; + readonly HISTORY_LIMIT: "history:limit"; + readonly SHORT_DOTS_ENABLE: "shortDots:enable"; + readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap"; + readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern"; + readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern"; + readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern"; + readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern"; + readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale"; + readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults"; + readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges"; + readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults"; + readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder"; + readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault"; + readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues"; }; @@ -820,12 +822,13 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:129:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:129:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:191:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:190:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:193:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index de978c7968aee..e825ef7f6c945 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -518,7 +518,7 @@ export function getUiSettings(): Record> { }`, type: 'json', description: i18n.translate('data.advancedSettings.timepicker.refreshIntervalDefaultsText', { - defaultMessage: `The timefilter's default refresh interval`, + defaultMessage: `The timefilter's default refresh interval. The "value" needs to be specified in milliseconds.`, }), requiresPageReload: true, schema: schema.object({ diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index 364edbb030dc9..23adef2626a72 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -1,7 +1,7 @@ { "id": "indexPatternManagement", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["management", "data", "kibanaLegacy"] } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 5c955bbd3283e..70200e03c0dbe 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -1,41 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CreateIndexPatternWizard defaults to the loading state 1`] = ` - -
-
- -
+ + -
+ `; exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = ` - -
+ +
+ -
+ -
+ `; exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` - -
-
- -
+ + -
+ `; exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = ` - -
+ +
+ -
+ -
+ `; exports[`CreateIndexPatternWizard renders when there are no indices but there are remote clusters 1`] = ` - -
+ +
+ -
+ -
+ `; exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = ` - -
-
- -
+ + -
+ `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap index 81ca3e644d3ce..6a2fd1000e6b4 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap @@ -2,10 +2,15 @@ exports[`Header should render a different name, prompt, and beta tag if provided 1`] = `
Test prompt @@ -31,76 +36,114 @@ exports[`Header should render a different name, prompt, and beta tag if provided -
+ + +
- -
+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data souces, + + + + + filebeat-* + + + + + . + + +
+ - -
-

- - - - - Kibana uses index patterns to retrieve data from Elasticsearch indices for things like visualizations. - - - - -

-
-
-
-
+ + Read documentation + + + + +

- +
Test prompt
- -
-
`; exports[`Header should render normally 1`] = `
@@ -110,66 +153,104 @@ exports[`Header should render normally 1`] = ` Create test index pattern -
+ + +
- -
+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } > - + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data souces, + + + + + filebeat-* + + + + + . + + +
+ +
-
+ + Read documentation + + + + +

- - -
- +
`; exports[`Header should render without including system indices 1`] = `
@@ -179,57 +260,90 @@ exports[`Header should render without including system indices 1`] = ` Create test index pattern -
+ + +
- -
+ + multiple + , + "single": + filebeat-4-3-22 + , + "star": + filebeat-* + , + } + } + > + + An index pattern can match a single source, for example, + + + + + filebeat-4-3-22 + + + + + , or + + multiple + + data souces, + + + + + filebeat-* + + + + + . + + +
+ - -
-

- - - - - Kibana uses index patterns to retrieve data from Elasticsearch indices for things like visualizations. - - - - -

-
-
-
-
+ + Read documentation + + + + +

- - -
- +
`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.test.tsx index d12e0401380b9..865b3ec353f76 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.test.tsx @@ -22,18 +22,20 @@ import { Header } from '../header'; import { mount } from 'enzyme'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { mockManagementPlugin } from '../../../../mocks'; +import { DocLinksStart } from 'kibana/public'; describe('Header', () => { const indexPatternName = 'test index pattern'; const mockedContext = mockManagementPlugin.createIndexPatternManagmentContext(); + const mockedDocLinks = { + links: { + indexPatterns: {}, + }, + } as DocLinksStart; it('should render normally', () => { const component = mount( -
{}} - />, +
, { wrappingComponent: KibanaContextProvider, wrappingComponentProps: { @@ -47,11 +49,7 @@ describe('Header', () => { it('should render without including system indices', () => { const component = mount( -
{}} - />, +
, { wrappingComponent: KibanaContextProvider, wrappingComponentProps: { @@ -66,11 +64,10 @@ describe('Header', () => { it('should render a different name, prompt, and beta tag if provided', () => { const component = mount(
{}} prompt={
Test prompt
} indexPatternName={indexPatternName} isBeta={true} + docLinks={mockedDocLinks} />, { wrappingComponent: KibanaContextProvider, diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx index 35c6e67d0ea0e..f90425311142d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx @@ -17,38 +17,26 @@ * under the License. */ -import React, { Fragment } from 'react'; +import React from 'react'; -import { - EuiBetaBadge, - EuiSpacer, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTextColor, - EuiSwitch, -} from '@elastic/eui'; +import { EuiBetaBadge, EuiSpacer, EuiTitle, EuiText, EuiCode, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart } from 'kibana/public'; import { useKibana } from '../../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../../types'; export const Header = ({ prompt, indexPatternName, - showSystemIndices = false, - isIncludingSystemIndices, - onChangeIncludingSystemIndices, isBeta = false, + docLinks, }: { prompt?: React.ReactNode; indexPatternName: string; - showSystemIndices?: boolean; - isIncludingSystemIndices: boolean; - onChangeIncludingSystemIndices: () => void; isBeta?: boolean; + docLinks: DocLinksStart; }) => { const changeTitle = useKibana().services.chrome.docTitle.change; const createIndexPatternHeader = i18n.translate( @@ -67,53 +55,44 @@ export const Header = ({

{createIndexPatternHeader} {isBeta ? ( - + <> {' '} - + ) : null}

- - - -

- - - -

-
-
- {showSystemIndices ? ( - - - } - id="checkboxShowSystemIndices" - checked={isIncludingSystemIndices} - onChange={onChangeIncludingSystemIndices} + + +

+ multiple, + single: filebeat-4-3-22, + star: filebeat-*, + }} + /> +
+ + - - ) : null} - + +

+
{prompt ? ( - - + <> + {prompt} - + ) : null} - ); }; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/__snapshots__/step_index_pattern.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/__snapshots__/step_index_pattern.test.tsx.snap index b68ba4720b935..813a0c61c0829 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/__snapshots__/step_index_pattern.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/__snapshots__/step_index_pattern.test.tsx.snap @@ -11,8 +11,10 @@ Object { ] } goToNextStep={[Function]} + isIncludingSystemIndices={false} isInputInvalid={true} isNextStepDisabled={true} + onChangeIncludingSystemIndices={[Function]} onQueryChanged={[Function]} query="?" />, @@ -25,6 +27,7 @@ exports[`StepIndexPattern renders indices which match the initial query 1`] = ` indices={ Array [ Object { + "item": Object {}, "name": "kibana", }, ] @@ -39,6 +42,7 @@ exports[`StepIndexPattern renders matching indices when input is valid 1`] = ` indices={ Array [ Object { + "item": Object {}, "name": "kibana", }, ] diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap index 3021292953ff5..c4f735558b1f2 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap @@ -16,13 +16,8 @@ exports[`Header should mark the input as invalid 1`] = ` - - + + @@ -34,43 +29,40 @@ exports[`Header should mark the input as invalid 1`] = ` "Input is invalid", ] } - fullWidth={false} + fullWidth={true} hasChildLabel={true} hasEmptyLabelSpace={false} helpText={ -
-

- - * - , - } + + + * + , } - /> -

-

- - % - , - } + } + /> + + + % + , } - /> -

-
+ } + /> + } isInvalid={true} label={ @@ -79,6 +71,7 @@ exports[`Header should mark the input as invalid 1`] = ` > - - - + + + +
@@ -124,13 +128,8 @@ exports[`Header should render normally 1`] = ` - - + + @@ -138,43 +137,40 @@ exports[`Header should render normally 1`] = ` describedByIds={Array []} display="row" error={Array []} - fullWidth={false} + fullWidth={true} hasChildLabel={true} hasEmptyLabelSpace={false} helpText={ -
-

- - * - , - } + + + * + , } - /> -

-

- - % - , - } + } + /> + + + % + , } - /> -

-
+ } + /> + } isInvalid={false} label={ @@ -183,6 +179,7 @@ exports[`Header should render normally 1`] = ` > - - - + + + +
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.test.tsx index f56340d0009be..acc133a4dd649 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.test.tsx @@ -32,6 +32,8 @@ describe('Header', () => { onQueryChanged={() => {}} goToNextStep={() => {}} isNextStepDisabled={false} + onChangeIncludingSystemIndices={() => {}} + isIncludingSystemIndices={false} /> ); @@ -48,6 +50,8 @@ describe('Header', () => { onQueryChanged={() => {}} goToNextStep={() => {}} isNextStepDisabled={true} + onChangeIncludingSystemIndices={() => {}} + isIncludingSystemIndices={false} /> ); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx index 9ce72aeeea6e3..f1bf0d54a1cbf 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx @@ -28,6 +28,8 @@ import { EuiForm, EuiFormRow, EuiFieldText, + EuiSwitchEvent, + EuiSwitch, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -41,6 +43,9 @@ interface HeaderProps { onQueryChanged: (e: React.ChangeEvent) => void; goToNextStep: (query: string) => void; isNextStepDisabled: boolean; + showSystemIndices?: boolean; + onChangeIncludingSystemIndices: (event: EuiSwitchEvent) => void; + isIncludingSystemIndices: boolean; } export const Header: React.FC = ({ @@ -51,6 +56,9 @@ export const Header: React.FC = ({ onQueryChanged, goToNextStep, isNextStepDisabled, + showSystemIndices = false, + onChangeIncludingSystemIndices, + isIncludingSystemIndices, ...rest }) => (
@@ -63,35 +71,32 @@ export const Header: React.FC = ({ - - + + } isInvalid={isInputInvalid} error={errors} helpText={ -
-

- * }} - /> -

-

- {characterList} }} - /> -

-
+ <> + * }} + />{' '} + {characterList} }} + /> + } > = ({ isInvalid={isInputInvalid} onChange={onQueryChanged} data-test-subj="createIndexPatternNameInput" + fullWidth />
+ + {showSystemIndices ? ( + + + } + id="checkboxShowSystemIndices" + checked={isIncludingSystemIndices} + onChange={onChangeIncludingSystemIndices} + /> + + ) : null}
- goToNextStep(query)} - isDisabled={isNextStepDisabled} - data-test-subj="createIndexPatternGoToStep2Button" - > - - + + goToNextStep(query)} + isDisabled={isNextStepDisabled} + data-test-subj="createIndexPatternGoToStep2Button" + > + + +
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.test.tsx index d8a1d1a0ab72f..fbd60cbe3d131 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.test.tsx @@ -20,11 +20,12 @@ import React from 'react'; import { IndicesList } from '../indices_list'; import { shallow } from 'enzyme'; +import { MatchedItem } from '../../../../types'; -const indices = [ +const indices = ([ { name: 'kibana', tags: [] }, { name: 'es', tags: [] }, -]; +] as unknown) as MatchedItem[]; describe('IndicesList', () => { it('should render normally', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.tsx index c590d2a7ddfe2..4a051ee698209 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/indices_list.tsx @@ -39,10 +39,10 @@ import { Pager } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { PER_PAGE_INCREMENTS } from '../../../../constants'; -import { MatchedIndex, Tag } from '../../../../types'; +import { MatchedItem, Tag } from '../../../../types'; interface IndicesListProps { - indices: MatchedIndex[]; + indices: MatchedItem[]; query: string; } @@ -187,7 +187,7 @@ export class IndicesList extends React.Component {index.tags.map((tag: Tag) => { return ( - + {tag.name} ); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/__snapshots__/status_message.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/__snapshots__/status_message.test.tsx.snap index 4a063f1430d1c..44b753c473803 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/__snapshots__/status_message.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/__snapshots__/status_message.test.tsx.snap @@ -1,67 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`StatusMessage should render with exact matches 1`] = ` - - - + title={   - - , - "strongSuccess": - - , + "sourceCount": 1, } } /> - - + } +/> `; exports[`StatusMessage should render with no partial matches 1`] = ` - - + title={ - - + } +/> `; exports[`StatusMessage should render with partial matches 1`] = ` - - + title={ - - + } +/> `; exports[`StatusMessage should render without a query 1`] = ` - - + title={ - 2 - indices - , + "sourceCount": 2, } } /> - - + } +/> `; exports[`StatusMessage should show that no indices exist 1`] = ` - - + title={ - - + } +/> `; exports[`StatusMessage should show that system indices exist 1`] = ` - - + title={ - - + } +/> `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.test.tsx index 899c21d59c5bc..f97c9ffe8a364 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.test.tsx @@ -20,18 +20,19 @@ import React from 'react'; import { StatusMessage } from '../status_message'; import { shallow } from 'enzyme'; +import { MatchedItem } from '../../../../types'; const tagsPartial = { tags: [], }; const matchedIndices = { - allIndices: [ + allIndices: ([ { name: 'kibana', ...tagsPartial }, { name: 'es', ...tagsPartial }, - ], - exactMatchedIndices: [], - partialMatchedIndices: [{ name: 'kibana', ...tagsPartial }], + ] as unknown) as MatchedItem[], + exactMatchedIndices: [] as MatchedItem[], + partialMatchedIndices: ([{ name: 'kibana', ...tagsPartial }] as unknown) as MatchedItem[], }; describe('StatusMessage', () => { @@ -51,7 +52,7 @@ describe('StatusMessage', () => { it('should render with exact matches', () => { const localMatchedIndices = { ...matchedIndices, - exactMatchedIndices: [{ name: 'kibana', ...tagsPartial }], + exactMatchedIndices: ([{ name: 'kibana', ...tagsPartial }] as unknown) as MatchedItem[], }; const component = shallow( diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx index ccdd1833ea9bf..22b75071b93bb 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx @@ -19,16 +19,17 @@ import React from 'react'; -import { EuiText, EuiTextColor, EuiIcon } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MatchedIndex } from '../../../../types'; +import { MatchedItem } from '../../../../types'; interface StatusMessageProps { matchedIndices: { - allIndices: MatchedIndex[]; - exactMatchedIndices: MatchedIndex[]; - partialMatchedIndices: MatchedIndex[]; + allIndices: MatchedItem[]; + exactMatchedIndices: MatchedItem[]; + partialMatchedIndices: MatchedItem[]; }; isIncludingSystemIndices: boolean; query: string; @@ -41,23 +42,26 @@ export const StatusMessage: React.FC = ({ query, showSystemIndices, }) => { - let statusIcon; + let statusIcon: EuiIconType | undefined; let statusMessage; - let statusColor: 'default' | 'secondary' | undefined; + let statusColor: 'primary' | 'success' | 'warning' | undefined; const allIndicesLength = allIndices.length; if (query.length === 0) { - statusIcon = null; - statusColor = 'default'; + statusIcon = undefined; + statusColor = 'primary'; - if (allIndicesLength > 1) { + if (allIndicesLength >= 1) { statusMessage = ( {allIndicesLength} indices }} + defaultMessage="Your index pattern can match {sourceCount, plural, + one {your # source} + other {any of your # sources} + }." + values={{ sourceCount: allIndicesLength }} /> ); @@ -66,8 +70,7 @@ export const StatusMessage: React.FC = ({ ); @@ -83,51 +86,44 @@ export const StatusMessage: React.FC = ({ } } else if (exactMatchedIndices.length) { statusIcon = 'check'; - statusColor = 'secondary'; + statusColor = 'success'; statusMessage = (   - - - ), - strongIndices: ( - - - - ), + sourceCount: exactMatchedIndices.length, }} /> ); } else if (partialMatchedIndices.length) { - statusIcon = null; - statusColor = 'default'; + statusIcon = undefined; + statusColor = 'primary'; statusMessage = ( @@ -137,20 +133,26 @@ export const StatusMessage: React.FC = ({ ); } else if (allIndicesLength) { - statusIcon = null; - statusColor = 'default'; + statusIcon = undefined; + statusColor = 'warning'; statusMessage = ( @@ -163,11 +165,12 @@ export const StatusMessage: React.FC = ({ } return ( - - - {statusIcon ? : null} - {statusMessage} - - + ); }; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx index 053940270c2b6..c88918041ca81 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { SavedObjectsFindResponsePublic } from 'kibana/public'; -import { StepIndexPattern } from '../step_index_pattern'; +import { StepIndexPattern, canPreselectTimeField } from './step_index_pattern'; import { Header } from './components/header'; import { IndexPatternCreationConfig } from '../../../../../../../plugins/index_pattern_management/public'; import { mockManagementPlugin } from '../../../../mocks'; @@ -38,16 +38,16 @@ const mockIndexPatternCreationType = new IndexPatternCreationConfig({ jest.mock('../../lib/get_indices', () => ({ getIndices: ({}, {}, query: string) => { if (query.startsWith('e')) { - return [{ name: 'es' }]; + return [{ name: 'es', item: {} }]; } - return [{ name: 'kibana' }]; + return [{ name: 'kibana', item: {} }]; }, })); const allIndices = [ - { name: 'kibana', tags: [] }, - { name: 'es', tags: [] }, + { name: 'kibana', tags: [], item: {} }, + { name: 'es', tags: [], item: {} }, ]; const goToNextStep = () => {}; @@ -205,4 +205,53 @@ describe('StepIndexPattern', () => { await new Promise((resolve) => process.nextTick(resolve)); expect(component.state('exactMatchedIndices')).toEqual([]); }); + + it('it can preselect time field', async () => { + const dataStream1 = { + name: 'data stream 1', + tags: [], + item: { name: 'data stream 1', backing_indices: [], timestamp_field: 'timestamp_field' }, + }; + + const dataStream2 = { + name: 'data stream 2', + tags: [], + item: { name: 'data stream 2', backing_indices: [], timestamp_field: 'timestamp_field' }, + }; + + const differentDataStream = { + name: 'different data stream', + tags: [], + item: { name: 'different data stream 2', backing_indices: [], timestamp_field: 'x' }, + }; + + const index = { + name: 'index', + tags: [], + item: { + name: 'index', + }, + }; + + const alias = { + name: 'alias', + tags: [], + item: { + name: 'alias', + indices: [], + }, + }; + + expect(canPreselectTimeField([index])).toEqual(undefined); + expect(canPreselectTimeField([alias])).toEqual(undefined); + expect(canPreselectTimeField([index, alias, dataStream1])).toEqual(undefined); + + expect(canPreselectTimeField([dataStream1])).toEqual('timestamp_field'); + + expect(canPreselectTimeField([dataStream1, dataStream2])).toEqual('timestamp_field'); + + expect(canPreselectTimeField([dataStream1, dataStream2, differentDataStream])).toEqual( + undefined + ); + }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index b6205a8731dfa..5797149a51aea 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -18,7 +18,7 @@ */ import React, { Component } from 'react'; -import { EuiPanel, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiSpacer, EuiCallOut, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -26,7 +26,6 @@ import { IndexPatternAttributes, UI_SETTINGS, } from '../../../../../../../plugins/data/public'; -import { MAX_SEARCH_SIZE } from '../../constants'; import { getIndices, containsIllegalCharacters, @@ -40,20 +39,20 @@ import { IndicesList } from './components/indices_list'; import { Header } from './components/header'; import { context as contextType } from '../../../../../../kibana_react/public'; import { IndexPatternCreationConfig } from '../../../../../../../plugins/index_pattern_management/public'; -import { MatchedIndex } from '../../types'; +import { MatchedItem } from '../../types'; import { IndexPatternManagmentContextValue } from '../../../../types'; interface StepIndexPatternProps { - allIndices: MatchedIndex[]; - isIncludingSystemIndices: boolean; + allIndices: MatchedItem[]; indexPatternCreationType: IndexPatternCreationConfig; - goToNextStep: (query: string) => void; + goToNextStep: (query: string, timestampField?: string) => void; initialQuery?: string; + showSystemIndices: boolean; } interface StepIndexPatternState { - partialMatchedIndices: MatchedIndex[]; - exactMatchedIndices: MatchedIndex[]; + partialMatchedIndices: MatchedItem[]; + exactMatchedIndices: MatchedItem[]; isLoadingIndices: boolean; existingIndexPatterns: string[]; indexPatternExists: boolean; @@ -61,8 +60,35 @@ interface StepIndexPatternState { appendedWildcard: boolean; showingIndexPatternQueryErrors: boolean; indexPatternName: string; + isIncludingSystemIndices: boolean; } +export const canPreselectTimeField = (indices: MatchedItem[]) => { + const preselectStatus = indices.reduce( + ( + { canPreselect, timeFieldName }: { canPreselect: boolean; timeFieldName?: string }, + matchedItem + ) => { + const dataStreamItem = matchedItem.item; + const dataStreamTimestampField = dataStreamItem.timestamp_field; + const isDataStream = !!dataStreamItem.timestamp_field; + const timestampFieldMatches = + timeFieldName === undefined || timeFieldName === dataStreamTimestampField; + + return { + canPreselect: canPreselect && isDataStream && timestampFieldMatches, + timeFieldName: dataStreamTimestampField || timeFieldName, + }; + }, + { + canPreselect: true, + timeFieldName: undefined, + } + ); + + return preselectStatus.canPreselect ? preselectStatus.timeFieldName : undefined; +}; + export class StepIndexPattern extends Component { static contextType = contextType; @@ -78,9 +104,9 @@ export class StepIndexPattern extends Component goToNextStep(query, canPreselectTimeField(indices))} isNextStepDisabled={isNextStepDisabled} + onChangeIncludingSystemIndices={this.onChangeIncludingSystemIndices} + isIncludingSystemIndices={isIncludingSystemIndices} + showSystemIndices={this.props.showSystemIndices} /> ); } + onChangeIncludingSystemIndices = (event: EuiSwitchEvent) => { + this.setState({ isIncludingSystemIndices: event.target.checked }, () => + this.fetchIndices(this.state.query) + ); + }; + render() { - const { isIncludingSystemIndices, allIndices } = this.props; - const { partialMatchedIndices, exactMatchedIndices } = this.state; + const { allIndices } = this.props; + const { partialMatchedIndices, exactMatchedIndices, isIncludingSystemIndices } = this.state; const matchedIndices = getMatchedIndices( allIndices, @@ -334,15 +372,15 @@ export class StepIndexPattern extends Component + <> {this.renderHeader(matchedIndices)} - + {this.renderLoadingState()} {this.renderIndexPatternExists()} {this.renderStatusMessage(matchedIndices)} - + {this.renderList(matchedIndices)} - + ); } } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap index f865a1ddfd223..6cc92d20cfdcc 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap @@ -17,9 +17,7 @@ exports[`StepTimeField should enable the action button if the user decides to no `; exports[`StepTimeField should render "Custom index pattern ID already exists" when error is "Conflict" 1`] = ` - +
- + - + `; exports[`StepTimeField should render a loading state when creating the index pattern 1`] = ` - - + - - - - - +

- - - - +

+ +
+ + + +
`; exports[`StepTimeField should render a selected timeField 1`] = ` - +
- + - + `; exports[`StepTimeField should render advanced options 1`] = ` - +
- + - + `; exports[`StepTimeField should render advanced options with an index pattern id 1`] = ` - +
- + - + `; exports[`StepTimeField should render any error message 1`] = ` - +
- + - + `; exports[`StepTimeField should render normally 1`] = ` - +
- + - + `; exports[`StepTimeField should render timeFields 1`] = ` - +
- + - + `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap index 63008ec5b52e7..2ac243780b31d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap @@ -16,21 +16,10 @@ exports[`Header should render normally 1`] = ` - - - ki* - , - "indexPatternName": "ki*", - } - } - /> + + + ki* + `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx index 22e245f7ac137..c17b356e159f6 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx @@ -39,15 +39,8 @@ export const Header: React.FC = ({ indexPattern, indexPatternName } - - {indexPattern}, - indexPatternName, - }} - /> + + {indexPattern} ); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap index 886a4ccad39cc..73277b1963626 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/__snapshots__/time_field.test.tsx.snap @@ -2,55 +2,33 @@ exports[`TimeField should render a loading state 1`] = ` + +

+ +

+
+ -

- -

-

- -

- - } label={ - - - - - - - - - - + + } + labelAppend={ + } labelType="label" > @@ -73,62 +51,43 @@ exports[`TimeField should render a loading state 1`] = ` exports[`TimeField should render a selected time field 1`] = ` + +

+ +

+
+ -

- -

-

- -

- - } label={ - + } + labelAppend={ + - - - - - - - - - - - + + +
} labelType="label" > @@ -154,62 +113,43 @@ exports[`TimeField should render a selected time field 1`] = ` exports[`TimeField should render normally 1`] = ` + +

+ +

+
+ -

- -

-

- -

- - } label={ - + } + labelAppend={ + - - - - - - - - - - - + + +
} labelType="label" > diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx index b4ed37118966b..7a3d72551f464 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx @@ -24,8 +24,7 @@ import React from 'react'; import { EuiForm, EuiFormRow, - EuiFlexGroup, - EuiFlexItem, + EuiSpacer, EuiLink, EuiSelect, EuiText, @@ -54,77 +53,68 @@ export const TimeField: React.FC = ({ }) => ( {isVisible ? ( - - - - - - - - {isLoading ? ( - - ) : ( - + <> + +

+ +

+
+ + + } + labelAppend={ + isLoading ? ( + + ) : ( + + - )} -
- - } - helpText={ -
-

- -

-

- -

-
- } - > - {isLoading ? ( - - ) : ( - - )} -
+ + ) + } + > + {isLoading ? ( + + ) : ( + + )} + + ) : (

diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx index 98ce22cd14227..5d33a08557fed 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx @@ -22,10 +22,10 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, - EuiPanel, - EuiText, + EuiTitle, EuiSpacer, EuiLoadingSpinner, + EuiHorizontalRule, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ensureMinimumTime, extractTimeFields } from '../../lib'; @@ -43,6 +43,7 @@ interface StepTimeFieldProps { goToPreviousStep: () => void; createIndexPattern: (selectedTimeField: string | undefined, indexPatternId: string) => void; indexPatternCreationType: IndexPatternCreationConfig; + selectedTimeField?: string; } interface StepTimeFieldState { @@ -69,7 +70,7 @@ export class StepTimeField extends Component - - - - - - + + + +

- - - - +

+ + + + + + + ); } @@ -236,7 +242,7 @@ export class StepTimeField extends Component + <>
- + - + ); } } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index 111be41cfc53a..cd76ca09ccb74 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -19,10 +19,16 @@ import React, { ReactElement, Component } from 'react'; -import { EuiGlobalToastList, EuiGlobalToastListToast, EuiPanel } from '@elastic/eui'; +import { + EuiGlobalToastList, + EuiGlobalToastListToast, + EuiPageContent, + EuiHorizontalRule, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { DocLinksStart } from 'src/core/public'; import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; import { Header } from './components/header'; @@ -31,21 +37,21 @@ import { EmptyState } from './components/empty_state'; import { context as contextType } from '../../../../kibana_react/public'; import { getCreateBreadcrumbs } from '../breadcrumbs'; -import { MAX_SEARCH_SIZE } from './constants'; import { ensureMinimumTime, getIndices } from './lib'; import { IndexPatternCreationConfig } from '../..'; import { IndexPatternManagmentContextValue } from '../../types'; -import { MatchedIndex } from './types'; +import { MatchedItem } from './types'; interface CreateIndexPatternWizardState { step: number; indexPattern: string; - allIndices: MatchedIndex[]; + allIndices: MatchedItem[]; remoteClustersExist: boolean; isInitiallyLoadingIndices: boolean; - isIncludingSystemIndices: boolean; toasts: EuiGlobalToastListToast[]; indexPatternCreationType: IndexPatternCreationConfig; + selectedTimeField?: string; + docLinks: DocLinksStart; } export class CreateIndexPatternWizard extends Component< @@ -69,9 +75,9 @@ export class CreateIndexPatternWizard extends Component< allIndices: [], remoteClustersExist: false, isInitiallyLoadingIndices: true, - isIncludingSystemIndices: false, toasts: [], indexPatternCreationType: context.services.indexPatternManagementStart.creation.getType(type), + docLinks: context.services.docLinks, }; } @@ -80,7 +86,7 @@ export class CreateIndexPatternWizard extends Component< } catchAndWarn = async ( - asyncFn: Promise, + asyncFn: Promise, errorValue: [] | string[], errorMsg: ReactElement ) => { @@ -102,12 +108,6 @@ export class CreateIndexPatternWizard extends Component< }; fetchData = async () => { - this.setState({ - allIndices: [], - isInitiallyLoadingIndices: true, - remoteClustersExist: false, - }); - const indicesFailMsg = ( + ).then((allIndices: MatchedItem[]) => this.setState({ allIndices, isInitiallyLoadingIndices: false }) ); this.catchAndWarn( // if we get an error from remote cluster query, supply fallback value that allows user entry. // ['a'] is fallback value - getIndices( - this.context.services.data.search.__LEGACY.esClient, - this.state.indexPatternCreationType, - `*:*`, - 1 - ), + getIndices(this.context.services.http, this.state.indexPatternCreationType, `*:*`, false), ['a'], clustersFailMsg - ).then((remoteIndices: string[] | MatchedIndex[]) => + ).then((remoteIndices: string[] | MatchedItem[]) => this.setState({ remoteClustersExist: !!remoteIndices.length }) ); }; @@ -189,7 +179,7 @@ export class CreateIndexPatternWizard extends Component< if (isConfirmed) { return history.push(`/patterns/${indexPatternId}`); } else { - return false; + return; } } @@ -201,31 +191,21 @@ export class CreateIndexPatternWizard extends Component< history.push(`/patterns/${createdId}`); }; - goToTimeFieldStep = (indexPattern: string) => { - this.setState({ step: 2, indexPattern }); + goToTimeFieldStep = (indexPattern: string, selectedTimeField?: string) => { + this.setState({ step: 2, indexPattern, selectedTimeField }); }; goToIndexPatternStep = () => { this.setState({ step: 1 }); }; - onChangeIncludingSystemIndices = () => { - this.setState((prevState) => ({ - isIncludingSystemIndices: !prevState.isIncludingSystemIndices, - })); - }; - renderHeader() { - const { isIncludingSystemIndices } = this.state; - return (
); } @@ -234,7 +214,6 @@ export class CreateIndexPatternWizard extends Component< const { allIndices, isInitiallyLoadingIndices, - isIncludingSystemIndices, step, indexPattern, remoteClustersExist, @@ -244,8 +223,8 @@ export class CreateIndexPatternWizard extends Component< return ; } - const hasDataIndices = allIndices.some(({ name }: MatchedIndex) => !name.startsWith('.')); - if (!hasDataIndices && !isIncludingSystemIndices && !remoteClustersExist) { + const hasDataIndices = allIndices.some(({ name }: MatchedItem) => !name.startsWith('.')); + if (!hasDataIndices && !remoteClustersExist) { return ( + + {header} + + + ); } if (step === 2) { return ( - + + {header} + + + ); } @@ -290,15 +282,11 @@ export class CreateIndexPatternWizard extends Component< }; render() { - const header = this.renderHeader(); const content = this.renderContent(); return ( - -
- {header} - {content} -
+ <> + {content} { @@ -306,7 +294,7 @@ export class CreateIndexPatternWizard extends Component< }} toastLifeTimeMs={6000} /> -
+ ); } } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap new file mode 100644 index 0000000000000..99876383b4343 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/__snapshots__/get_indices.test.ts.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getIndices response object to item array 1`] = ` +Array [ + Object { + "item": Object { + "attributes": Array [ + "frozen", + ], + "name": "frozen_index", + }, + "name": "frozen_index", + "tags": Array [ + Object { + "color": "default", + "key": "index", + "name": "Index", + }, + Object { + "color": "danger", + "key": "frozen", + "name": "Frozen", + }, + ], + }, + Object { + "item": Object { + "indices": Array [], + "name": "test_alias", + }, + "name": "test_alias", + "tags": Array [ + Object { + "color": "default", + "key": "alias", + "name": "Alias", + }, + ], + }, + Object { + "item": Object { + "backing_indices": Array [], + "name": "test_data_stream", + "timestamp_field": "test_timestamp_field", + }, + "name": "test_data_stream", + "tags": Array [ + Object { + "color": "primary", + "key": "data_stream", + "name": "Data stream", + }, + ], + }, + Object { + "item": Object { + "name": "test_index", + }, + "name": "test_index", + "tags": Array [ + Object { + "color": "default", + "key": "index", + "name": "Index", + }, + ], + }, +] +`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts index b1faca8a04964..8e4dd37284333 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts @@ -17,66 +17,31 @@ * under the License. */ -import { getIndices } from './get_indices'; +import { getIndices, responseToItemArray } from './get_indices'; import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyApiCaller } from '../../../../../data/public/search/legacy'; +import { httpServiceMock } from '../../../../../../core/public/mocks'; +import { ResolveIndexResponseItemIndexAttrs } from '../types'; export const successfulResponse = { - hits: { - total: 1, - max_score: 0.0, - hits: [], - }, - aggregations: { - indices: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '1', - doc_count: 1, - }, - { - key: '2', - doc_count: 1, - }, - ], + indices: [ + { + name: 'remoteCluster1:bar-01', + attributes: ['open'], }, - }, -}; - -export const exceptionResponse = { - body: { - error: { - root_cause: [ - { - type: 'index_not_found_exception', - reason: 'no such index', - index_uuid: '_na_', - 'resource.type': 'index_or_alias', - 'resource.id': 't', - index: 't', - }, - ], - type: 'transport_exception', - reason: 'unable to communicate with remote cluster [cluster_one]', - caused_by: { - type: 'index_not_found_exception', - reason: 'no such index', - index_uuid: '_na_', - 'resource.type': 'index_or_alias', - 'resource.id': 't', - index: 't', - }, + ], + aliases: [ + { + name: 'f-alias', + indices: ['freeze-index', 'my-index'], }, - }, - status: 500, -}; - -export const errorResponse = { - statusCode: 400, - error: 'Bad Request', + ], + data_streams: [ + { + name: 'foo', + backing_indices: ['foo-000001'], + timestamp_field: '@timestamp', + }, + ], }; const mockIndexPatternCreationType = new IndexPatternCreationConfig({ @@ -87,81 +52,62 @@ const mockIndexPatternCreationType = new IndexPatternCreationConfig({ isBeta: false, }); -function esClientFactory(search: (params: any) => any): LegacyApiCaller { - return { - search, - msearch: () => ({ - abort: () => {}, - ...new Promise((resolve) => resolve({})), - }), - }; -} - -const es = esClientFactory(() => successfulResponse); +const http = httpServiceMock.createStartContract(); +http.get.mockResolvedValue(successfulResponse); describe('getIndices', () => { it('should work in a basic case', async () => { - const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1); - expect(result.length).toBe(2); - expect(result[0].name).toBe('1'); - expect(result[1].name).toBe('2'); + const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); + expect(result.length).toBe(3); + expect(result[0].name).toBe('f-alias'); + expect(result[1].name).toBe('foo'); }); it('should ignore ccs query-all', async () => { - expect((await getIndices(es, mockIndexPatternCreationType, '*:', 10)).length).toBe(0); + expect((await getIndices(http, mockIndexPatternCreationType, '*:', false)).length).toBe(0); }); it('should ignore a single comma', async () => { - expect((await getIndices(es, mockIndexPatternCreationType, ',', 10)).length).toBe(0); - expect((await getIndices(es, mockIndexPatternCreationType, ',*', 10)).length).toBe(0); - expect((await getIndices(es, mockIndexPatternCreationType, ',foobar', 10)).length).toBe(0); - }); - - it('should trim the input', async () => { - let index; - const esClient = esClientFactory( - jest.fn().mockImplementation((params) => { - index = params.index; - }) - ); - - await getIndices(esClient, mockIndexPatternCreationType, 'kibana ', 1); - expect(index).toBe('kibana'); + expect((await getIndices(http, mockIndexPatternCreationType, ',', false)).length).toBe(0); + expect((await getIndices(http, mockIndexPatternCreationType, ',*', false)).length).toBe(0); + expect((await getIndices(http, mockIndexPatternCreationType, ',foobar', false)).length).toBe(0); }); - it('should use the limit', async () => { - let limit; - const esClient = esClientFactory( - jest.fn().mockImplementation((params) => { - limit = params.body.aggs.indices.terms.size; - }) - ); - await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 10); - expect(limit).toBe(10); + it('response object to item array', () => { + const result = { + indices: [ + { + name: 'test_index', + }, + { + name: 'frozen_index', + attributes: ['frozen' as ResolveIndexResponseItemIndexAttrs], + }, + ], + aliases: [ + { + name: 'test_alias', + indices: [], + }, + ], + data_streams: [ + { + name: 'test_data_stream', + backing_indices: [], + timestamp_field: 'test_timestamp_field', + }, + ], + }; + expect(responseToItemArray(result, mockIndexPatternCreationType)).toMatchSnapshot(); + expect(responseToItemArray({}, mockIndexPatternCreationType)).toEqual([]); }); describe('errors', () => { it('should handle errors gracefully', async () => { - const esClient = esClientFactory(() => errorResponse); - const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1); - expect(result.length).toBe(0); - }); - - it('should throw exceptions', async () => { - const esClient = esClientFactory(() => { - throw new Error('Fail'); + http.get.mockImplementationOnce(() => { + throw new Error('Test error'); }); - - await expect( - getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1) - ).rejects.toThrow(); - }); - - it('should handle index_not_found_exception errors gracefully', async () => { - const esClient = esClientFactory( - () => new Promise((resolve, reject) => reject(exceptionResponse)) - ); - const result = await getIndices(esClient, mockIndexPatternCreationType, 'kibana', 1); + const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); expect(result.length).toBe(0); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts index 9f75dc39a654c..c6a11de1bc4fc 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts @@ -17,17 +17,31 @@ * under the License. */ -import { get, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; +import { HttpStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public'; -import { DataPublicPluginStart } from '../../../../../data/public'; -import { MatchedIndex } from '../types'; +import { MatchedItem, ResolveIndexResponse, ResolveIndexResponseItemIndexAttrs } from '../types'; + +const aliasLabel = i18n.translate('indexPatternManagement.aliasLabel', { defaultMessage: 'Alias' }); +const dataStreamLabel = i18n.translate('indexPatternManagement.dataStreamLabel', { + defaultMessage: 'Data stream', +}); + +const indexLabel = i18n.translate('indexPatternManagement.indexLabel', { + defaultMessage: 'Index', +}); + +const frozenLabel = i18n.translate('indexPatternManagement.frozenLabel', { + defaultMessage: 'Frozen', +}); export async function getIndices( - es: DataPublicPluginStart['search']['__LEGACY']['esClient'], + http: HttpStart, indexPatternCreationType: IndexPatternCreationConfig, rawPattern: string, - limit: number -): Promise { + showAllIndices: boolean +): Promise { const pattern = rawPattern.trim(); // Searching for `*:` fails for CCS environments. The search request @@ -48,54 +62,58 @@ export async function getIndices( return []; } - // We need to always provide a limit and not rely on the default - if (!limit) { - throw new Error('`getIndices()` was called without the required `limit` parameter.'); - } - - const params = { - ignoreUnavailable: true, - index: pattern, - ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, - }, - }, - }, - }, - }; + const query = showAllIndices ? { expand_wildcards: 'all' } : undefined; try { - const response = await es.search(params); - if (!response || response.error || !response.aggregations) { - return []; - } - - return sortBy( - response.aggregations.indices.buckets - .map((bucket: { key: string; doc_count: number }) => { - return bucket.key; - }) - .map((indexName: string) => { - return { - name: indexName, - tags: indexPatternCreationType.getIndexTags(indexName), - }; - }), - 'name' + const response = await http.get( + `/internal/index-pattern-management/resolve_index/${pattern}`, + { query } ); - } catch (err) { - const type = get(err, 'body.error.caused_by.type'); - if (type === 'index_not_found_exception') { - // This happens in a CSS environment when the controlling node returns a 500 even though the data - // nodes returned a 404. Remove this when/if this is handled: https://github.com/elastic/elasticsearch/issues/27461 + if (!response) { return []; } - throw err; + + return responseToItemArray(response, indexPatternCreationType); + } catch { + return []; } } + +export const responseToItemArray = ( + response: ResolveIndexResponse, + indexPatternCreationType: IndexPatternCreationConfig +): MatchedItem[] => { + const source: MatchedItem[] = []; + + (response.indices || []).forEach((index) => { + const tags: MatchedItem['tags'] = [{ key: 'index', name: indexLabel, color: 'default' }]; + const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN); + + tags.push(...indexPatternCreationType.getIndexTags(index.name)); + if (isFrozen) { + tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' }); + } + + source.push({ + name: index.name, + tags, + item: index, + }); + }); + (response.aliases || []).forEach((alias) => { + source.push({ + name: alias.name, + tags: [{ key: 'alias', name: aliasLabel, color: 'default' }], + item: alias, + }); + }); + (response.data_streams || []).forEach((dataStream) => { + source.push({ + name: dataStream.name, + tags: [{ key: 'data_stream', name: dataStreamLabel, color: 'primary' }], + item: dataStream, + }); + }); + + return sortBy(source, 'name'); +}; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts index 65840aa64046d..c27eaa5ebc99e 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.test.ts @@ -18,7 +18,7 @@ */ import { getMatchedIndices } from './get_matched_indices'; -import { Tag } from '../types'; +import { Tag, MatchedItem } from '../types'; jest.mock('./../constants', () => ({ MAX_NUMBER_OF_MATCHING_INDICES: 6, @@ -32,18 +32,18 @@ const indices = [ { name: 'packetbeat', tags }, { name: 'metricbeat', tags }, { name: '.kibana', tags }, -]; +] as MatchedItem[]; const partialIndices = [ { name: 'kibana', tags }, { name: 'es', tags }, { name: '.kibana', tags }, -]; +] as MatchedItem[]; const exactIndices = [ { name: 'kibana', tags }, { name: '.kibana', tags }, -]; +] as MatchedItem[]; describe('getMatchedIndices', () => { it('should return all indices', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts index 7e2eeb17ab387..dbb166597152e 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_matched_indices.ts @@ -33,7 +33,7 @@ function isSystemIndex(index: string): boolean { return false; } -function filterSystemIndices(indices: MatchedIndex[], isIncludingSystemIndices: boolean) { +function filterSystemIndices(indices: MatchedItem[], isIncludingSystemIndices: boolean) { if (!indices) { return indices; } @@ -65,12 +65,12 @@ function filterSystemIndices(indices: MatchedIndex[], isIncludingSystemIndices: We call this `exact` matches because ES is telling us exactly what it matches */ -import { MatchedIndex } from '../types'; +import { MatchedItem } from '../types'; export function getMatchedIndices( - unfilteredAllIndices: MatchedIndex[], - unfilteredPartialMatchedIndices: MatchedIndex[], - unfilteredExactMatchedIndices: MatchedIndex[], + unfilteredAllIndices: MatchedItem[], + unfilteredPartialMatchedIndices: MatchedItem[], + unfilteredExactMatchedIndices: MatchedItem[], isIncludingSystemIndices: boolean = false ) { const allIndices = filterSystemIndices(unfilteredAllIndices, isIncludingSystemIndices); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts index 634bbd856ea86..b23924837ffb7 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/types.ts @@ -17,12 +17,54 @@ * under the License. */ -export interface MatchedIndex { +export interface MatchedItem { name: string; tags: Tag[]; + item: { + name: string; + backing_indices?: string[]; + timestamp_field?: string; + indices?: string[]; + aliases?: string[]; + attributes?: ResolveIndexResponseItemIndexAttrs[]; + data_stream?: string; + }; +} + +export interface ResolveIndexResponse { + indices?: ResolveIndexResponseItemIndex[]; + aliases?: ResolveIndexResponseItemAlias[]; + data_streams?: ResolveIndexResponseItemDataStream[]; +} + +export interface ResolveIndexResponseItem { + name: string; +} + +export interface ResolveIndexResponseItemDataStream extends ResolveIndexResponseItem { + backing_indices: string[]; + timestamp_field: string; +} + +export interface ResolveIndexResponseItemAlias extends ResolveIndexResponseItem { + indices: string[]; +} + +export interface ResolveIndexResponseItemIndex extends ResolveIndexResponseItem { + aliases?: string[]; + attributes?: ResolveIndexResponseItemIndexAttrs[]; + data_stream?: string; +} + +export enum ResolveIndexResponseItemIndexAttrs { + OPEN = 'open', + CLOSED = 'closed', + HIDDEN = 'hidden', + FROZEN = 'frozen', } export interface Tag { name: string; key: string; + color: string; } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap index 6bc99c356592e..7a7545580d82a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -836,7 +836,6 @@ exports[`FieldEditor should show deprecated lang warning 1`] = ` testlang , "painlessLink": , "scriptsInAggregation": Please familiarize yourself with - - + and with - - + before using scripted fields. diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 93574cde7dc85..ec8100db42085 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -76,6 +76,13 @@ const createInstance = async () => { }; }; +const docLinks = { + links: { + indexPatterns: {}, + scriptedFields: {}, + }, +}; + const createIndexPatternManagmentContext = () => { const { chrome, @@ -84,7 +91,6 @@ const createIndexPatternManagmentContext = () => { uiSettings, notifications, overlays, - docLinks, } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); diff --git a/src/plugins/index_pattern_management/public/service/creation/config.ts b/src/plugins/index_pattern_management/public/service/creation/config.ts index 95a91fd7594ca..04510b1d64e1e 100644 --- a/src/plugins/index_pattern_management/public/service/creation/config.ts +++ b/src/plugins/index_pattern_management/public/service/creation/config.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { MatchedIndex } from '../../components/create_index_pattern_wizard/types'; +import { MatchedItem } from '../../components/create_index_pattern_wizard/types'; const indexPatternTypeName = i18n.translate( 'indexPatternManagement.editIndexPattern.createIndex.defaultTypeName', @@ -105,7 +105,7 @@ export class IndexPatternCreationConfig { return []; } - public checkIndicesForErrors(indices: MatchedIndex[]) { + public checkIndicesForErrors(indices: MatchedItem[]) { return undefined; } diff --git a/src/plugins/data/public/search/expressions/utils/types.ts b/src/plugins/index_pattern_management/server/index.ts similarity index 71% rename from src/plugins/data/public/search/expressions/utils/types.ts rename to src/plugins/index_pattern_management/server/index.ts index b2311e664820e..02a4631589832 100644 --- a/src/plugins/data/public/search/expressions/utils/types.ts +++ b/src/plugins/index_pattern_management/server/index.ts @@ -17,18 +17,9 @@ * under the License. */ -interface InspectorStat { - label: string; - value: string; - description: string; -} +import { PluginInitializerContext } from 'src/core/server'; +import { IndexPatternManagementPlugin } from './plugin'; -/** @internal */ -export interface RequestInspectorStats { - indexPattern?: InspectorStat; - indexPatternId?: InspectorStat; - queryTime?: InspectorStat; - hitsTotal?: InspectorStat; - hits?: InspectorStat; - requestTime?: InspectorStat; +export function plugin(initializerContext: PluginInitializerContext) { + return new IndexPatternManagementPlugin(initializerContext); } diff --git a/src/plugins/index_pattern_management/server/plugin.ts b/src/plugins/index_pattern_management/server/plugin.ts new file mode 100644 index 0000000000000..ecca45cbcc453 --- /dev/null +++ b/src/plugins/index_pattern_management/server/plugin.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; + +export class IndexPatternManagementPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { + path: '/internal/index-pattern-management/resolve_index/{query}', + validate: { + params: schema.object({ + query: schema.string(), + }), + query: schema.object({ + expand_wildcards: schema.maybe( + schema.oneOf([ + schema.literal('all'), + schema.literal('open'), + schema.literal('closed'), + schema.literal('hidden'), + schema.literal('none'), + ]) + ), + }), + }, + }, + async (context, req, res) => { + const queryString = req.query.expand_wildcards + ? { expand_wildcards: req.query.expand_wildcards } + : null; + const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'transport.request', + { + method: 'GET', + path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ + queryString ? '?' + new URLSearchParams(queryString).toString() : '' + }`, + } + ); + return res.ok({ body: result }); + } + ); + } + + public start() {} + + public stop() {} +} diff --git a/src/plugins/inspector/common/adapters/index.ts b/src/plugins/inspector/common/adapters/index.ts index 8e1979ab33275..2fc465e7d0b2d 100644 --- a/src/plugins/inspector/common/adapters/index.ts +++ b/src/plugins/inspector/common/adapters/index.ts @@ -18,4 +18,4 @@ */ export { DataAdapter, FormattedData } from './data'; -export { RequestAdapter, RequestStatus } from './request'; +export { RequestAdapter, RequestStatistic, RequestStatistics, RequestStatus } from './request'; diff --git a/src/plugins/inspector/common/adapters/request/index.ts b/src/plugins/inspector/common/adapters/request/index.ts index 7359c56999a94..5c93757e86d05 100644 --- a/src/plugins/inspector/common/adapters/request/index.ts +++ b/src/plugins/inspector/common/adapters/request/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export { RequestStatus } from './types'; - +export { RequestStatistic, RequestStatistics, RequestStatus } from './types'; export { RequestAdapter } from './request_adapter'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2911a9ae75689..e2d6ae647abb1 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -35,7 +35,6 @@ export { export * from './core'; export * from '../common/errors'; export * from './field_wildcard'; -export * from './parse'; export * from './render_complete'; export * from './resize_checker'; export * from '../common/state_containers'; diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json index cdbed7fa06367..470544cf35b30 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_files.json @@ -406,6 +406,48 @@ "zh-tw": "國家" } }, + { + "layer_id": "world_countries_with_compromised_attribution", + "created_at": "2017-04-26T17:12:15.978370", + "attribution": [ + { + "label": { + "en": "
Made with NaturalEarth
" + }, + "url": { + "en": "http://www.naturalearthdata.com/about/terms-of-use" + } + }, + { + "label": { + "en": "Elastic Maps Service" + }, + "url": { + "en": "javascript:alert('foobar')" + } + } + ], + "formats": [ + { + "type": "geojson", + "url": "/files/world_countries_v1.geo.json", + "legacy_default": true + } + ], + "fields": [ + { + "type": "id", + "id": "iso2", + "label": { + "en": "ISO 3166-1 alpha-2 code" + } + } + ], + "legacy_ids": [], + "layer_name": { + "en": "World Countries (compromised)" + } + }, { "layer_id": "australia_states", "created_at": "2018-06-27T23:47:32.202380", diff --git a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json index c038bb411daec..1bbd94879b70c 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json +++ b/src/plugins/maps_legacy/public/__tests__/map/ems_mocks/sample_tiles.json @@ -11,7 +11,7 @@ { "label": { "en": "OpenMapTiles" }, "url": { "en": "https://openmaptiles.org" } }, { "label": { "en": "MapTiler" }, "url": { "en": "https://www.maptiler.com" } }, { - "label": { "en": "Elastic Maps Service" }, + "label": { "en": "" }, "url": { "en": "https://www.elastic.co/elastic-maps-service" } } ], diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index f4f88bd5807d5..ae40b2c92d40e 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -89,28 +89,31 @@ export class ServiceSettings { }; } + _backfillSettings = (fileLayer) => { + // Older version of Kibana stored EMS state in the URL-params + // Creates object literal with required parameters as key-value pairs + const format = fileLayer.getDefaultFormatType(); + const meta = fileLayer.getDefaultFormatMeta(); + + return { + name: fileLayer.getDisplayName(), + origin: fileLayer.getOrigin(), + id: fileLayer.getId(), + created_at: fileLayer.getCreatedAt(), + attribution: getAttributionString(fileLayer), + fields: fileLayer.getFieldsInLanguage(), + format: format, //legacy: format and meta are split up + meta: meta, //legacy, format and meta are split up + }; + }; + async getFileLayers() { if (!this._mapConfig.includeElasticMapsService) { return []; } const fileLayers = await this._emsClient.getFileLayers(); - return fileLayers.map((fileLayer) => { - //backfill to older settings - const format = fileLayer.getDefaultFormatType(); - const meta = fileLayer.getDefaultFormatMeta(); - - return { - name: fileLayer.getDisplayName(), - origin: fileLayer.getOrigin(), - id: fileLayer.getId(), - created_at: fileLayer.getCreatedAt(), - attribution: fileLayer.getHTMLAttribution(), - fields: fileLayer.getFieldsInLanguage(), - format: format, //legacy: format and meta are split up - meta: meta, //legacy, format and meta are split up - }; - }); + return fileLayers.map(this._backfillSettings); } /** @@ -139,7 +142,7 @@ export class ServiceSettings { id: tmsService.getId(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), }; }) ); @@ -159,16 +162,25 @@ export class ServiceSettings { this._emsClient.addQueryParams(additionalQueryParams); } - async getEMSHotLink(fileLayerConfig) { + async getFileLayerFromConfig(fileLayerConfig) { const fileLayers = await this._emsClient.getFileLayers(); - const layer = fileLayers.find((fileLayer) => { + return fileLayers.find((fileLayer) => { const hasIdByName = fileLayer.hasId(fileLayerConfig.name); //legacy const hasIdById = fileLayer.hasId(fileLayerConfig.id); return hasIdByName || hasIdById; }); + } + + async getEMSHotLink(fileLayerConfig) { + const layer = await this.getFileLayerFromConfig(fileLayerConfig); return layer ? layer.getEMSHotLink() : null; } + async loadFileLayerConfig(fileLayerConfig) { + const fileLayer = await this.getFileLayerFromConfig(fileLayerConfig); + return fileLayer ? this._backfillSettings(fileLayer) : null; + } + async _getAttributesForEMSTMSLayer(isDesaturated, isDarkMode) { const tmsServices = await this._emsClient.getTMSServices(); const emsTileLayerId = this._mapConfig.emsTileLayerId; @@ -189,7 +201,7 @@ export class ServiceSettings { url: await tmsService.getUrlTemplate(), minZoom: await tmsService.getMinZoom(), maxZoom: await tmsService.getMaxZoom(), - attribution: await tmsService.getHTMLAttribution(), + attribution: getAttributionString(tmsService), origin: ORIGIN.EMS, }; } @@ -255,3 +267,17 @@ export class ServiceSettings { return await response.json(); } } + +function getAttributionString(emsService) { + const attributions = emsService.getAttributions(); + const attributionSnippets = attributions.map((attribution) => { + const anchorTag = document.createElement('a'); + anchorTag.setAttribute('rel', 'noreferrer noopener'); + if (attribution.url.startsWith('http://') || attribution.url.startsWith('https://')) { + anchorTag.setAttribute('href', attribution.url); + } + anchorTag.textContent = attribution.label; + return anchorTag.outerHTML; + }); + return attributionSnippets.join(' | '); //!!!this is the current convention used in Kibana +} diff --git a/src/plugins/maps_legacy/public/map/service_settings.test.js b/src/plugins/maps_legacy/public/map/service_settings.test.js index 01facdc54137e..6e416f7fd5c84 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.test.js +++ b/src/plugins/maps_legacy/public/map/service_settings.test.js @@ -98,6 +98,9 @@ describe('service_settings (FKA tile_map test)', function () { expect(attrs.url.includes('{x}')).toEqual(true); expect(attrs.url.includes('{y}')).toEqual(true); expect(attrs.url.includes('{z}')).toEqual(true); + expect(attrs.attribution).toEqual( + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>' + ); const urlObject = url.parse(attrs.url, true); expect(urlObject.hostname).toEqual('tiles.foobar'); @@ -182,7 +185,7 @@ describe('service_settings (FKA tile_map test)', function () { minZoom: 0, maxZoom: 10, attribution: - 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service', + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | <iframe id=\'iframe\' style=\'position:fixed;height: 40%;width: 100%;top: 60%;left: 5%;right:5%;border: 0px;background:white;\' src=\'http://256.256.256.256\'></iframe>', subdomains: [], }, ]; @@ -276,7 +279,6 @@ describe('service_settings (FKA tile_map test)', function () { serviceSettings = makeServiceSettings({ includeElasticMapsService: false, }); - // mapConfig.includeElasticMapsService = false; const tilemapServices = await serviceSettings.getTMSServices(); const expected = []; expect(tilemapServices).toEqual(expected); @@ -289,7 +291,7 @@ describe('service_settings (FKA tile_map test)', function () { const serviceSettings = makeServiceSettings(); serviceSettings.setQueryParams({ foo: 'bar' }); const fileLayers = await serviceSettings.getFileLayers(); - expect(fileLayers.length).toEqual(18); + expect(fileLayers.length).toEqual(19); const assertions = fileLayers.map(async function (fileLayer) { expect(fileLayer.origin).toEqual(ORIGIN.EMS); const fileUrl = await serviceSettings.getUrlForRegionLayer(fileLayer); @@ -343,5 +345,16 @@ describe('service_settings (FKA tile_map test)', function () { const hotlink = await serviceSettings.getEMSHotLink(fileLayers[0]); expect(hotlink).toEqual('?locale=en#file/world_countries'); //url host undefined becuase emsLandingPageUrl is set at kibana-load }); + + it('should sanitize EMS attribution', async () => { + const serviceSettings = makeServiceSettings(); + const fileLayers = await serviceSettings.getFileLayers(); + const fileLayer = fileLayers.find((layer) => { + return layer.id === 'world_countries_with_compromised_attribution'; + }); + expect(fileLayer.attribution).toEqual( + '<div onclick=\'alert(1\')>Made with NaturalEarth</div> | Elastic Maps Service' + ); + }); }); }); diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 002d020fcd568..43959c367558f 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -22,9 +22,11 @@ import ChoroplethLayer from './choropleth_layer'; import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services'; import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider } from '../../maps_legacy/public'; +import { mapTooltipProvider, ORIGIN } from '../../maps_legacy/public'; +import _ from 'lodash'; export function createRegionMapVisualization({ + regionmapsConfig, serviceSettings, uiSettings, BaseMapsVisualization, @@ -60,17 +62,18 @@ export function createRegionMapVisualization({ }); } - if (!this._params.selectedJoinField && this._params.selectedLayer) { - this._params.selectedJoinField = this._params.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!this._params.selectedLayer) { + if (!selectedLayer) { return; } this._updateChoroplethLayerForNewMetrics( - this._params.selectedLayer.name, - this._params.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes, results ); @@ -90,29 +93,57 @@ export function createRegionMapVisualization({ this._kibanaMap.useUiStateFromVisualization(this._vis); } + async _loadConfig(fileLayerConfig) { + // Load the selected layer from the metadata-service. + // Do not use the selectedLayer from the visState. + // These settings are stored in the URL and can be used to inject dirty display content. + + if ( + fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS + (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects + ) { + return await serviceSettings.loadFileLayerConfig(fileLayerConfig); + } + + //Configured in the kibana.yml. Needs to be resolved through the settings. + const configuredLayer = regionmapsConfig.layers.find( + (layer) => layer.name === fileLayerConfig.name + ); + + if (configuredLayer) { + return { + ...configuredLayer, + attribution: _.escape(configuredLayer.attribution ? configuredLayer.attribution : ''), + }; + } + + return null; + } + async _updateParams() { await super._updateParams(); - const visParams = this._params; - if (!visParams.selectedJoinField && visParams.selectedLayer) { - visParams.selectedJoinField = visParams.selectedLayer.fields[0]; + const selectedLayer = await this._loadConfig(this._params.selectedLayer); + + if (!this._params.selectedJoinField && selectedLayer) { + this._params.selectedJoinField = selectedLayer.fields[0]; } - if (!visParams.selectedJoinField || !visParams.selectedLayer) { + if (!this._params.selectedJoinField || !selectedLayer) { return; } this._updateChoroplethLayerForNewProperties( - visParams.selectedLayer.name, - visParams.selectedLayer.attribution, + selectedLayer.name, + selectedLayer.attribution, this._params.showAllShapes ); const metricFieldFormatter = getFormatService().deserialize(this._params.metric.format); - this._choroplethLayer.setJoinField(visParams.selectedJoinField.name); - this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); - this._choroplethLayer.setLineWeight(visParams.outlineWeight); + this._choroplethLayer.setJoinField(this._params.selectedJoinField.name); + this._choroplethLayer.setColorRamp(truncatedColorMaps[this._params.colorSchema].value); + this._choroplethLayer.setLineWeight(this._params.outlineWeight); this._choroplethLayer.setTooltipFormatter( this._tooltipFormatter, metricFieldFormatter, diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts new file mode 100644 index 0000000000000..a3e2425c1f122 --- /dev/null +++ b/src/plugins/usage_collection/server/collector/collector.test.ts @@ -0,0 +1,213 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { loggingSystemMock } from '../../../../core/server/mocks'; +import { Collector } from './collector'; +import { UsageCollector } from './usage_collector'; + +const logger = loggingSystemMock.createLogger(); + +describe('collector', () => { + describe('options validations', () => { + it('should not accept an empty object', () => { + // @ts-expect-error + expect(() => new Collector(logger, {})).toThrowError( + 'Collector must be instantiated with a options.type string property' + ); + }); + + it('should fail if init is not a function', () => { + expect( + () => + new Collector(logger, { + type: 'my_test_collector', + // @ts-expect-error + init: 1, + }) + ).toThrowError( + 'If init property is passed, Collector must be instantiated with a options.init as a function property' + ); + }); + + it('should fail if fetch is not defined', () => { + expect( + () => + // @ts-expect-error + new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + }) + ).toThrowError('Collector must be instantiated with a options.fetch function property'); + }); + + it('should fail if fetch is not a function', () => { + expect( + () => + new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + // @ts-expect-error + fetch: 1, + }) + ).toThrowError('Collector must be instantiated with a options.fetch function property'); + }); + + it('should be OK with all mandatory properties', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => ({ testPass: 100 }), + }); + expect(collector).toBeDefined(); + }); + + it('should fallback when isReady is not provided', () => { + const fetchOutput = { testPass: 100 }; + // @ts-expect-error not providing isReady to test the logic fallback + const collector = new Collector(logger, { + type: 'my_test_collector', + fetch: () => fetchOutput, + }); + expect(collector.isReady()).toBe(true); + }); + }); + + describe('formatForBulkUpload', () => { + it('should use the default formatter', () => { + const fetchOutput = { testPass: 100 }; + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => fetchOutput, + }); + expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({ + type: 'my_test_collector', + payload: fetchOutput, + }); + }); + + it('should use a custom formatter', () => { + const fetchOutput = { testPass: 100 }; + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => fetchOutput, + formatForBulkUpload: (a) => ({ type: 'other_value', payload: { nested: a } }), + }); + expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({ + type: 'other_value', + payload: { nested: fetchOutput }, + }); + }); + + it("should use UsageCollector's default formatter", () => { + const fetchOutput = { testPass: 100 }; + const collector = new UsageCollector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => fetchOutput, + }); + expect(collector.formatForBulkUpload(fetchOutput)).toStrictEqual({ + type: 'kibana_stats', + payload: { usage: { my_test_collector: fetchOutput } }, + }); + }); + }); + + describe('schema TS validations', () => { + // These tests below are used to ensure types inference is working as expected. + // We don't intend to test any logic as such, just the relation between the types in `fetch` and `schema`. + // Using ts-expect-error when an error is expected will fail the compilation if there is not such error. + + test('when fetch returns a simple object', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => ({ testPass: 100 }), + schema: { + testPass: { type: 'long' }, + }, + }); + expect(collector).toBeDefined(); + }); + + test('when fetch returns array-properties and schema', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => ({ testPass: [{ name: 'a', value: 100 }] }), + schema: { + testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, + }); + expect(collector).toBeDefined(); + }); + + test('TS should complain when schema is missing some properties', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + fetch: () => ({ testPass: [{ name: 'a', value: 100 }], otherProp: 1 }), + // @ts-expect-error + schema: { + testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, + }); + expect(collector).toBeDefined(); + }); + + test('TS complains if schema misses any of the optional properties', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + // Need to be explicit with the returned type because TS struggles to identify it + fetch: (): { testPass?: Array<{ name: string; value: number }>; otherProp?: number } => { + if (Math.random() > 0.5) { + return { testPass: [{ name: 'a', value: 100 }] }; + } + return { otherProp: 1 }; + }, + // @ts-expect-error + schema: { + testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + }, + }); + expect(collector).toBeDefined(); + }); + + test('schema defines all the optional properties', () => { + const collector = new Collector(logger, { + type: 'my_test_collector', + isReady: () => false, + // Need to be explicit with the returned type because TS struggles to identify it + fetch: (): { testPass?: Array<{ name: string; value: number }>; otherProp?: number } => { + if (Math.random() > 0.5) { + return { testPass: [{ name: 'a', value: 100 }] }; + } + return { otherProp: 1 }; + }, + schema: { + testPass: { name: { type: 'keyword' }, value: { type: 'long' } }, + otherProp: { type: 'long' }, + }, + }); + expect(collector).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 9ae63b9f50e42..d57700024c088 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -34,20 +34,20 @@ export interface SchemaField { type: string; } -type Purify = { [P in T]: T }[T]; +export type RecursiveMakeSchemaFrom = U extends object + ? MakeSchemaFrom + : { type: AllowedSchemaTypes }; export type MakeSchemaFrom = { - [Key in Purify>]: Base[Key] extends Array - ? { type: AllowedSchemaTypes } - : Base[Key] extends object - ? MakeSchemaFrom - : { type: AllowedSchemaTypes }; + [Key in keyof Base]: Base[Key] extends Array + ? RecursiveMakeSchemaFrom + : RecursiveMakeSchemaFrom; }; export interface CollectorOptions { type: string; init?: Function; - schema?: MakeSchemaFrom; + schema?: MakeSchemaFrom>; // Using Required to enforce all optional keys in the object fetch: (callCluster: LegacyAPICaller) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed diff --git a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx index 8fe5cdb47a53d..c0c46f6714c2d 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/components/from_to_list.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import { EuiFieldText, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Ipv4Address } from '../../../../../kibana_utils/public'; +import { search } from '../../../../../data/public'; import { InputList, InputListConfig, InputModel, InputObject, InputItem } from './input_list'; const EMPTY_STRING = ''; @@ -49,7 +49,7 @@ const defaultConfig = { from: { value: '0.0.0.0', model: '0.0.0.0', isInvalid: false }, to: { value: '255.255.255.255', model: '255.255.255.255', isInvalid: false }, }, - validateClass: Ipv4Address, + validateClass: search.aggs.Ipv4Address, getModelValue: (item: FromToObject = {}) => ({ from: { value: item.from || EMPTY_STRING, diff --git a/src/plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx index 0228c79139f16..94fd2d9bc9151 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx @@ -22,6 +22,7 @@ import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@el import { i18n } from '@kbn/i18n'; import { IAggConfig, Query, QueryStringInput } from '../../../../data/public'; +import { useKibana } from '../../../../kibana_react/public'; interface FilterRowProps { id: string; @@ -48,6 +49,7 @@ function FilterRow({ onChangeValue, onRemoveFilter, }: FilterRowProps) { + const { services } = useKibana(); const [showCustomLabel, setShowCustomLabel] = useState(false); const filterLabel = i18n.translate('visDefaultEditor.controls.filters.filterLabel', { defaultMessage: 'Filter {index}', @@ -56,6 +58,13 @@ function FilterRow({ }, }); + const onBlur = () => { + if (value.query.length > 0) { + // Store filter to the query log so that it is available in autocomplete. + services.data.query.addToQueryLog(services.appName, value); + } + }; + const FilterControl = (
onChangeValue(id, query, customLabel)} + onBlur={onBlur} disableAutoFocus={!autoFocus} dataTestSubj={dataTestSubj} bubbleSubmitEvent={true} diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 05644eddc5fca..e0ec4801b3caf 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -39,7 +39,9 @@ const createStartContract = (): VisualizationsStart => ({ get: jest.fn(), all: jest.fn(), getAliases: jest.fn(), - savedVisualizationsLoader: {} as any, + savedVisualizationsLoader: { + get: jest.fn(), + } as any, showNewVisModal: jest.fn(), createVis: jest.fn(), convertFromSerializedVis: jest.fn(), diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index a6adaf1f3c62b..02ae1cc155dd2 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -50,7 +50,7 @@ export type PureVisState = SavedVisState; export interface VisualizeAppState { filters: Filter[]; - uiState: PersistedState; + uiState: Record; vis: PureVisState; query: Query; savedQuery?: string; diff --git a/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts new file mode 100644 index 0000000000000..885eec8a68d2d --- /dev/null +++ b/src/plugins/visualize/public/application/utils/create_visualize_app_state.test.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IKbnUrlStateStorage } from 'src/plugins/kibana_utils/public'; +import { createVisualizeAppState } from './create_visualize_app_state'; +import { migrateAppState } from './migrate_app_state'; +import { visualizeAppStateStub } from './stubs'; + +const mockStartStateSync = jest.fn(); +const mockStopStateSync = jest.fn(); + +jest.mock('../../../../kibana_utils/public', () => ({ + createStateContainer: jest.fn(() => 'stateContainer'), + syncState: jest.fn(() => ({ + start: mockStartStateSync, + stop: mockStopStateSync, + })), +})); +jest.mock('./migrate_app_state', () => ({ + migrateAppState: jest.fn(() => 'migratedAppState'), +})); + +const { createStateContainer, syncState } = jest.requireMock('../../../../kibana_utils/public'); + +describe('createVisualizeAppState', () => { + const kbnUrlStateStorage = ({ + set: jest.fn(), + get: jest.fn(() => ({ linked: false })), + } as unknown) as IKbnUrlStateStorage; + + const { stateContainer, stopStateSync } = createVisualizeAppState({ + stateDefaults: visualizeAppStateStub, + kbnUrlStateStorage, + }); + const transitions = createStateContainer.mock.calls[0][1]; + + test('should initialize visualize app state', () => { + expect(kbnUrlStateStorage.get).toHaveBeenCalledWith('_a'); + expect(migrateAppState).toHaveBeenCalledWith({ + ...visualizeAppStateStub, + linked: false, + }); + expect(kbnUrlStateStorage.set).toHaveBeenCalledWith('_a', 'migratedAppState', { + replace: true, + }); + expect(createStateContainer).toHaveBeenCalled(); + expect(syncState).toHaveBeenCalled(); + expect(mockStartStateSync).toHaveBeenCalled(); + }); + + test('should return the stateContainer and stopStateSync', () => { + expect(stateContainer).toBe('stateContainer'); + stopStateSync(); + expect(stopStateSync).toHaveBeenCalledTimes(1); + }); + + describe('stateContainer transitions', () => { + test('set', () => { + const newQuery = { query: '', language: '' }; + expect(transitions.set(visualizeAppStateStub)('query', newQuery)).toEqual({ + ...visualizeAppStateStub, + query: newQuery, + }); + }); + + test('setVis', () => { + const newVis = { data: 'data' }; + expect(transitions.setVis(visualizeAppStateStub)(newVis)).toEqual({ + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + ...newVis, + }, + }); + }); + + test('unlinkSavedSearch', () => { + const params = { + query: { query: '', language: '' }, + parentFilters: [{ test: 'filter2' }], + }; + expect(transitions.unlinkSavedSearch(visualizeAppStateStub)(params)).toEqual({ + ...visualizeAppStateStub, + query: params.query, + filters: [...visualizeAppStateStub.filters, { test: 'filter2' }], + linked: false, + }); + }); + + test('updateVisState: should not include resctricted param types', () => { + const newVisState = { + a: 1, + _b: 2, + $c: 3, + d: () => {}, + }; + expect(transitions.updateVisState(visualizeAppStateStub)(newVisState)).toEqual({ + ...visualizeAppStateStub, + vis: { a: 1 }, + }); + }); + + test('updateSavedQuery: add savedQuery', () => { + const savedQueryId = '123test'; + expect(transitions.updateSavedQuery(visualizeAppStateStub)(savedQueryId)).toEqual({ + ...visualizeAppStateStub, + savedQuery: savedQueryId, + }); + }); + + test('updateSavedQuery: remove savedQuery from state', () => { + const savedQueryId = '123test'; + expect( + transitions.updateSavedQuery({ ...visualizeAppStateStub, savedQuery: savedQueryId })() + ).toEqual(visualizeAppStateStub); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts new file mode 100644 index 0000000000000..31f0fc5f94479 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createSavedSearchesLoader } from '../../../../discover/public'; +import { getVisualizationInstance } from './get_visualization_instance'; +import { createVisualizeServicesMock } from './mocks'; +import { VisualizeServices } from '../types'; +import { BehaviorSubject } from 'rxjs'; + +const mockSavedSearchObj = {}; +const mockGetSavedSearch = jest.fn(() => mockSavedSearchObj); + +jest.mock('../../../../discover/public', () => ({ + createSavedSearchesLoader: jest.fn(() => ({ + get: mockGetSavedSearch, + })), +})); + +describe('getVisualizationInstance', () => { + const serializedVisMock = { + type: 'area', + }; + let savedVisMock: any; + let visMock: any; + let mockServices: jest.Mocked; + let subj: BehaviorSubject; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + subj = new BehaviorSubject({}); + visMock = { + type: {}, + data: {}, + }; + savedVisMock = {}; + // @ts-expect-error + mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); + // @ts-expect-error + mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock); + // @ts-expect-error + mockServices.visualizations.createVis.mockImplementation(() => visMock); + // @ts-expect-error + mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({ + getOutput$: jest.fn(() => subj.asObservable()), + })); + }); + + test('should create new instances of savedVis, vis and embeddableHandler', async () => { + const opts = { + type: 'area', + indexPattern: 'my_index_pattern', + }; + const { savedVis, savedSearch, vis, embeddableHandler } = await getVisualizationInstance( + mockServices, + opts + ); + + expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith(opts); + expect(savedVisMock.searchSourceFields).toEqual({ + index: opts.indexPattern, + }); + expect(mockServices.visualizations.convertToSerializedVis).toHaveBeenCalledWith(savedVisMock); + expect(mockServices.visualizations.createVis).toHaveBeenCalledWith( + serializedVisMock.type, + serializedVisMock + ); + expect(mockServices.createVisEmbeddableFromObject).toHaveBeenCalledWith(visMock, { + timeRange: undefined, + filters: undefined, + id: '', + }); + + expect(vis).toBe(visMock); + expect(savedVis).toBe(savedVisMock); + expect(embeddableHandler).toBeDefined(); + expect(savedSearch).toBeUndefined(); + }); + + test('should load existing vis by id and call vis type setup if exists', async () => { + const newVisObj = { data: {} }; + visMock.type.setup = jest.fn(() => newVisObj); + const { vis } = await getVisualizationInstance(mockServices, 'saved_vis_id'); + + expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith('saved_vis_id'); + expect(savedVisMock.searchSourceFields).toBeUndefined(); + expect(visMock.type.setup).toHaveBeenCalledWith(visMock); + expect(vis).toBe(newVisObj); + }); + + test('should create saved search instance if vis based on saved search id', async () => { + visMock.data.savedSearchId = 'saved_search_id'; + const { savedSearch } = await getVisualizationInstance(mockServices, 'saved_vis_id'); + + expect(createSavedSearchesLoader).toHaveBeenCalled(); + expect(mockGetSavedSearch).toHaveBeenCalledWith(visMock.data.savedSearchId); + expect(savedSearch).toBe(mockSavedSearchObj); + }); + + test('should subscribe on embeddable handler updates and send toasts on errors', async () => { + await getVisualizationInstance(mockServices, 'saved_vis_id'); + + subj.next({ + error: 'error', + }); + + expect(mockServices.toastNotifications.addError).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/mocks.ts b/src/plugins/visualize/public/application/utils/mocks.ts new file mode 100644 index 0000000000000..09e7ba23875ca --- /dev/null +++ b/src/plugins/visualize/public/application/utils/mocks.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { visualizationsPluginMock } from '../../../../visualizations/public/mocks'; +import { VisualizeServices } from '../types'; + +export const createVisualizeServicesMock = () => { + const coreStartMock = coreMock.createStart(); + const dataStartMock = dataPluginMock.createStartContract(); + const toastNotifications = coreStartMock.notifications.toasts; + const visualizations = visualizationsPluginMock.createStartContract(); + + return ({ + ...coreStartMock, + data: dataStartMock, + toastNotifications, + history: { + replace: jest.fn(), + location: { pathname: '' }, + }, + visualizations, + savedVisualizations: visualizations.savedVisualizationsLoader, + createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, + } as unknown) as jest.Mocked; +}; diff --git a/src/plugins/visualize/public/application/utils/stubs.ts b/src/plugins/visualize/public/application/utils/stubs.ts new file mode 100644 index 0000000000000..1bbd738a739cf --- /dev/null +++ b/src/plugins/visualize/public/application/utils/stubs.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VisualizeAppState } from '../types'; + +export const visualizeAppStateStub: VisualizeAppState = { + uiState: { + vis: { + defaultColors: { + '0 - 2': 'rgb(165,0,38)', + '2 - 3': 'rgb(255,255,190)', + '3 - 4': 'rgb(0,104,55)', + }, + }, + }, + query: { query: '', language: 'kuery' }, + filters: [], + vis: { + title: '[eCommerce] Average Sold Quantity', + type: 'gauge', + aggs: [ + { + id: '1', + enabled: true, + // @ts-expect-error + type: 'avg', + schema: 'metric', + params: { field: 'total_quantity', customLabel: 'average items' }, + }, + ], + params: { + type: 'gauge', + addTooltip: true, + addLegend: true, + isDisplayWarning: false, + gauge: { + extendRange: true, + percentageMode: false, + gaugeType: 'Circle', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'Labels', + colorsRange: [ + { from: 0, to: 2 }, + { from: 2, to: 3 }, + { from: 3, to: 4 }, + ], + invertColors: true, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333' }, + type: 'meter', + style: { + bgWidth: 0.9, + width: 0.9, + mask: false, + bgMask: false, + maskBars: 50, + bgFill: '#eee', + bgColor: false, + subText: 'per order', + fontSize: 60, + labelColor: true, + }, + minAngle: 0, + maxAngle: 6.283185307179586, + alignment: 'horizontal', + }, + }, + }, + linked: false, +}; diff --git a/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts new file mode 100644 index 0000000000000..904816db22278 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_chrome_visibility.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { chromeServiceMock } from '../../../../../../core/public/mocks'; +import { useChromeVisibility } from './use_chrome_visibility'; + +describe('useChromeVisibility', () => { + const chromeMock = chromeServiceMock.createStartContract(); + + test('should set up a subscription for chrome visibility', () => { + const { result } = renderHook(() => useChromeVisibility(chromeMock)); + + expect(chromeMock.getIsVisible$).toHaveBeenCalled(); + expect(result.current).toEqual(false); + }); + + test('should change chrome visibility to true if change was emitted', () => { + const { result } = renderHook(() => useChromeVisibility(chromeMock)); + const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value; + act(() => { + behaviorSubj.next(true); + }); + + expect(result.current).toEqual(true); + }); + + test('should destroy a subscription', () => { + const { unmount } = renderHook(() => useChromeVisibility(chromeMock)); + const behaviorSubj = chromeMock.getIsVisible$.mock.results[0].value; + const subscription = behaviorSubj.observers[0]; + subscription.unsubscribe = jest.fn(); + + unmount(); + + expect(subscription.unsubscribe).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts new file mode 100644 index 0000000000000..3546ee7b321bb --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts @@ -0,0 +1,327 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { useEditorUpdates } from './use_editor_updates'; +import { + VisualizeServices, + VisualizeAppStateContainer, + SavedVisInstance, + IEditorController, +} from '../../types'; +import { visualizeAppStateStub } from '../stubs'; +import { createVisualizeServicesMock } from '../mocks'; + +describe('useEditorUpdates', () => { + const eventEmitter = new EventEmitter(); + const setHasUnsavedChangesMock = jest.fn(); + let mockServices: jest.Mocked; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + // @ts-expect-error + mockServices.visualizations.convertFromSerializedVis.mockImplementation(() => ({ + visState: visualizeAppStateStub.vis, + })); + }); + + test('should not create any subscriptions if app state container is not ready', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + null, + undefined, + undefined + ) + ); + + expect(result.current).toEqual({ + isEmbeddableRendered: false, + currentAppState: undefined, + }); + }); + + let unsubscribeStateUpdatesMock: jest.Mock; + let appState: VisualizeAppStateContainer; + let savedVisInstance: SavedVisInstance; + let visEditorController: IEditorController; + let timeRange: any; + let mockFilters: any; + + beforeEach(() => { + unsubscribeStateUpdatesMock = jest.fn(); + appState = ({ + getState: jest.fn(() => visualizeAppStateStub), + subscribe: jest.fn(() => unsubscribeStateUpdatesMock), + transitions: { + set: jest.fn(), + }, + } as unknown) as VisualizeAppStateContainer; + savedVisInstance = ({ + vis: { + uiState: { + on: jest.fn(), + off: jest.fn(), + setSilent: jest.fn(), + getChanges: jest.fn(() => visualizeAppStateStub.uiState), + }, + data: {}, + serialize: jest.fn(), + title: visualizeAppStateStub.vis.title, + setState: jest.fn(), + }, + embeddableHandler: { + updateInput: jest.fn(), + reload: jest.fn(), + }, + savedVis: {}, + } as unknown) as SavedVisInstance; + visEditorController = { + render: jest.fn(), + destroy: jest.fn(), + }; + timeRange = { + from: 'now-15m', + to: 'now', + }; + mockFilters = ['mockFilters']; + // @ts-expect-error + mockServices.data.query.timefilter.timefilter.getTime.mockImplementation(() => timeRange); + // @ts-expect-error + mockServices.data.query.filterManager.getFilters.mockImplementation(() => mockFilters); + }); + + test('should set up current app state and render the editor', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + visEditorController + ) + ); + + expect(result.current).toEqual({ + isEmbeddableRendered: false, + currentAppState: visualizeAppStateStub, + }); + expect(savedVisInstance.vis.uiState.setSilent).toHaveBeenCalledWith( + visualizeAppStateStub.uiState + ); + expect(visEditorController.render).toHaveBeenCalledWith({ + core: mockServices, + data: mockServices.data, + uiState: savedVisInstance.vis.uiState, + timeRange, + filters: mockFilters, + query: visualizeAppStateStub.query, + linked: false, + savedSearch: undefined, + }); + }); + + test('should update embeddable handler in embeded mode', () => { + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledWith({ + timeRange, + filters: mockFilters, + query: visualizeAppStateStub.query, + }); + }); + + test('should update isEmbeddableRendered value when embedabble is rendered', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + act(() => { + eventEmitter.emit('embeddableRendered'); + }); + + expect(result.current.isEmbeddableRendered).toBe(true); + }); + + test('should destroy subscriptions on unmount', () => { + const { unmount } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + + unmount(); + + expect(unsubscribeStateUpdatesMock).toHaveBeenCalledTimes(1); + expect(savedVisInstance.vis.uiState.off).toHaveBeenCalledTimes(1); + }); + + describe('subscribe on app state updates', () => { + test('should subscribe on appState updates', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener(visualizeAppStateStub); + }); + + expect(result.current.currentAppState).toEqual(visualizeAppStateStub); + expect(setHasUnsavedChangesMock).toHaveBeenCalledWith(true); + expect(savedVisInstance.embeddableHandler.updateInput).toHaveBeenCalledTimes(2); + }); + + test('should update vis state and reload the editor if changes come from url', () => { + const { result } = renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + const newAppState = { + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + title: 'New title', + }, + }; + const { aggs, ...visState } = newAppState.vis; + const updateEditorSpy = jest.fn(); + + eventEmitter.on('updateEditor', updateEditorSpy); + + act(() => { + listener(newAppState); + }); + + expect(result.current.currentAppState).toEqual(newAppState); + expect(savedVisInstance.vis.setState).toHaveBeenCalledWith({ + ...visState, + data: { aggs }, + }); + expect(savedVisInstance.embeddableHandler.reload).toHaveBeenCalled(); + expect(updateEditorSpy).toHaveBeenCalled(); + }); + + describe('handle linked search changes', () => { + test('should update saved search id in saved instance', () => { + // @ts-expect-error + savedVisInstance.savedSearch = { + id: 'saved_search_id', + }; + + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener({ + ...visualizeAppStateStub, + linked: true, + }); + }); + + expect(savedVisInstance.savedVis.savedSearchId).toEqual('saved_search_id'); + expect(savedVisInstance.vis.data.savedSearchId).toEqual('saved_search_id'); + }); + + test('should remove saved search id from vis instance', () => { + // @ts-expect-error + savedVisInstance.savedVis = { + savedSearchId: 'saved_search_id', + }; + // @ts-expect-error + savedVisInstance.savedSearch = { + id: 'saved_search_id', + }; + savedVisInstance.vis.data.savedSearchId = 'saved_search_id'; + + renderHook(() => + useEditorUpdates( + mockServices, + eventEmitter, + setHasUnsavedChangesMock, + appState, + savedVisInstance, + undefined + ) + ); + // @ts-expect-error + const listener = appState.subscribe.mock.calls[0][0]; + + act(() => { + listener(visualizeAppStateStub); + }); + + expect(savedVisInstance.savedVis.savedSearchId).toBeUndefined(); + expect(savedVisInstance.vis.data.savedSearchId).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts new file mode 100644 index 0000000000000..4c9ebbc1d9abd --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_linked_search_updates.test.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { useLinkedSearchUpdates } from './use_linked_search_updates'; +import { VisualizeServices, SavedVisInstance, VisualizeAppStateContainer } from '../../types'; +import { createVisualizeServicesMock } from '../mocks'; + +describe('useLinkedSearchUpdates', () => { + let mockServices: jest.Mocked; + const eventEmitter = new EventEmitter(); + const savedVisInstance = ({ + vis: { + data: { + searchSource: { setField: jest.fn(), setParent: jest.fn() }, + }, + }, + savedVis: {}, + embeddableHandler: {}, + } as unknown) as SavedVisInstance; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + }); + + it('should not subscribe on unlinkFromSavedSearch event if appState or savedSearch are not defined', () => { + renderHook(() => useLinkedSearchUpdates(mockServices, eventEmitter, null, savedVisInstance)); + + expect(mockServices.toastNotifications.addSuccess).not.toHaveBeenCalled(); + }); + + it('should subscribe on unlinkFromSavedSearch event if vis is based on saved search', () => { + const mockAppState = ({ + transitions: { + unlinkSavedSearch: jest.fn(), + }, + } as unknown) as VisualizeAppStateContainer; + savedVisInstance.savedSearch = ({ + searchSource: { + getParent: jest.fn(), + getField: jest.fn(), + getOwnField: jest.fn(), + }, + title: 'savedSearch', + } as unknown) as SavedVisInstance['savedSearch']; + + renderHook(() => + useLinkedSearchUpdates(mockServices, eventEmitter, mockAppState, savedVisInstance) + ); + + eventEmitter.emit('unlinkFromSavedSearch'); + + expect(savedVisInstance.savedSearch?.searchSource?.getParent).toHaveBeenCalled(); + expect(savedVisInstance.savedSearch?.searchSource?.getField).toHaveBeenCalledWith('index'); + expect(mockAppState.transitions.unlinkSavedSearch).toHaveBeenCalled(); + expect(mockServices.toastNotifications.addSuccess).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts new file mode 100644 index 0000000000000..a6b6d8ca0e837 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -0,0 +1,224 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; + +import { coreMock } from '../../../../../../core/public/mocks'; +import { useSavedVisInstance } from './use_saved_vis_instance'; +import { redirectWhenMissing } from '../../../../../kibana_utils/public'; +import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; +import { VisualizeServices } from '../../types'; +import { VisualizeConstants } from '../../visualize_constants'; + +const mockDefaultEditorControllerDestroy = jest.fn(); +const mockEmbeddableHandlerDestroy = jest.fn(); +const mockEmbeddableHandlerRender = jest.fn(); +const mockSavedVisDestroy = jest.fn(); +const savedVisId = '9ca7aa90-b892-11e8-a6d9-e546fe2bba5f'; +const mockSavedVisInstance = { + embeddableHandler: { + destroy: mockEmbeddableHandlerDestroy, + render: mockEmbeddableHandlerRender, + }, + savedVis: { + id: savedVisId, + title: 'Test Vis', + destroy: mockSavedVisDestroy, + }, + vis: { + type: {}, + }, +}; + +jest.mock('../get_visualization_instance', () => ({ + getVisualizationInstance: jest.fn(() => mockSavedVisInstance), +})); +jest.mock('../breadcrumbs', () => ({ + getEditBreadcrumbs: jest.fn((text) => text), + getCreateBreadcrumbs: jest.fn((text) => text), +})); +jest.mock('../../../../../vis_default_editor/public', () => ({ + DefaultEditorController: jest.fn(() => ({ destroy: mockDefaultEditorControllerDestroy })), +})); +jest.mock('../../../../../kibana_utils/public'); + +const mockGetVisualizationInstance = jest.requireMock('../get_visualization_instance') + .getVisualizationInstance; + +describe('useSavedVisInstance', () => { + const coreStartMock = coreMock.createStart(); + const toastNotifications = coreStartMock.notifications.toasts; + let mockServices: VisualizeServices; + const eventEmitter = new EventEmitter(); + + beforeEach(() => { + mockServices = ({ + ...coreStartMock, + toastNotifications, + history: { + location: { + pathname: VisualizeConstants.EDIT_PATH, + }, + replace: () => {}, + }, + visualizations: { + all: jest.fn(() => [ + { + name: 'area', + requiresSearch: true, + options: { + showIndexSelection: true, + }, + }, + { name: 'gauge' }, + ]), + }, + } as unknown) as VisualizeServices; + + mockDefaultEditorControllerDestroy.mockClear(); + mockEmbeddableHandlerDestroy.mockClear(); + mockEmbeddableHandlerRender.mockClear(); + mockSavedVisDestroy.mockClear(); + toastNotifications.addWarning.mockClear(); + mockGetVisualizationInstance.mockClear(); + }); + + test('should not load instance until chrome is defined', () => { + const { result } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, undefined, undefined) + ); + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeUndefined(); + expect(result.current.savedVisInstance).toBeUndefined(); + expect(result.current.visEditorRef).toBeDefined(); + }); + + describe('edit saved visualization route', () => { + test('should load instance and initiate an editor if chrome is set up', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + ); + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); + expect(mockGetVisualizationInstance.mock.calls.length).toBe(1); + + await waitForNextUpdate(); + expect(mockServices.chrome.setBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(getEditBreadcrumbs).toHaveBeenCalledWith('Test Vis'); + expect(getCreateBreadcrumbs).not.toHaveBeenCalled(); + expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeDefined(); + expect(result.current.savedVisInstance).toBeDefined(); + }); + + test('should destroy the editor and the savedVis on unmount if chrome exists', async () => { + const { unmount, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, savedVisId) + ); + + await waitForNextUpdate(); + unmount(); + + expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1); + expect(mockEmbeddableHandlerDestroy).not.toHaveBeenCalled(); + expect(mockSavedVisDestroy.mock.calls.length).toBe(1); + }); + }); + + describe('create new visualization route', () => { + beforeEach(() => { + mockServices.history.location = { + ...mockServices.history.location, + pathname: VisualizeConstants.CREATE_PATH, + search: '?type=area&indexPattern=1a2b3c4d', + }; + delete mockSavedVisInstance.savedVis.id; + }); + + test('should create new visualization based on search params', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, true, undefined) + ); + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, { + indexPattern: '1a2b3c4d', + type: 'area', + }); + + await waitForNextUpdate(); + + expect(getCreateBreadcrumbs).toHaveBeenCalled(); + expect(mockEmbeddableHandlerRender).not.toHaveBeenCalled(); + expect(result.current.visEditorController).toBeDefined(); + expect(result.current.savedVisInstance).toBeDefined(); + }); + + test('should throw error if vis type is invalid', async () => { + mockServices.history.location = { + ...mockServices.history.location, + search: '?type=myVisType&indexPattern=1a2b3c4d', + }; + + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(redirectWhenMissing).toHaveBeenCalled(); + expect(toastNotifications.addWarning).toHaveBeenCalled(); + }); + + test("should throw error if index pattern or saved search id doesn't exist in search params", async () => { + mockServices.history.location = { + ...mockServices.history.location, + search: '?type=area', + }; + + renderHook(() => useSavedVisInstance(mockServices, eventEmitter, true, undefined)); + + expect(mockGetVisualizationInstance).not.toHaveBeenCalled(); + expect(redirectWhenMissing).toHaveBeenCalled(); + expect(toastNotifications.addWarning).toHaveBeenCalled(); + }); + }); + + describe('embeded mode', () => { + test('should create new visualization based on search params', async () => { + const { result, unmount, waitForNextUpdate } = renderHook(() => + useSavedVisInstance(mockServices, eventEmitter, false, savedVisId) + ); + + // mock editor ref + // @ts-expect-error + result.current.visEditorRef.current = 'div'; + + expect(mockGetVisualizationInstance).toHaveBeenCalledWith(mockServices, savedVisId); + + await waitForNextUpdate(); + + expect(mockEmbeddableHandlerRender).toHaveBeenCalled(); + expect(result.current.visEditorController).toBeUndefined(); + expect(result.current.savedVisInstance).toBeDefined(); + + unmount(); + expect(mockDefaultEditorControllerDestroy).not.toHaveBeenCalled(); + expect(mockEmbeddableHandlerDestroy.mock.calls.length).toBe(1); + expect(mockSavedVisDestroy.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts new file mode 100644 index 0000000000000..e885067c58184 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.test.ts @@ -0,0 +1,210 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { EventEmitter } from 'events'; +import { Observable } from 'rxjs'; + +import { useVisualizeAppState } from './use_visualize_app_state'; +import { VisualizeServices, SavedVisInstance } from '../../types'; +import { visualizeAppStateStub } from '../stubs'; +import { VisualizeConstants } from '../../visualize_constants'; +import { createVisualizeServicesMock } from '../mocks'; + +jest.mock('../utils'); +jest.mock('../create_visualize_app_state'); +jest.mock('../../../../../data/public'); + +describe('useVisualizeAppState', () => { + const { visStateToEditorState } = jest.requireMock('../utils'); + const { createVisualizeAppState } = jest.requireMock('../create_visualize_app_state'); + const { connectToQueryState } = jest.requireMock('../../../../../data/public'); + const stopStateSyncMock = jest.fn(); + const stateContainerGetStateMock = jest.fn(() => visualizeAppStateStub); + const stopSyncingAppFiltersMock = jest.fn(); + const stateContainer = { + getState: stateContainerGetStateMock, + state$: new Observable(), + transitions: { + updateVisState: jest.fn(), + set: jest.fn(), + }, + }; + + visStateToEditorState.mockImplementation(() => visualizeAppStateStub); + createVisualizeAppState.mockImplementation(() => ({ + stateContainer, + stopStateSync: stopStateSyncMock, + })); + connectToQueryState.mockImplementation(() => stopSyncingAppFiltersMock); + + const eventEmitter = new EventEmitter(); + const savedVisInstance = ({ + vis: { + setState: jest.fn().mockResolvedValue({}), + }, + savedVis: {}, + embeddableHandler: {}, + } as unknown) as SavedVisInstance; + let mockServices: jest.Mocked; + + beforeEach(() => { + mockServices = createVisualizeServicesMock(); + + stopStateSyncMock.mockClear(); + stopSyncingAppFiltersMock.mockClear(); + visStateToEditorState.mockClear(); + }); + + it("should not create appState if vis instance isn't ready", () => { + const { result } = renderHook(() => useVisualizeAppState(mockServices, eventEmitter)); + + expect(result.current).toEqual({ + appState: null, + hasUnappliedChanges: false, + }); + }); + + it('should create appState and connect it to query search params', () => { + const { result } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + expect(visStateToEditorState).toHaveBeenCalledWith(savedVisInstance, mockServices); + expect(createVisualizeAppState).toHaveBeenCalledWith({ + stateDefaults: visualizeAppStateStub, + kbnUrlStateStorage: undefined, + }); + expect(mockServices.data.query.filterManager.setAppFilters).toHaveBeenCalledWith( + visualizeAppStateStub.filters + ); + expect(connectToQueryState).toHaveBeenCalledWith(mockServices.data.query, expect.any(Object), { + filters: 'appState', + }); + expect(result.current).toEqual({ + appState: stateContainer, + hasUnappliedChanges: false, + }); + }); + + it('should stop state and app filters syncing with query on destroy', () => { + const { unmount } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + unmount(); + + expect(stopStateSyncMock).toBeCalledTimes(1); + expect(stopSyncingAppFiltersMock).toBeCalledTimes(1); + }); + + it('should be subscribed on dirtyStateChange event from an editor', () => { + const { result } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + act(() => { + eventEmitter.emit('dirtyStateChange', { isDirty: true }); + }); + + expect(result.current.hasUnappliedChanges).toEqual(true); + expect(stateContainer.transitions.updateVisState).not.toHaveBeenCalled(); + expect(visStateToEditorState).toHaveBeenCalledTimes(1); + + act(() => { + eventEmitter.emit('dirtyStateChange', { isDirty: false }); + }); + + expect(result.current.hasUnappliedChanges).toEqual(false); + expect(stateContainer.transitions.updateVisState).toHaveBeenCalledWith( + visualizeAppStateStub.vis + ); + expect(visStateToEditorState).toHaveBeenCalledTimes(2); + }); + + describe('update vis state if the url params are not equal with the saved object vis state', () => { + const newAgg = { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'total_quantity', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: '', + }, + }; + const state = { + ...visualizeAppStateStub, + vis: { + ...visualizeAppStateStub.vis, + aggs: [...visualizeAppStateStub.vis.aggs, newAgg], + }, + }; + + it('should successfully update vis state and set up app state container', async () => { + // @ts-expect-error + stateContainerGetStateMock.mockImplementation(() => state); + const { result, waitForNextUpdate } = renderHook(() => + useVisualizeAppState(mockServices, eventEmitter, savedVisInstance) + ); + + await waitForNextUpdate(); + + const { aggs, ...visState } = stateContainer.getState().vis; + const expectedNewVisState = { + ...visState, + data: { aggs: state.vis.aggs }, + }; + + expect(savedVisInstance.vis.setState).toHaveBeenCalledWith(expectedNewVisState); + expect(result.current).toEqual({ + appState: stateContainer, + hasUnappliedChanges: false, + }); + }); + + it(`should add warning toast and redirect to the landing page + if setting new vis state was not successful, e.x. invalid query params`, async () => { + // @ts-expect-error + stateContainerGetStateMock.mockImplementation(() => state); + // @ts-expect-error + savedVisInstance.vis.setState.mockRejectedValue({ + message: 'error', + }); + + renderHook(() => useVisualizeAppState(mockServices, eventEmitter, savedVisInstance)); + + await new Promise((res) => { + setTimeout(() => res()); + }); + + expect(mockServices.toastNotifications.addWarning).toHaveBeenCalled(); + expect(mockServices.history.replace).toHaveBeenCalledWith( + `${VisualizeConstants.LANDING_PAGE_PATH}?notFound=visualization` + ); + }); + }); +}); diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 7c4f75bea8801..fa4bdc8ed2266 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -110,7 +110,7 @@ module.exports = function (grunt) { customLaunchers: { Chrome_Headless: { base: 'Chrome', - flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222', '--no-sandbox'], + flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222'], }, }, diff --git a/tasks/test_jest.js b/tasks/test_jest.js index 810ed42324840..d8f51806e8ddc 100644 --- a/tasks/test_jest.js +++ b/tasks/test_jest.js @@ -22,7 +22,7 @@ const { resolve } = require('path'); module.exports = function (grunt) { grunt.registerTask('test:jest', function () { const done = this.async(); - runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done); + runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done); }); grunt.registerTask('test:jest_integration', function () { @@ -30,10 +30,10 @@ module.exports = function (grunt) { runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done); }); - function runJest(jestScript, args = []) { + function runJest(jestScript) { const serverCmd = { cmd: 'node', - args: [jestScript, '--ci', ...args], + args: [jestScript, '--ci'], opts: { stdio: 'inherit' }, }; diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 906f0b83e99e7..949a01ff7873a 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -218,6 +218,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); await queryBar.setQuery(''); + // To remove focus of the of the search bar so date/time picker can show + await PageObjects.discover.selectIndexPattern(defaultSettings.defaultIndex); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug( diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 8209f3e1ac9d6..cb8b5a6ddc65f 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -22,6 +22,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); + const es = getService('legacyEs'); const PageObjects = getPageObjects(['settings', 'common']); describe('"Create Index Pattern" wizard', function () { @@ -48,5 +49,59 @@ export default function ({ getService, getPageObjects }) { expect(isEnabled).to.be.ok(); }); }); + + describe('data streams', () => { + it('can be an index pattern', async () => { + await es.transport.request({ + path: '/_index_template/generic-logs', + method: 'PUT', + body: { + index_patterns: ['logs-*', 'test_data_stream'], + template: { + mappings: { + properties: { + '@timestamp': { + type: 'date', + }, + }, + }, + }, + data_stream: { + timestamp_field: '@timestamp', + }, + }, + }); + + await es.transport.request({ + path: '/_data_stream/test_data_stream', + method: 'PUT', + }); + + await PageObjects.settings.createIndexPattern('test_data_stream', false); + + await es.transport.request({ + path: '/_data_stream/test_data_stream', + method: 'DELETE', + }); + }); + }); + + describe('index alias', () => { + it('can be an index pattern', async () => { + await es.transport.request({ + path: '/blogs/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + + await es.transport.request({ + path: '/_aliases', + method: 'POST', + body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] }, + }); + + await PageObjects.settings.createIndexPattern('alias1', false); + }); + }); }); } diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 92482a3779771..7c325ba6d4aec 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -290,14 +290,16 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide dashboardName: string, saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true } ) { - await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); + await retry.try(async () => { + await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); - if (saveOptions.needsConfirm) { - await this.clickSave(); - } + if (saveOptions.needsConfirm) { + await this.clickSave(); + } - // Confirm that the Dashboard has actually been saved - await testSubjects.existOrFail('saveDashboardSuccess'); + // Confirm that the Dashboard has actually been saved + await testSubjects.existOrFail('saveDashboardSuccess'); + }); const message = await PageObjects.common.closeToast(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.common.waitForSaveModalToClose(); diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 49133d8b13836..a08598fc42d68 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -257,7 +257,7 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide public async openSavedVisualization(vizName: string) { const dataTestSubj = `visListingTitleLink-${vizName.split(' ').join('-')}`; - await testSubjects.click(dataTestSubj); + await testSubjects.click(dataTestSubj, 20000); await header.waitUntilLoadingHasFinished(); } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 27814060e70c1..78f659a064a0c 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -88,6 +88,7 @@ async function attemptToCreateCommand( ) { const attemptId = ++attemptCounter; log.debug('[webdriver] Creating session'); + const remoteSessionUrl = process.env.REMOTE_SESSION_URL; const buildDriverInstance = async () => { switch (browserType) { @@ -133,11 +134,20 @@ async function attemptToCreateCommand( chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' }); chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts); - const session = await new Builder() - .forBrowser(browserType) - .withCapabilities(chromeCapabilities) - .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging()) - .build(); + let session; + if (remoteSessionUrl) { + session = await new Builder() + .forBrowser(browserType) + .withCapabilities(chromeCapabilities) + .usingServer(remoteSessionUrl) + .build(); + } else { + session = await new Builder() + .forBrowser(browserType) + .withCapabilities(chromeCapabilities) + .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging()) + .build(); + } return { session, @@ -284,11 +294,19 @@ async function attemptToCreateCommand( logLevel: 'TRACE', }); - const session = await new Builder() - .forBrowser(browserType) - .withCapabilities(ieCapabilities) - .build(); - + let session; + if (remoteSessionUrl) { + session = await new Builder() + .forBrowser(browserType) + .withCapabilities(ieCapabilities) + .usingServer(remoteSessionUrl) + .build(); + } else { + session = await new Builder() + .forBrowser(browserType) + .withCapabilities(ieCapabilities) + .build(); + } return { session, consoleLog$: Rx.EMPTY, diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh deleted file mode 100755 index 503d12b2f6d73..0000000000000 --- a/test/scripts/checks/doc_api_changes.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:checkDocApiChanges diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh deleted file mode 100755 index 513664263791b..0000000000000 --- a/test/scripts/checks/file_casing.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:checkFileCasing diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh deleted file mode 100755 index 7a6fd46c46c76..0000000000000 --- a/test/scripts/checks/i18n.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:i18nCheck diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh deleted file mode 100755 index a08d7d07a24a1..0000000000000 --- a/test/scripts/checks/licenses.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:licenses diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh deleted file mode 100755 index 1d43d32c9feb8..0000000000000 --- a/test/scripts/checks/lock_file_symlinks.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:checkLockfileSymlinks diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh deleted file mode 100755 index 9184758577654..0000000000000 --- a/test/scripts/checks/test_hardening.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_hardening diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh deleted file mode 100755 index 5f9aafe80e10e..0000000000000 --- a/test/scripts/checks/test_projects.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_projects diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh deleted file mode 100755 index d667c753baec2..0000000000000 --- a/test/scripts/checks/ts_projects.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:checkTsProjects diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh deleted file mode 100755 index 07c49638134be..0000000000000 --- a/test/scripts/checks/type_check.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:typeCheck diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh deleted file mode 100755 index b73a71e7ff7fd..0000000000000 --- a/test/scripts/checks/verify_dependency_versions.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:verifyDependencyVersions diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh deleted file mode 100755 index 9f8343e540861..0000000000000 --- a/test/scripts/checks/verify_notice.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:verifyNotice diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh old mode 100755 new mode 100644 diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index f449986713f97..3e49edc8e6ae5 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,9 +2,19 @@ source src/dev/ci_setup/setup_env.sh -if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - ./test/scripts/jenkins_build_plugins.sh -fi +echo " -> building examples separate from test plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --no-examples \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ + --verbose; # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -16,7 +26,4 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup; if [[ -z "$CODE_COVERAGE" ]] ; then echo " -> building and extracting OSS Kibana distributable for use in functional tests" node scripts/build --debug --oss - - mkdir -p "$WORKSPACE/kibana-build-oss" - cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/ fi diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh deleted file mode 100755 index 32b3942074b34..0000000000000 --- a/test/scripts/jenkins_build_plugins.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -echo " -> building examples separate from test plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --examples \ - --workers 6 \ - --verbose - -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --oss \ - --no-examples \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \ - --workers 6 \ - --verbose diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 2542d7032e83b..60d7f0406f4c9 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; - if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then + if [ "$CI_GROUP" == "1" ]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:exampleFunctionalTestsRelease --from=source; diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh deleted file mode 100755 index 1d691d98982de..0000000000000 --- a/test/scripts/jenkins_plugin_functional.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup_oss.sh - -cd test/plugin_functional/plugins/kbn_sample_panel_action; -if [[ ! -d "target" ]]; then - yarn build; -fi -cd -; - -pwd - -yarn run grunt run:pluginFunctionalTestsRelease --from=source; -yarn run grunt run:exampleFunctionalTestsRelease --from=source; -yarn run grunt run:interpreterFunctionalTestsRelease; diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh old mode 100755 new mode 100644 index a5a1a2103801f..204911a3eedaa --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -1,6 +1,12 @@ #!/usr/bin/env bash -source test/scripts/jenkins_test_setup_xpack.sh +source test/scripts/jenkins_test_setup.sh + +installDir="$PARENT_DIR/install/kibana" +destDir="${installDir}-${CI_WORKER_NUMBER}" +cp -R "$installDir" "$destDir" + +export KIBANA_INSTALL_DIR="$destDir" echo " -> Running security solution cypress tests" cd "$XPACK_DIR" diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh deleted file mode 100755 index 5274d05572e71..0000000000000 --- a/test/scripts/jenkins_setup_parallel_workspace.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -e - -CURRENT_DIR=$(pwd) - -# Copy everything except node_modules into the current workspace -rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules -rsync -a ${WORKSPACE}/kibana/.??* . - -# Symlink all non-root, non-fixture node_modules into our new workspace -cd ${WORKSPACE}/kibana -find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" -find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%" -cd "${CURRENT_DIR}" - -# Symlink all of the individual root-level node_modules into the node_modules/ directory -mkdir -p node_modules -ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/ -ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/ - -# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked -unlink node_modules/@kbn -unlink node_modules/css-loader -unlink node_modules/style-loader - -# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink -unlink node_modules/val-loader - -cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/ -cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/ -cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/ -cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/ diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh old mode 100755 new mode 100644 index 7cced76eb650f..49ee8a6b526ca --- a/test/scripts/jenkins_test_setup.sh +++ b/test/scripts/jenkins_test_setup.sh @@ -14,7 +14,3 @@ trap 'post_work' EXIT export TEST_BROWSER_HEADLESS=1 source src/dev/ci_setup/setup_env.sh - -if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then - cp -R $WORKSPACE/kibana/.es ./ -fi diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh old mode 100755 new mode 100644 index b7eac33f35176..7bbb867526384 --- a/test/scripts/jenkins_test_setup_oss.sh +++ b/test/scripts/jenkins_test_setup_oss.sh @@ -2,17 +2,10 @@ source test/scripts/jenkins_test_setup.sh -if [[ -z "$CODE_COVERAGE" ]]; then - - destDir="build/kibana-build-oss" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi - - if [[ ! -d $destDir ]]; then - mkdir -p $destDir - cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/ - fi +if [[ -z "$CODE_COVERAGE" ]] ; then + installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)" + destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER} + cp -R "$installDir" "$destDir" export KIBANA_INSTALL_DIR="$destDir" fi diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh old mode 100755 new mode 100644 index 74a3de77e3a76..a72e9749ebbd5 --- a/test/scripts/jenkins_test_setup_xpack.sh +++ b/test/scripts/jenkins_test_setup_xpack.sh @@ -3,18 +3,11 @@ source test/scripts/jenkins_test_setup.sh if [[ -z "$CODE_COVERAGE" ]]; then + installDir="$PARENT_DIR/install/kibana" + destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}" + cp -R "$installDir" "$destDir" - destDir="build/kibana-build-xpack" - if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}" - fi - - if [[ ! -d $destDir ]]; then - mkdir -p $destDir - cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/ - fi - - export KIBANA_INSTALL_DIR="$(realpath $destDir)" + export KIBANA_INSTALL_DIR="$destDir" cd "$XPACK_DIR" fi diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 2452e2f5b8c58..58ef6a42d3fe4 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,9 +3,21 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh -if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then - ./test/scripts/jenkins_xpack_build_plugins.sh -fi +echo " -> building examples separate from test plugins" +node scripts/build_kibana_platform_plugins \ + --examples \ + --verbose; + +echo " -> building test plugins" +node scripts/build_kibana_platform_plugins \ + --no-examples \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --verbose; # doesn't persist, also set in kibanaPipeline.groovy export KBN_NP_PLUGINS_BUILT=true @@ -30,10 +42,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then cd "$KIBANA_DIR" node scripts/build --debug --no-oss linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" - installDir="$KIBANA_DIR/install/kibana" + installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - - mkdir -p "$WORKSPACE/kibana-build-xpack" - cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ fi diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh deleted file mode 100755 index fea30c547bd5f..0000000000000 --- a/test/scripts/jenkins_xpack_build_plugins.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -echo " -> building examples separate from test plugins" -node scripts/build_kibana_platform_plugins \ - --workers 12 \ - --examples \ - --verbose - -echo " -> building kibana platform plugins" -node scripts/build_kibana_platform_plugins \ - --no-examples \ - --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ - --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ - --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ - --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ - --workers 12 \ - --verbose diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh old mode 100755 new mode 100644 diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh deleted file mode 100755 index c3211300b96c5..0000000000000 --- a/test/scripts/lint/eslint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:eslint diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh deleted file mode 100755 index b9c683bcb049e..0000000000000 --- a/test/scripts/lint/sasslint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:sasslint diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh deleted file mode 100755 index 152c97a3ca7df..0000000000000 --- a/test/scripts/test/api_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:apiIntegrationTests diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh deleted file mode 100755 index 73dbbddfb38f6..0000000000000 --- a/test/scripts/test/jest_integration.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_jest_integration diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh deleted file mode 100755 index e25452698cebc..0000000000000 --- a/test/scripts/test/jest_unit.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_jest diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh deleted file mode 100755 index e9985300ba19d..0000000000000 --- a/test/scripts/test/karma_ci.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_karma_ci diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh deleted file mode 100755 index 43c00f0a09dcf..0000000000000 --- a/test/scripts/test/mocha.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:mocha diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh deleted file mode 100755 index 93d70ec355391..0000000000000 --- a/test/scripts/test/xpack_jest_unit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10 diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh deleted file mode 100755 index 9078f01f1b870..0000000000000 --- a/test/scripts/test/xpack_karma.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh deleted file mode 100755 index 493fe9f58d322..0000000000000 --- a/test/scripts/test/xpack_list_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh deleted file mode 100755 index b21301f25ad08..0000000000000 --- a/test/scripts/test/xpack_siem_cyclic_dependency.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -cd x-pack -checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy index 2a1b55d832606..460a90b8ec0c0 100644 --- a/vars/catchErrors.groovy +++ b/vars/catchErrors.groovy @@ -1,15 +1,8 @@ // Basically, this is a shortcut for catchError(catchInterruptions: false) {} // By default, catchError will swallow aborts/timeouts, which we almost never want -// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps def call(Map params = [:], Closure closure) { - try { - closure() - } catch (ex) { - params.catchInterruptions = false - catchError(params) { - throw ex - } - } + params.catchInterruptions = false + return catchError(params, closure) } return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 0f11204311451..f3fc5f84583c9 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -16,34 +16,27 @@ def withPostBuildReporting(Closure closure) { } } -def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { - // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue - def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER - - def kibanaPort = "61${parallelId}1" - def esPort = "61${parallelId}2" - def esTransportPort = "61${parallelId}3" - def ingestManagementPackageRegistryPort = "61${parallelId}4" - - withEnv([ - "CI_GROUP=${parallelId}", - "REMOVE_KIBANA_INSTALL_DIR=1", - "CI_PARALLEL_PROCESS_NUMBER=${parallelId}", - "TEST_KIBANA_HOST=localhost", - "TEST_KIBANA_PORT=${kibanaPort}", - "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", - "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", - "TEST_ES_TRANSPORT_PORT=${esTransportPort}", - "KBN_NP_PLUGINS_BUILT=true", - "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", - ] + additionalEnvs) { - closure() - } -} - def functionalTestProcess(String name, Closure closure) { - return { - withFunctionalTestEnv(["JOB=${name}"], closure) + return { processNumber -> + def kibanaPort = "61${processNumber}1" + def esPort = "61${processNumber}2" + def esTransportPort = "61${processNumber}3" + def ingestManagementPackageRegistryPort = "61${processNumber}4" + + withEnv([ + "CI_PARALLEL_PROCESS_NUMBER=${processNumber}", + "TEST_KIBANA_HOST=localhost", + "TEST_KIBANA_PORT=${kibanaPort}", + "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}", + "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", + "TEST_ES_TRANSPORT_PORT=${esTransportPort}", + "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}", + "IS_PIPELINE_JOB=1", + "JOB=${name}", + "KBN_NP_PLUGINS_BUILT=true", + ]) { + closure() + } } } @@ -107,17 +100,11 @@ def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', - 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', 'target/junit/**/*', - 'target/test-suites-ci-plan.json', - 'test/**/screenshots/session/*.png', - 'test/**/screenshots/failure/*.png', - 'test/**/screenshots/diff/*.png', + 'test/**/screenshots/**/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/session/*.png', - 'x-pack/test/**/screenshots/failure/*.png', - 'x-pack/test/**/screenshots/diff/*.png', + 'x-pack/test/**/screenshots/**/*.png', 'x-pack/test/functional/failure_debug/html/*.html', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] @@ -132,12 +119,6 @@ def withGcsArtifactUpload(workerName, closure) { ARTIFACT_PATTERNS.each { pattern -> uploadGcsArtifact(uploadPrefix, pattern) } - - dir(env.WORKSPACE) { - ARTIFACT_PATTERNS.each { pattern -> - uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}") - } - } } } }) @@ -150,11 +131,6 @@ def withGcsArtifactUpload(workerName, closure) { def publishJunit() { junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) - - // junit() is weird about paths for security reasons, so we need to actually change to an upper directory first - dir(env.WORKSPACE) { - junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true) - } } def sendMail() { @@ -218,16 +194,12 @@ def doSetup() { } } -def buildOss(maxWorkers = '') { - withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") - } +def buildOss() { + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } -def buildXpack(maxWorkers = '') { - withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) { - runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") - } +def buildXpack() { + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } def runErrorReporter() { @@ -276,100 +248,6 @@ def call(Map params = [:], Closure closure) { } } -// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace -// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh -def withCiTaskQueue(Map options = [:], Closure closure) { - def setupClosure = { - // This can't use runbld, because it expects the source to be there, which isn't yet - bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process") - } - - def config = [parallel: 24, setup: setupClosure] + options - - withTaskQueue(config) { - closure.call() - } -} - -def scriptTask(description, script) { - return { - withFunctionalTestEnv { - runbld(script, description) - } - } -} - -def scriptTaskDocker(description, script) { - return { - withDocker(scriptTask(description, script)) - } -} - -def buildDocker() { - sh( - script: """ - cp /usr/local/bin/runbld .ci/ - cp /usr/local/bin/bash_standard_lib.sh .ci/ - cd .ci - docker build -t kibana-ci -f ./Dockerfile . - """, - label: 'Build CI Docker image' - ) -} - -def withDocker(Closure closure) { - docker - .image('kibana-ci') - .inside( - "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4", - closure - ) -} - -def buildOssPlugins() { - runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins') -} - -def buildXpackPlugins() { - runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') -} - -def withTasks(Map params = [worker: [:]], Closure closure) { - catchErrors { - def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) - - workers.ci(config) { - withCiTaskQueue(parallel: 24) { - parallel([ - docker: { - retry(2) { - buildDocker() - } - }, - - // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces - ossPlugins: { buildOssPlugins() }, - xpackPlugins: { buildXpackPlugins() }, - ]) - - catchErrors { - closure() - } - } - } - } -} - -def allCiTasks() { - withTasks { - tasks.check() - tasks.lint() - tasks.test() - tasks.functionalOss() - tasks.functionalXpack() - } -} - def pipelineLibraryTests() { whenChanged(['vars/', '.ci/pipeline-library/']) { workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) { @@ -380,4 +258,5 @@ def pipelineLibraryTests() { } } + return this diff --git a/vars/task.groovy b/vars/task.groovy deleted file mode 100644 index 0c07b519b6fef..0000000000000 --- a/vars/task.groovy +++ /dev/null @@ -1,5 +0,0 @@ -def call(Closure closure) { - withTaskQueue.addTask(closure) -} - -return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy deleted file mode 100644 index 9de4c78322d3e..0000000000000 --- a/vars/tasks.groovy +++ /dev/null @@ -1,118 +0,0 @@ -def call(List closures) { - withTaskQueue.addTasks(closures) -} - -def check() { - tasks([ - kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'), - kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'), - kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'), - kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'), - kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'), - kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'), - kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'), - kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'), - kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'), - kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'), - kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'), - ]) -} - -def lint() { - tasks([ - kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'), - kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'), - ]) -} - -def test() { - tasks([ - // These 4 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here - kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'), - kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'), - kibanaPipeline.scriptTaskDocker('Karma CI Tests', 'test/scripts/test/karma_ci.sh'), - kibanaPipeline.scriptTaskDocker('X-Pack Karma Tests', 'test/scripts/test/xpack_karma.sh'), - - kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), - kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), - kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), - kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), - kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), - ]) -} - -def functionalOss(Map params = [:]) { - def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false] - - task { - kibanaPipeline.buildOss(6) - - if (config.ciGroups) { - def ciGroups = 1..12 - tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) }) - } - - if (config.firefox) { - task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh')) - } - - if (config.accessibility) { - task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh')) - } - - if (config.pluginFunctional) { - task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh')) - } - - if (config.visualRegression) { - task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')) - } - } -} - -def functionalXpack(Map params = [:]) { - def config = params ?: [ - ciGroups: true, - firefox: true, - accessibility: true, - pluginFunctional: true, - savedObjectsFieldMetrics: true, - pageLoadMetrics: false, - visualRegression: false, - ] - - task { - kibanaPipeline.buildXpack(10) - - if (config.ciGroups) { - def ciGroups = 1..10 - tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) }) - } - - if (config.firefox) { - task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh')) - } - - if (config.accessibility) { - task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh')) - } - - if (config.visualRegression) { - task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')) - } - - if (config.pageLoadMetrics) { - task(kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh')) - } - - if (config.savedObjectsFieldMetrics) { - task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh')) - } - - whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) { - task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) - } - } -} - -return this diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy deleted file mode 100644 index 8132d6264744f..0000000000000 --- a/vars/withTaskQueue.groovy +++ /dev/null @@ -1,154 +0,0 @@ -import groovy.transform.Field - -public static @Field TASK_QUEUES = [:] -public static @Field TASK_QUEUES_COUNTER = 0 - -/** - withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency. - This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once. - - Each "process" will execute in a separate, unique, empty directory. - If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue - - Using the queue currently requires an agent/worker. - - Usage: - - withTaskQueue(parallel: 10) { - task { print "This is a task" } - - // This is the same as calling task() multiple times - tasks([ { print "Another task" }, { print "And another task" } ]) - - // Tasks can queue up subsequent tasks - task { - buildThing() - task { print "I depend on buildThing()" } - } - } - - You can also define a setup task that each process should execute one time before executing tasks: - withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) { - ... - } - -*/ -def call(Map options = [:], Closure closure) { - def config = [ parallel: 10 ] + options - def counter = ++TASK_QUEUES_COUNTER - - // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block - // This way, we could have multiple task queue instances in the same pipeline - withEnv(["TASK_QUEUE_ID=${counter}"]) { - withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [ - tasks: [], - tmpFile: sh(script: 'mktemp', returnStdout: true).trim() - ] - - closure.call() - - def processesExecuting = 0 - def processes = [:] - def iterationId = 0 - - for(def i = 1; i <= config.parallel; i++) { - def j = i - processes["task-queue-process-${j}"] = { - catchErrors { - withEnv([ - "TASK_QUEUE_PROCESS_ID=${j}", - "TASK_QUEUE_ITERATION_ID=${++iterationId}" - ]) { - dir("${WORKSPACE}/parallel/${j}/kibana") { - if (config.setup) { - config.setup.call(j) - } - - def isDone = false - while(!isDone) { // TODO some kind of timeout? - catchErrors { - if (!getTasks().isEmpty()) { - processesExecuting++ - catchErrors { - def task - try { - task = getTasks().pop() - } catch (java.util.NoSuchElementException ex) { - return - } - - task.call() - } - processesExecuting-- - // If a task finishes, and no new tasks were queued up, and nothing else is executing - // Then all of the processes should wake up and exit - if (processesExecuting < 1 && getTasks().isEmpty()) { - taskNotify() - } - return - } - - if (processesExecuting > 0) { - taskSleep() - return - } - - // Queue is empty, no processes are executing - isDone = true - } - } - } - } - } - } - } - parallel(processes) - } -} - -// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps -// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue -// There's a 20 minute timeout just in case something goes wrong, -// in which case this method will get called again if the process is actually supposed to be waiting. -def taskSleep() { - sh(script: """#!/bin/bash - TIMESTAMP=\$(date '+%s' -d "0 seconds ago") - for (( i=1; i<=240; i++ )) - do - if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ] - then - break - else - sleep 5 - if [[ \$i == 240 ]]; then - echo "Waited for new tasks for 20 minutes, exiting in case something went wrong" - fi - fi - done - """, label: "Waiting for new tasks...") -} - -// Used to let the task queue processes know that either a new task has been queued up, or work is complete -def taskNotify() { - sh "touch '${getTmpFile()}'" -} - -def getTasks() { - return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks -} - -def getTmpFile() { - return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile -} - -def addTask(Closure closure) { - getTasks() << closure - taskNotify() -} - -def addTasks(List closures) { - closures.reverse().each { - getTasks() << it - } - taskNotify() -} diff --git a/vars/workers.groovy b/vars/workers.groovy index 2e94ce12f34c0..8b7e8525a7ce3 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -13,8 +13,6 @@ def label(size) { return 'docker && tests-l' case 'xl': return 'docker && tests-xl' - case 'xl-highmem': - return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl' } @@ -57,11 +55,6 @@ def base(Map params, Closure closure) { } } - sh( - script: "mkdir -p ${env.WORKSPACE}/tmp", - label: "Create custom temp directory" - ) - def checkoutInfo = [:] if (config.scm) { @@ -96,7 +89,6 @@ def base(Map params, Closure closure) { "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", "TEST_BROWSER_HEADLESS=1", "GIT_BRANCH=${checkoutInfo.branch}", - "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it ]) { withCredentials([ string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'), @@ -175,9 +167,7 @@ def parallelProcesses(Map params) { sleep(delay) } - withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) { - processClosure() - } + processClosure(processNumber) } } diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json index 2c16491c1096b..4da44667e167f 100644 --- a/x-pack/.telemetryrc.json +++ b/x-pack/.telemetryrc.json @@ -7,6 +7,7 @@ "plugins/apm/server/lib/apm_telemetry/index.ts", "plugins/canvas/server/collectors/collector.ts", "plugins/infra/server/usage/usage_collector.ts", + "plugins/ingest_manager/server/collectors/register.ts", "plugins/lens/server/usage/collectors.ts", "plugins/reporting/server/usage/reporting_usage_collector.ts", "plugins/maps/server/maps_telemetry/collectors/register.ts" diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index e17a8046b5c6a..6c5b539fcecfa 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -119,7 +119,7 @@ export function PageLoadDistChart({ xScaleType={ScaleType.Linear} yScaleType={ScaleType.Linear} data={data?.pageLoadDistribution ?? []} - curve={CurveType.CURVE_NATURAL} + curve={CurveType.CURVE_CATMULL_ROM} /> {breakdowns.map(({ name, type }) => ( ): LineAnnotationDatum[] { return Object.entries(values ?? {}).map((value) => ({ - dataValue: Math.round(value[1] / 1000), + dataValue: value[1], details: `${(+value[0]).toFixed(0)}`, })); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 7d48cee49b104..81503e16f7bcf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -68,7 +68,7 @@ export const PageLoadDistribution = () => { ); const onPercentileChange = (min: number, max: number) => { - setPercentileRange({ min: min * 1000, max: max * 1000 }); + setPercentileRange({ min, max }); }; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 3ddaa66b8de5e..3380a81c7bfab 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -46,7 +46,7 @@ export function RumOverview() { (callApmApi) => { if (start && end) { return callApmApi({ - pathname: '/api/apm/services', + pathname: '/api/apm/rum-client/services', params: { query: { start, @@ -68,11 +68,7 @@ export function RumOverview() { {!isRumServiceRoute && ( <> - service.serviceName) ?? [] - } - /> + {' '} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f31ad83666a17..6e3a29d9f3dbc 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; -import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; import { ConfigSchema } from '.'; import { ObservabilityPluginSetup } from '../../observability/public'; import { @@ -83,7 +82,7 @@ export class ApmPlugin implements Plugin { plugins.observability.dashboard.register({ appName: 'apm', fetchData: async (params) => { - return fetchLandingPageData(params, { theme }); + return fetchLandingPageData(params); }, hasData, }); diff --git a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts b/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts deleted file mode 100644 index 299e8a2104282..0000000000000 --- a/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const anomalyData = { - dates: [ - 1530614880000, - 1530614940000, - 1530615000000, - 1530615060000, - 1530615120000, - 1530615180000, - 1530615240000, - 1530615300000, - 1530615360000, - 1530615420000, - 1530615480000, - 1530615540000, - 1530615600000, - 1530615660000, - 1530615720000, - 1530615780000, - 1530615840000, - 1530615900000, - 1530615960000, - 1530616020000, - 1530616080000, - 1530616140000, - 1530616200000, - 1530616260000, - 1530616320000, - 1530616380000, - 1530616440000, - 1530616500000, - 1530616560000, - 1530616620000, - ], - buckets: [ - { - anomalyScore: null, - lower: 15669, - upper: 54799, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17808, - upper: 49874, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 18012, - upper: 49421, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17889, - upper: 49654, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17713, - upper: 50026, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 18044, - upper: 49371, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17713, - upper: 50110, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: 0, - lower: 17582, - upper: 50419, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - { - anomalyScore: null, - lower: null, - upper: null, - }, - ], -}; diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts index a14d827eeaec5..fd407a8bf72ad 100644 --- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -6,7 +6,6 @@ import { fetchLandingPageData, hasData } from './observability_dashboard'; import * as createCallApmApi from './createCallApmApi'; -import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme'; describe('Observability dashboard data', () => { const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); @@ -38,39 +37,31 @@ describe('Observability dashboard data', () => { ], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 10, }, transactions: { type: 'number', - label: 'Transactions', value: 2, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [ { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 }, ], - color: '#6092c0', }, }, }); @@ -82,35 +73,27 @@ describe('Observability dashboard data', () => { transactionCoordinates: [], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 0, }, transactions: { type: 'number', - label: 'Transactions', value: 0, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [], - color: '#6092c0', }, }, }); @@ -122,35 +105,27 @@ describe('Observability dashboard data', () => { transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], }) ); - const response = await fetchLandingPageData( - { - startTime: '1', - endTime: '2', - bucketSize: '3', - }, - { theme } - ); + const response = await fetchLandingPageData({ + startTime: '1', + endTime: '2', + bucketSize: '3', + }); expect(response).toEqual({ title: 'APM', appLink: '/app/apm', stats: { services: { type: 'number', - label: 'Services', value: 0, }, transactions: { type: 'number', - label: 'Transactions', value: 0, - color: '#6092c0', }, }, series: { transactions: { - label: 'Transactions', coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], - color: '#6092c0', }, }, }); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 79ccf8dbd6f9b..409cec8b9ce10 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -6,21 +6,17 @@ import { i18n } from '@kbn/i18n'; import { mean } from 'lodash'; -import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, FetchDataParams, } from '../../../../observability/public'; import { callApmApi } from './createCallApmApi'; -interface Options { - theme: Theme; -} - -export const fetchLandingPageData = async ( - { startTime, endTime, bucketSize }: FetchDataParams, - { theme }: Options -): Promise => { +export const fetchLandingPageData = async ({ + startTime, + endTime, + bucketSize, +}: FetchDataParams): Promise => { const data = await callApmApi({ pathname: '/api/apm/observability_dashboard', params: { query: { start: startTime, end: endTime, bucketSize } }, @@ -36,34 +32,20 @@ export const fetchLandingPageData = async ( stats: { services: { type: 'number', - label: i18n.translate( - 'xpack.apm.observabilityDashboard.stats.services', - { defaultMessage: 'Services' } - ), value: serviceCount, }, transactions: { type: 'number', - label: i18n.translate( - 'xpack.apm.observabilityDashboard.stats.transactions', - { defaultMessage: 'Transactions' } - ), value: mean( transactionCoordinates .map(({ y }) => y) .filter((y) => y && isFinite(y)) ) || 0, - color: theme.euiColorVis1, }, }, series: { transactions: { - label: i18n.translate( - 'xpack.apm.observabilityDashboard.chart.transactions', - { defaultMessage: 'Transactions' } - ), - color: theme.euiColorVis1, coordinates: transactionCoordinates, }, }, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts index c648cf4cc116a..e3161b49b315d 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts @@ -65,4 +65,40 @@ describe('data telemetry collection tasks', () => { }); }); }); + + describe('integrations', () => { + const integrationsTask = tasks.find((task) => task.name === 'integrations'); + + it('returns the count of ML jobs', async () => { + const transportRequest = jest + .fn() + .mockResolvedValueOnce({ body: { count: 1 } }); + + expect( + await integrationsTask?.executor({ indices, transportRequest } as any) + ).toEqual({ + integrations: { + ml: { + all_jobs_count: 1, + }, + }, + }); + }); + + describe('with no data', () => { + it('returns a count of 0', async () => { + const transportRequest = jest.fn().mockResolvedValueOnce({}); + + expect( + await integrationsTask?.executor({ indices, transportRequest } as any) + ).toEqual({ + integrations: { + ml: { + all_jobs_count: 0, + }, + }, + }); + }); + }); + }); }); diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index f27af9a2cc516..4bbaaf3e86e78 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -4,31 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ import { flatten, merge, sortBy, sum } from 'lodash'; -import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { TelemetryTask } from '.'; import { AGENT_NAMES } from '../../../../common/agent_name'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { - PROCESSOR_EVENT, - SERVICE_NAME, AGENT_NAME, AGENT_VERSION, + CLOUD_AVAILABILITY_ZONE, + CLOUD_PROVIDER, + CLOUD_REGION, ERROR_GROUP_ID, - TRANSACTION_NAME, PARENT_ID, + PROCESSOR_EVENT, SERVICE_FRAMEWORK_NAME, SERVICE_FRAMEWORK_VERSION, SERVICE_LANGUAGE_NAME, SERVICE_LANGUAGE_VERSION, + SERVICE_NAME, SERVICE_RUNTIME_NAME, SERVICE_RUNTIME_VERSION, + TRANSACTION_NAME, USER_AGENT_ORIGINAL, - CLOUD_AVAILABILITY_ZONE, - CLOUD_PROVIDER, - CLOUD_REGION, } from '../../../../common/elasticsearch_fieldnames'; -import { Span } from '../../../../typings/es_schemas/ui/span'; import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; -import { TelemetryTask } from '.'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { Span } from '../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { APMTelemetry } from '../types'; const TIME_RANGES = ['1d', 'all'] as const; @@ -465,17 +465,17 @@ export const tasks: TelemetryTask[] = [ { name: 'integrations', executor: async ({ transportRequest }) => { - const apmJobs = ['*-high_mean_response_time']; + const apmJobs = ['apm-*', '*-high_mean_response_time']; const response = (await transportRequest({ method: 'get', path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`, - })) as { data?: { count: number } }; + })) as { body?: { count: number } }; return { integrations: { ml: { - all_jobs_count: response.data?.count ?? 0, + all_jobs_count: response.body?.count ?? 0, }, }, }; diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index c006d01637483..602eb88ba8940 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -70,6 +70,9 @@ Object { "durPercentiles": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 3, + }, "percents": Array [ 50, 75, @@ -179,3 +182,55 @@ Object { "index": "myIndex", } `; + +exports[`rum client dashboard queries fetches rum services 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 1000, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "term": Object { + "processor.event": "transaction", + }, + }, + Object { + "term": Object { + "transaction.type": "page-load", + }, + }, + Object { + "exists": Object { + "field": "transaction.marks.navigationTiming.fetchStart", + }, + }, + Object { + "term": Object { + "my.custom.ui.filter": "foo-bar", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 43af18999547d..e847a87264759 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -12,6 +12,12 @@ import { SetupUIFilters, } from '../helpers/setup_request'; +export const MICRO_TO_SEC = 1000000; + +export function microToSec(val: number) { + return Math.round((val / MICRO_TO_SEC + Number.EPSILON) * 100) / 100; +} + export async function getPageLoadDistribution({ setup, minPercentile, @@ -42,6 +48,9 @@ export async function getPageLoadDistribution({ percentiles: { field: 'transaction.duration.us', percents: [50, 75, 90, 95, 99], + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -59,20 +68,29 @@ export async function getPageLoadDistribution({ return null; } - const minDuration = aggregations?.minDuration.value ?? 0; + const { durPercentiles, minDuration } = aggregations ?? {}; - const minPerc = minPercentile ? +minPercentile : minDuration; + const minPerc = minPercentile + ? +minPercentile * MICRO_TO_SEC + : minDuration?.value ?? 0; - const maxPercQuery = aggregations?.durPercentiles.values['99.0'] ?? 10000; + const maxPercQuery = durPercentiles?.values['99.0'] ?? 10000; - const maxPerc = maxPercentile ? +maxPercentile : maxPercQuery; + const maxPerc = maxPercentile ? +maxPercentile * MICRO_TO_SEC : maxPercQuery; const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc); + + Object.entries(durPercentiles?.values ?? {}).forEach(([key, val]) => { + if (durPercentiles?.values?.[key]) { + durPercentiles.values[key] = microToSec(val as number); + } + }); + return { pageLoadDistribution: pageDist, - percentiles: aggregations?.durPercentiles.values, - minDuration: minPerc, - maxDuration: maxPerc, + percentiles: durPercentiles?.values, + minDuration: microToSec(minPerc), + maxDuration: microToSec(maxPerc), }; } @@ -81,9 +99,9 @@ const getPercentilesDistribution = async ( minDuration: number, maxDuration: number ) => { - const stepValue = (maxDuration - minDuration) / 50; + const stepValue = (maxDuration - minDuration) / 100; const stepValues = []; - for (let i = 1; i < 51; i++) { + for (let i = 1; i < 101; i++) { stepValues.push((stepValue * i + minDuration).toFixed(2)); } @@ -103,6 +121,9 @@ const getPercentilesDistribution = async ( field: 'transaction.duration.us', values: stepValues, keyed: false, + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -117,7 +138,7 @@ const getPercentilesDistribution = async ( return pageDist.map(({ key, value }, index: number, arr) => { return { - x: Math.round(key / 1000), + x: microToSec(key), y: index === 0 ? value : value - arr[index - 1].value, }; }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 5ae6bd1540f7c..ea9d701e64c3d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -17,6 +17,7 @@ import { USER_AGENT_NAME, USER_AGENT_OS, } from '../../../common/elasticsearch_fieldnames'; +import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; export const getBreakdownField = (breakdown: string) => { switch (breakdown) { @@ -38,7 +39,9 @@ export const getPageLoadDistBreakdown = async ( maxDuration: number, breakdown: string ) => { - const stepValue = (maxDuration - minDuration) / 50; + // convert secs to micros + const stepValue = + (maxDuration * MICRO_TO_SEC - minDuration * MICRO_TO_SEC) / 50; const stepValues = []; for (let i = 1; i < 51; i++) { @@ -67,6 +70,9 @@ export const getPageLoadDistBreakdown = async ( field: 'transaction.duration.us', values: stepValues, keyed: false, + hdr: { + number_of_significant_value_digits: 3, + }, }, }, }, @@ -86,7 +92,7 @@ export const getPageLoadDistBreakdown = async ( name: String(key), data: pageDist.values?.map(({ key: pKey, value }, index: number, arr) => { return { - x: Math.round(pKey / 1000), + x: microToSec(pKey), y: index === 0 ? value : value - arr[index - 1].value, }; }), diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts new file mode 100644 index 0000000000000..5957a25239307 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getRumOverviewProjection } from '../../../common/projections/rum_overview'; +import { mergeProjection } from '../../../common/projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getRumServices({ + setup, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const projection = getRumOverviewProjection({ + setup, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + query: { + bool: projection.body.query.bool, + }, + aggs: { + services: { + terms: { + field: 'service.name', + size: 1000, + }, + }, + }, + }, + }); + + const { client } = setup; + + const response = await client.search(params); + + const result = response.aggregations?.services.buckets ?? []; + + return result.map(({ key }) => key as string); +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts index 5f5a48eced746..37432672c5d89 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts @@ -11,6 +11,7 @@ import { import { getClientMetrics } from './get_client_metrics'; import { getPageViewTrends } from './get_page_view_trends'; import { getPageLoadDistribution } from './get_page_load_distribution'; +import { getRumServices } from './get_rum_services'; describe('rum client dashboard queries', () => { let mock: SearchParamsMock; @@ -49,4 +50,13 @@ describe('rum client dashboard queries', () => { ); expect(mock.params).toMatchSnapshot(); }); + + it('fetches rum services', async () => { + mock = await inspectSearchParams((setup) => + getRumServices({ + setup, + }) + ); + expect(mock.params).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index c314debcd8049..513c44904683e 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -76,6 +76,7 @@ import { rumPageViewsTrendRoute, rumPageLoadDistributionRoute, rumPageLoadDistBreakdownRoute, + rumServicesRoute, } from './rum_client'; import { observabilityDashboardHasDataRoute, @@ -172,6 +173,7 @@ const createApmApi = () => { .add(rumPageLoadDistributionRoute) .add(rumPageLoadDistBreakdownRoute) .add(rumClientMetricsRoute) + .add(rumServicesRoute) // Observability dashboard .add(observabilityDashboardHasDataRoute) diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 75651f646a50d..01e549632a0bc 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -12,6 +12,7 @@ import { rangeRt, uiFiltersRt } from './default_api_types'; import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; +import { getRumServices } from '../lib/rum_client/get_rum_services'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -91,3 +92,15 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ return getPageViewTrends({ setup, breakdowns }); }, })); + +export const rumServicesRoute = createRoute(() => ({ + path: '/api/apm/rum-client/services', + params: { + query: t.intersection([uiFiltersRt, rangeRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + return getRumServices({ setup }); + }, +})); diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index a340aa24aebfb..ac7499c23e926 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -150,6 +150,7 @@ export interface AggregationOptionsByType { field: string; values: string[]; keyed?: boolean; + hdr?: { number_of_significant_value_digits: number }; }; } diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/.storybook/storyshots.test.js index e3217ad4dbe58..b9fe0914b3698 100644 --- a/x-pack/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/.storybook/storyshots.test.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import fs from 'fs'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; @@ -77,12 +76,6 @@ import { RenderedElement } from '../shareable_runtime/components/rendered_elemen jest.mock('../shareable_runtime/components/rendered_element'); RenderedElement.mockImplementation(() => 'RenderedElement'); -// Some of the code requires that this directory exists, but the tests don't actually require any css to be present -const cssDir = path.resolve(__dirname, '../../../../built_assets/css'); -if (!fs.existsSync(cssDir)) { - fs.mkdirSync(cssDir, { recursive: true }); -} - addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 577f04a4a7efd..a0381557db21e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -8,35 +8,35 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, EuiButtonEmpty, EuiDescriptionList, - EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiIconTip, + EuiLink, + EuiTitle, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../shared_imports'; import { SectionLoading, SectionError, Error } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; interface Props { dataStreamName: string; + backingIndicesLink: ReturnType; onClose: (shouldReload?: boolean) => void; } -/** - * NOTE: This currently isn't in use by data_stream_list.tsx because it doesn't contain any - * information that doesn't already exist in the table. We'll use it once we add additional - * info, e.g. storage size, docs count. - */ export const DataStreamDetailPanel: React.FunctionComponent = ({ dataStreamName, + backingIndicesLink, onClose, }) => { const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName); @@ -68,28 +68,95 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ /> ); } else if (dataStream) { - const { timeStampField, generation } = dataStream; + const { indices, timeStampField, generation } = dataStream; content = ( - - - - + + + + + + + + + + + + } + position="top" + /> + + + - {timeStampField.name} + + {indices.length} + - - - - - {generation} - + + + + + + + + + } + position="top" + /> + + + + + {timeStampField.name} + + + + + + + + + + + + + + } + position="top" + /> + + + + + {generation} + + + ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index adfaa7820aff3..239b119051c06 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -8,14 +8,15 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate, extractQueryParams } from '../../../../shared_imports'; import { useAppContext } from '../../../app_context'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; -import { decodePathFromReactRouter } from '../../../services/routing'; +import { encodePathForReactRouter, decodePathFromReactRouter } from '../../../services/routing'; +import { documentationService } from '../../../services/documentation'; import { Section } from '../../home'; import { DataStreamTable } from './data_stream_table'; import { DataStreamDetailPanel } from './data_stream_detail_panel'; @@ -79,7 +80,7 @@ export const DataStreamList: React.FunctionComponent {' ' /* We need this space to separate these two sentences. */} {ingestManager ? ( @@ -134,14 +135,25 @@ export const DataStreamList: React.FunctionComponent {/* TODO: Add a switch for toggling on data streams created by Ingest Manager */} - - - - - + + + {i18n.translate('xpack.idxMgmt.dataStreamListDescription.learnMoreLinkText', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + @@ -170,6 +182,12 @@ export const DataStreamList: React.FunctionComponent { history.push(`/${Section.DataStreams}`); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index 5f10eebc9d270..9122c6524d05d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -33,7 +33,6 @@ import { EuiTableRowCell, EuiTableRowCellCheckbox, EuiText, - EuiTitle, } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; @@ -41,6 +40,7 @@ import { reactRouterNavigate } from '../../../../../shared_imports'; import { REFRESH_RATE_INDEX_LIST } from '../../../../constants'; import { healthToColor } from '../../../../services'; import { encodePathForReactRouter } from '../../../../services/routing'; +import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; import { renderBadges } from '../../../../lib/render_badges'; import { NoMatch, PageErrorForbidden } from '../../../../components'; @@ -121,6 +121,11 @@ export class IndexTable extends Component { } componentWillUnmount() { + // When you deep-link to an index from the data streams tab, the hidden indices are toggled on. + // However, this state is lost when you navigate away. We need to clear the filter too, or else + // navigating back to this tab would just show an empty list because the backing indices + // would be hidden. + this.props.filterChanged(''); clearInterval(this.interval); } @@ -494,14 +499,28 @@ export class IndexTable extends Component { - - - - - + + + {i18n.translate( + 'xpack.idxMgmt.indexTableDescription.learnMoreLinkText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index afa8fa5b4ee04..18a65407ee20d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -17,12 +17,14 @@ import { EuiFlexItem, EuiFlexGroup, EuiButton, + EuiLink, } from '@elastic/eui'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants'; import { TemplateListItem } from '../../../../../common'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadIndexTemplates } from '../../../services/api'; +import { documentationService } from '../../../services/documentation'; import { useServices } from '../../../app_context'; import { getTemplateEditLink, @@ -109,14 +111,28 @@ export const TemplateList: React.FunctionComponent ( - - - - - + + + {i18n.translate( + 'xpack.idxMgmt.home.indexTemplatesDescription.learnMoreLinkText', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + filters={filters} onChange={setFilters} /> diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index ccccccce19766..972b4f4b25680 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -36,6 +36,10 @@ class DocumentationService { return `${this.esDocsBase}/mapping-routing-field.html`; } + public getDataStreamsDocumentationLink() { + return `${this.esDocsBase}/data-streams.html`; + } + public getTemplatesDocumentationLink() { return `${this.esDocsBase}/indices-templates.html`; } diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap index 99ab129fc36e3..4680414493a2c 100644 --- a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap +++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap @@ -91,7 +91,6 @@ Object { "y": 3.5, }, ], - "label": "Inbound traffic", }, "outboundTraffic": Object { "coordinates": Array [ @@ -180,32 +179,26 @@ Object { "y": 4, }, ], - "label": "Outbound traffic", }, }, "stats": Object { "cpu": Object { - "label": "CPU usage", "type": "percent", "value": 0.0015, }, "hosts": Object { - "label": "Hosts", "type": "number", "value": 2, }, "inboundTraffic": Object { - "label": "Inbound traffic", "type": "bytesPerSecond", "value": 3.5, }, "memory": Object { - "label": "Memory usage", "type": "percent", "value": 0.0015, }, "outboundTraffic": Object { - "label": "Outbound traffic", "type": "bytesPerSecond", "value": 3, }, diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index 15751fab39abc..25b334d03c4f7 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -103,14 +103,6 @@ export const createMetricsFetchData = ( body: JSON.stringify(snapshotRequest), }); - const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', { - defaultMessage: 'Inbound traffic', - }); - - const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', { - defaultMessage: 'Outbound traffic', - }); - return { title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', { defaultMessage: 'Metrics', @@ -119,43 +111,30 @@ export const createMetricsFetchData = ( stats: { hosts: { type: 'number', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', { - defaultMessage: 'Hosts', - }), value: results.nodes.length, }, cpu: { type: 'percent', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', { - defaultMessage: 'CPU usage', - }), value: combineNodesBy('cpu', results.nodes, average), }, memory: { type: 'percent', - label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', { - defaultMessage: 'Memory usage', - }), value: combineNodesBy('memory', results.nodes, average), }, inboundTraffic: { type: 'bytesPerSecond', - label: inboundLabel, value: combineNodesBy('rx', results.nodes, average), }, outboundTraffic: { type: 'bytesPerSecond', - label: outboundLabel, value: combineNodesBy('tx', results.nodes, average), }, }, series: { inboundTraffic: { - label: inboundLabel, coordinates: combineNodeTimeseriesBy('rx', results.nodes, average), }, outboundTraffic: { - label: outboundLabel, coordinates: combineNodeTimeseriesBy('tx', results.nodes, average), }, }, diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index c374cbb3bb146..4b10dab5d1ae5 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -4146,9 +4146,6 @@ "config_revision": { "type": ["number", "null"] }, - "config_newest_revision": { - "type": "number" - }, "last_checkin": { "type": "string" }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 27f0c61685fd4..1f4718acc2c1f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -81,7 +81,6 @@ interface AgentBase { default_api_key_id?: string; config_id?: string; config_revision?: number | null; - config_newest_revision?: number; last_checkin?: string; user_provided_metadata: AgentMetadata; local_metadata: AgentMetadata; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 23e31227cbf3c..a34038d4fba04 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -42,6 +42,8 @@ export enum AgentAssetType { input = 'input', } +export type RegistryRelease = 'ga' | 'beta' | 'experimental'; + // from /package/{name} // type Package struct at https://github.com/elastic/package-registry/blob/master/util/package.go // https://github.com/elastic/package-registry/blob/master/docs/api/package.json @@ -49,6 +51,7 @@ export interface RegistryPackage { name: string; title?: string; version: string; + release?: RegistryRelease; readme?: string; description: string; type: string; @@ -114,6 +117,7 @@ export type RegistrySearchResult = Pick< | 'name' | 'title' | 'version' + | 'release' | 'description' | 'type' | 'icons' diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index c5035d2d44432..1901b8c0c7039 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -12,13 +12,21 @@ import { PackageInfo, } from '../models/epm'; +export interface GetCategoriesRequest { + query: { + experimental?: boolean; + }; +} + export interface GetCategoriesResponse { response: CategorySummaryList; success: boolean; } + export interface GetPackagesRequest { query: { category?: string; + experimental?: boolean; }; } diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index 35447139607a6..181b93a9e2425 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,6 +5,6 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features", "cloud"], + "optionalPlugins": ["security", "features", "cloud", "usageCollection"], "extraPublicDirs": ["common"] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts index 011e0c69f2683..e5a7191372e9c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; import { ICON_TYPES } from '@elastic/eui'; -import { PackageInfo, PackageListItem } from '../../../../common/types/models'; +import { PackageInfo, PackageListItem } from '../types'; import { useLinks } from '../sections/epm/hooks'; import { sendGetPackageInfoByKey } from './index'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts index 64bee1763b08b..40a22f6b44d50 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpFetchQuery } from 'src/core/public'; import { useRequest, sendRequest } from './use_request'; import { epmRouteService } from '../../services'; import { + GetCategoriesRequest, GetCategoriesResponse, + GetPackagesRequest, GetPackagesResponse, GetLimitedPackagesResponse, GetInfoResponse, @@ -16,18 +17,19 @@ import { DeletePackageResponse, } from '../../types'; -export const useGetCategories = () => { +export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => { return useRequest({ path: epmRouteService.getCategoriesPath(), method: 'get', + query: { experimental: true, ...query }, }); }; -export const useGetPackages = (query: HttpFetchQuery = {}) => { +export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => { return useRequest({ path: epmRouteService.getListPath(), method: 'get', - query, + query: { experimental: true, ...query }, }); }; @@ -52,6 +54,13 @@ export const sendGetPackageInfoByKey = (pkgkey: string) => { }); }; +export const useGetFileByPath = (filePath: string) => { + return useRequest({ + path: epmRouteService.getFilePath(filePath), + method: 'get', + }); +}; + export const sendGetFileByPath = (filePath: string) => { return sendRequest({ path: epmRouteService.getFilePath(filePath), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx index ac74b09ab4391..24b4baeaa092b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx @@ -30,19 +30,24 @@ import { ServiceTitleMap, } from '../constants'; -export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) { - const FirstHeaderRow = styled(EuiFlexGroup)` - padding: 0 0 ${(props) => props.theme.eui.paddingSizes.m} 0; - `; +const FirstHeaderRow = styled(EuiFlexGroup)` + padding: 0 0 ${(props) => props.theme.eui.paddingSizes.m} 0; +`; + +const HeaderRow = styled(EuiFlexGroup)` + padding: ${(props) => props.theme.eui.paddingSizes.m} 0; +`; - const HeaderRow = styled(EuiFlexGroup)` - padding: ${(props) => props.theme.eui.paddingSizes.m} 0; - `; +const FacetGroup = styled(EuiFacetGroup)` + flex-grow: 0; +`; - const FacetGroup = styled(EuiFacetGroup)` - flex-grow: 0; - `; +const FacetButton = styled(EuiFacetButton)` + padding: '${(props) => props.theme.eui.paddingSizes.xs} 0'; + height: 'unset'; +`; +export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) { return ( {entries(assets).map(([service, typeToParts], index) => { @@ -77,10 +82,6 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT // only kibana assets have icons const iconType = type in AssetIcons && AssetIcons[type]; const iconNode = iconType ? : ''; - const FacetButton = styled(EuiFacetButton)` - padding: '${(props) => props.theme.eui.paddingSizes.xs} 0'; - height: 'unset'; - `; return ( + parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.spacerSizes.xl) * 2}px; + height: 1px; +`; + +const Panel = styled(EuiPanel)` + padding: ${(props) => props.theme.eui.spacerSizes.xl}; + margin-bottom: -100%; + svg, + img { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + } + .euiFlexItem { + height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px; + justify-content: center; + } +`; + +export function IconPanel({ + packageName, + version, + icons, +}: Pick) { + const iconType = usePackageIconType({ packageName, version, icons }); -export function IconPanel({ iconType }: { iconType: IconType }) { - const Panel = styled(EuiPanel)` - /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ - &&& { - position: absolute; - text-align: center; - vertical-align: middle; - padding: ${(props) => props.theme.eui.spacerSizes.xl}; - svg, - img { - height: ${(props) => props.theme.eui.euiKeyPadMenuSize}; - width: ${(props) => props.theme.eui.euiKeyPadMenuSize}; - } - } - `; + return ( + + + + + + ); +} +export function LoadingIconPanel() { return ( - - - + + + + + ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx index acdcd5b9a3406..3f0803af6daae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -3,13 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { EuiIconTip, EuiIconProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -export const StyledAlert = styled(EuiIcon)` - color: ${(props) => props.theme.eui.euiColorWarning}; - padding: 0 5px; -`; - -export const UpdateIcon = () => ; +export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => ( + +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx deleted file mode 100644 index 3fcf9758368de..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { EuiButtonEmpty } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -export function NavButtonBack({ href, text }: { href: string; text: string }) { - const ButtonEmpty = styled(EuiButtonEmpty)` - margin-right: ${(props) => props.theme.eui.spacerSizes.xl}; - `; - return ( - - {text} - - ); -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx index e3d8cdc8f4985..cf98f9dc90230 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -9,12 +9,9 @@ import { EuiCard } from '@elastic/eui'; import { PackageInfo, PackageListItem } from '../../../types'; import { useLink } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; +import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from './release_badge'; -export interface BadgeProps { - showInstalledBadge?: boolean; -} - -type PackageCardProps = (PackageListItem | PackageInfo) & BadgeProps; +type PackageCardProps = PackageListItem | PackageInfo; // adding the `href` causes EuiCard to use a `a` instead of a `button` // `a` tags use `euiLinkColor` which results in blueish Badge text @@ -27,7 +24,7 @@ export function PackageCard({ name, title, version, - showInstalledBadge, + release, status, icons, ...restProps @@ -41,12 +38,14 @@ export function PackageCard({ return ( } href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} + betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined} + betaBadgeTooltipContent={ + release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined + } /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx index dbf454acd2b74..0c1199f7c8867 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx @@ -20,22 +20,16 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Loading } from '../../../components'; import { PackageList } from '../../../types'; import { useLocalSearch, searchIdField } from '../hooks'; -import { BadgeProps, PackageCard } from './package_card'; +import { PackageCard } from './package_card'; -type ListProps = { +interface ListProps { isLoading?: boolean; controls?: ReactNode; title: string; list: PackageList; -} & BadgeProps; +} -export function PackageListGrid({ - isLoading, - controls, - title, - list, - showInstalledBadge, -}: ListProps) { +export function PackageListGrid({ isLoading, controls, title, list }: ListProps) { const initialQuery = EuiSearchBar.Query.MATCH_ALL; const [query, setQuery] = useState(initialQuery); @@ -71,7 +65,7 @@ export function PackageListGrid({ .includes(item[searchIdField]) ) : list; - gridContent = ; + gridContent = ; } return ( @@ -108,16 +102,16 @@ function ControlsColumn({ controls, title }: ControlsColumnProps) { - {controls} + {controls} ); } -type GridColumnProps = { +interface GridColumnProps { list: PackageList; -} & BadgeProps; +} function GridColumn({ list }: GridColumnProps) { return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts new file mode 100644 index 0000000000000..f3520b4e7a9b3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { RegistryRelease } from '../../../types'; + +export const RELEASE_BADGE_LABEL: { [key in Exclude]: string } = { + beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaLabel', { + defaultMessage: 'Beta', + }), + experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalLabel', { + defaultMessage: 'Experimental', + }), +}; + +export const RELEASE_BADGE_DESCRIPTION: { [key in Exclude]: string } = { + beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaDescription', { + defaultMessage: 'This integration is not recommended for use in production environments.', + }), + experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalDescription', { + defaultMessage: 'This integration may have breaking changes or be removed in a future release.', + }), +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index c9a8cabdf414b..f53b4e9150ca1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -16,22 +16,22 @@ import { SideNavLinks } from './side_nav_links'; import { PackageConfigsPanel } from './package_configs_panel'; import { SettingsPanel } from './settings_panel'; -type ContentProps = PackageInfo & Pick & { hasIconPanel: boolean }; -export function Content(props: ContentProps) { - const { hasIconPanel, name, panel, version } = props; - const SideNavColumn = hasIconPanel - ? styled(LeftColumn)` - /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ - &&& { - margin-top: 77px; - } - ` - : LeftColumn; +type ContentProps = PackageInfo & Pick; + +const SideNavColumn = styled(LeftColumn)` + /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */ + &&& { + margin-top: 77px; + } +`; + +// fixes IE11 problem with nested flex items +const ContentFlexGroup = styled(EuiFlexGroup)` + flex: 0 0 auto !important; +`; - // fixes IE11 problem with nested flex items - const ContentFlexGroup = styled(EuiFlexGroup)` - flex: 0 0 auto !important; - `; +export function Content(props: ContentProps) { + const { name, panel, version } = props; return ( @@ -75,13 +75,13 @@ function RightColumnContent(props: RightColumnContentProps) { const { assets, panel } = props; switch (panel) { case 'overview': - return ( + return assets ? ( - ); + ) : null; default: return ; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx deleted file mode 100644 index 875a8f5c5c127..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui'; -import { PackageInfo } from '../../../../types'; -import { useCapabilities, useLink } from '../../../../hooks'; -import { IconPanel } from '../../components/icon_panel'; -import { NavButtonBack } from '../../components/nav_button_back'; -import { CenterColumn, LeftColumn, RightColumn } from './layout'; -import { UpdateIcon } from '../../components/icons'; - -const FullWidthNavRow = styled(EuiPage)` - /* no left padding so link is against column left edge */ - padding-left: 0; -`; - -const Text = styled.span` - margin-right: ${(props) => props.theme.eui.euiSizeM}; -`; - -type HeaderProps = PackageInfo & { iconType?: IconType }; - -export function Header(props: HeaderProps) { - const { iconType, name, title, version, latestVersion } = props; - - let installedVersion; - if ('savedObject' in props) { - installedVersion = props.savedObject.attributes.version; - } - const hasWriteCapabilites = useCapabilities().write; - const { getHref } = useLink(); - const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; - return ( - - - - - - {iconType ? ( - - - - ) : null} - - -

- {title} - - - {version} {updateAvailable && } - - -

-
-
- - - - - - - - - -
-
- ); -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 505687068cf42..3267fbbe3733c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -3,15 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui'; -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiText, + EuiSpacer, + EuiBetaBadge, + EuiButton, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; import { DetailViewPanelName, InstallStatus, PackageInfo } from '../../../../types'; -import { sendGetPackageInfoByKey, usePackageIconType, useBreadcrumbs } from '../../../../hooks'; +import { Loading, Error } from '../../../../components'; +import { + useGetPackageInfoByKey, + useBreadcrumbs, + useLink, + useCapabilities, +} from '../../../../hooks'; +import { WithHeaderLayout } from '../../../../layouts'; import { useSetPackageInstallStatus } from '../../hooks'; +import { IconPanel, LoadingIconPanel } from '../../components/icon_panel'; +import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge'; +import { UpdateIcon } from '../../components/icons'; import { Content } from './content'; -import { Header } from './header'; export const DEFAULT_PANEL: DetailViewPanelName = 'overview'; @@ -20,66 +42,202 @@ export interface DetailParams { panel?: DetailViewPanelName; } +const Divider = styled.div` + width: 0; + height: 100%; + border-left: ${(props) => props.theme.eui.euiBorderThin}; +`; + +// Allows child text to be truncated +const FlexItemWithMinWidth = styled(EuiFlexItem)` + min-width: 0px; +`; + +function Breadcrumbs({ packageTitle }: { packageTitle: string }) { + useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); + return null; +} + export function Detail() { // TODO: fix forced cast if possible const { pkgkey, panel = DEFAULT_PANEL } = useParams() as DetailParams; + const { getHref } = useLink(); + const hasWriteCapabilites = useCapabilities().write; - const [info, setInfo] = useState(null); + // Package info state + const [packageInfo, setPackageInfo] = useState(null); const setPackageInstallStatus = useSetPackageInstallStatus(); + const updateAvailable = + packageInfo && + 'savedObject' in packageInfo && + packageInfo.savedObject && + packageInfo.savedObject.attributes.version < packageInfo.latestVersion; + + // Fetch package info + const { data: packageInfoData, error: packageInfoError, isLoading } = useGetPackageInfoByKey( + pkgkey + ); + + // Track install status state useEffect(() => { - sendGetPackageInfoByKey(pkgkey).then((response) => { - const packageInfo = response.data?.response; - const title = packageInfo?.title; - const name = packageInfo?.name; + if (packageInfoData?.response) { + const packageInfoResponse = packageInfoData.response; + setPackageInfo(packageInfoResponse); + let installedVersion; - if (packageInfo && 'savedObject' in packageInfo) { - installedVersion = packageInfo.savedObject.attributes.version; + const { name } = packageInfoData.response; + if ('savedObject' in packageInfoResponse) { + installedVersion = packageInfoResponse.savedObject.attributes.version; } - const status: InstallStatus = packageInfo?.status as any; - - // track install status state + const status: InstallStatus = packageInfoResponse?.status as any; if (name) { setPackageInstallStatus({ name, status, version: installedVersion || null }); } - if (packageInfo) { - setInfo({ ...packageInfo, title: title || '' }); - } - }); - }, [pkgkey, setPackageInstallStatus]); - - if (!info) return null; - - return ; -} + } + }, [packageInfoData, setPackageInstallStatus, setPackageInfo]); -const FullWidthHeader = styled(EuiPage)` - border-bottom: ${(props) => props.theme.eui.euiBorderThin}; - padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl}; -`; + const headerLeftContent = useMemo( + () => ( + + + {/* Allows button to break out of full width */} +
+ + + +
+
+ + + + {isLoading || !packageInfo ? ( + + ) : ( + + )} + + + + + + {/* Render space in place of package name while package info loads to prevent layout from jumping around */} +

{packageInfo?.title || '\u00A0'}

+
+
+ {packageInfo?.release && packageInfo.release !== 'ga' ? ( + + + + ) : null} +
+
+
+
+
+ ), + [getHref, isLoading, packageInfo] + ); -const FullWidthContent = styled(EuiPage)` - background-color: ${(props) => props.theme.eui.euiColorEmptyShade}; - padding-top: ${(props) => parseInt(props.theme.eui.paddingSizes.xl, 10) * 1.25}px; - flex-grow: 1; -`; + const headerRightContent = useMemo( + () => + packageInfo ? ( + <> + + + {[ + { + label: i18n.translate('xpack.ingestManager.epm.versionLabel', { + defaultMessage: 'Version', + }), + content: ( + + {packageInfo.version} + {updateAvailable ? ( + + + + ) : null} + + ), + }, + { isDivider: true }, + { + content: ( + + + + ), + }, + ].map((item, index) => ( + + {item.isDivider ?? false ? ( + + ) : item.label ? ( + + {item.label} + {item.content} + + ) : ( + item.content + )} + + ))} + + + ) : undefined, + [getHref, hasWriteCapabilites, packageInfo, pkgkey, updateAvailable] + ); -type LayoutProps = PackageInfo & Pick & Pick; -export function DetailLayout(props: LayoutProps) { - const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props; - const iconType = usePackageIconType({ packageName, version, icons }); - useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); return ( - - - -
- - - - - - - - + + {packageInfo ? : null} + {packageInfoError ? ( + + } + error={packageInfoError} + /> + ) : isLoading || !packageInfo ? ( + + ) : ( + + )} + ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx index a802e35add7db..c329596384730 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx @@ -22,7 +22,7 @@ export const LeftColumn: FunctionComponent = ({ children, ...rest } export const CenterColumn: FunctionComponent = ({ children, ...rest }) => { return ( - + {children} ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx index 696af14604c5b..d8388a71556d6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import React, { Fragment } from 'react'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { ScreenshotItem } from '../../../../types'; import { useLinks } from '../../hooks'; @@ -13,6 +14,29 @@ interface ScreenshotProps { images: ScreenshotItem[]; } +const getHorizontalPadding = (styledProps: any): number => + parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2; +const getVerticalPadding = (styledProps: any): number => + parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75; +const getPadding = (styledProps: any) => + styledProps.hascaption + ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding( + styledProps + )}px ${getVerticalPadding(styledProps)}px` + : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`; +const ScreenshotsContainer = styled(EuiFlexGroup)` + background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%), + ${(styledProps) => styledProps.theme.eui.euiColorPrimary}; + padding: ${(styledProps) => getPadding(styledProps)}; + flex: 0 0 auto; + border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius}; +`; + +// fixes ie11 problems with nested flex items +const NestedEuiFlexItem = styled(EuiFlexItem)` + flex: 0 0 auto !important; +`; + export function Screenshots(props: ScreenshotProps) { const { toImage } = useLinks(); const { images } = props; @@ -21,36 +45,23 @@ export function Screenshots(props: ScreenshotProps) { const image = images[0]; const hasCaption = image.title ? true : false; - const getHorizontalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2; - const getVerticalPadding = (styledProps: any): number => - parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75; - const getPadding = (styledProps: any) => - hasCaption - ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding( - styledProps - )}px ${getVerticalPadding(styledProps)}px` - : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`; - - const ScreenshotsContainer = styled(EuiFlexGroup)` - background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%), - ${(styledProps) => styledProps.theme.eui.euiColorPrimary}; - padding: ${(styledProps) => getPadding(styledProps)}; - flex: 0 0 auto; - border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius}; - `; - - // fixes ie11 problems with nested flex items - const NestedEuiFlexItem = styled(EuiFlexItem)` - flex: 0 0 auto !important; - `; return ( -

Screenshots

+

+ +

- + {hasCaption && ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 125289ce3ee8d..4832a89479026 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -33,7 +33,7 @@ const NoteLabel = () => ( ); const UpdatesAvailableMsg = () => ( - + {entries(PanelDisplayNames).map(([panel, display]) => { - const Link = styled(EuiButtonEmpty).attrs({ - href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }), - })` - font-weight: ${(p) => - active === panel - ? p.theme.eui.euiFontWeightSemiBold - : p.theme.eui.euiFontWeightRegular}; - `; // Don't display usages tab as we haven't implemented this yet // FIXME: Restore when we implement usages page if (panel === 'usages' && (true || packageInstallStatus.status !== InstallStatus.installed)) @@ -50,7 +41,11 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { return (
- {display} + + {active === panel ? {display} : display} +
); })} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx index c378e5a47a9b9..363b1ede89e9e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx @@ -39,22 +39,26 @@ export const HeroCopy = memo(() => { ); }); +const Illustration = styled(EuiImage)` + margin-bottom: -68px; + width: 80%; +`; + export const HeroImage = memo(() => { const { toAssets } = useLinks(); const { uiSettings } = useCore(); const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - const Illustration = styled(EuiImage).attrs((props) => ({ - alt: i18n.translate('xpack.ingestManager.epm.illustrationAltText', { - defaultMessage: 'Illustration of an integration', - }), - url: IS_DARK_THEME - ? toAssets('illustration_integrations_darkmode.svg') - : toAssets('illustration_integrations_lightmode.svg'), - }))` - margin-bottom: -68px; - width: 80%; - `; - - return ; + return ( + + ); }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index c68833c1b2d95..a8e4d0105066b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -61,7 +61,9 @@ export function EPMHomePage() { function InstalledPackages() { useBreadcrumbs('integrations_installed'); - const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); + const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ + experimental: true, + }); const [selectedCategory, setSelectedCategory] = useState(''); const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', { @@ -118,7 +120,8 @@ function AvailablePackages() { const queryParams = new URLSearchParams(useLocation().search); const initialCategory = queryParams.get('category') || ''; const [selectedCategory, setSelectedCategory] = useState(initialCategory); - const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ + const { data: allPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages(); + const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ category: selectedCategory, }); const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories(); @@ -126,7 +129,7 @@ function AvailablePackages() { categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : []; const title = i18n.translate('xpack.ingestManager.epmList.allTitle', { - defaultMessage: 'All integrations', + defaultMessage: 'Browse by category', }); const categories = [ @@ -135,13 +138,13 @@ function AvailablePackages() { title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', { defaultMessage: 'All', }), - count: packages.length, + count: allPackagesRes?.response?.length || 0, }, ...(categoriesRes ? categoriesRes.response : []), ]; const controls = categories ? ( { @@ -156,7 +159,7 @@ function AvailablePackages() { return ( ; - allPackages: PackageList; -} - -export function SearchPackages({ searchTerm, localSearchRef, allPackages }: SearchPackagesProps) { - // this means the search index hasn't been built yet. - // i.e. the intial fetch of all packages hasn't finished - if (!localSearchRef.current) return
Still fetching matches. Try again in a moment.
; - - const matches = localSearchRef.current.search(searchTerm) as PackageList; - const matchingIds = matches.map((match) => match[searchIdField]); - const filtered = allPackages.filter((item) => matchingIds.includes(item[searchIdField])); - - return ; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx deleted file mode 100644 index fbdcaac01931b..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiText, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import { PackageList } from '../../../../types'; -import { PackageListGrid } from '../../components/package_list_grid'; - -interface SearchResultsProps { - term: string; - results: PackageList; -} - -export function SearchResults({ term, results }: SearchResultsProps) { - const title = 'Search results'; - return ( - - - {results.length} results for "{term}" - - - } - /> - ); -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index ec58789becb72..30204603e764c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { EuiBasicTable, EuiButton, @@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { CSSProperties } from 'styled-components'; import { AgentEnrollmentFlyout } from '../components'; -import { Agent } from '../../../types'; +import { Agent, AgentConfig } from '../../../types'; import { usePagination, useCapabilities, @@ -220,6 +220,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }); const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + const agentConfigsIndexedById = useMemo(() => { + return agentConfigs.reduce((acc, config) => { + acc[config.id] = config; + + return acc; + }, {} as { [k: string]: AgentConfig }); + }, [agentConfigs]); const { isLoading: isAgentConfigsLoading } = agentConfigsRequest; const columns = [ @@ -271,9 +278,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
)} - {agent.config_revision && - agent.config_newest_revision && - agent.config_newest_revision > agent.config_revision && ( + {agent.config_id && + agent.config_revision && + agentConfigsIndexedById[agent.config_id] && + agentConfigsIndexedById[agent.config_id].revision > agent.config_revision && ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 9cd8a75642296..170a9cedc08d9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -91,7 +91,9 @@ export { RequirementVersion, ScreenshotItem, ServiceName, + GetCategoriesRequest, GetCategoriesResponse, + GetPackagesRequest, GetPackagesResponse, GetLimitedPackagesResponse, GetInfoResponse, @@ -101,6 +103,7 @@ export { InstallStatus, InstallationStatus, Installable, + RegistryRelease, } from '../../../../common'; export * from './intra_app_route_state'; diff --git a/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts new file mode 100644 index 0000000000000..920b336297171 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/agent_collectors.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClient } from 'kibana/server'; +import * as AgentService from '../services/agents'; +export interface AgentUsage { + total: number; + online: number; + error: number; + offline: number; +} + +export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise => { + // TODO: unsure if this case is possible at all. + if (!soClient) { + return { + total: 0, + online: 0, + error: 0, + offline: 0, + }; + } + const { total, online, error, offline } = await AgentService.getAgentStatusForConfig(soClient); + return { + total, + online, + error, + offline, + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts new file mode 100644 index 0000000000000..514984f7f859d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/config_collectors.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestManagerConfigType } from '..'; + +export const getIsFleetEnabled = (config: IngestManagerConfigType) => { + return config.fleet.enabled; +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/helpers.ts b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts new file mode 100644 index 0000000000000..c8ed54d5074fd --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/helpers.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../../src/core/server'; + +export async function getInternalSavedObjectsClient(core: CoreSetup) { + return core.getStartServices().then(async ([coreStart]) => { + const savedObjectsRepo = coreStart.savedObjects.createInternalRepository(); + return new SavedObjectsClient(savedObjectsRepo); + }); +} diff --git a/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts new file mode 100644 index 0000000000000..399e38f1919ba --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/package_collectors.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClient } from 'kibana/server'; +import _ from 'lodash'; +import { getPackageSavedObjects } from '../services/epm/packages/get'; +import { agentConfigService } from '../services'; +import { NewPackageConfig } from '../types'; + +export interface PackageUsage { + name: string; + version: string; + enabled: boolean; +} + +export const getPackageUsage = async (soClient?: SavedObjectsClient): Promise => { + if (!soClient) { + return []; + } + const packagesSavedObjects = await getPackageSavedObjects(soClient); + const agentConfigs = await agentConfigService.list(soClient, { + perPage: 1000, // avoiding pagination + withPackageConfigs: true, + }); + + // Once we provide detailed telemetry on agent configs, this logic should probably be moved + // to the (then to be created) agent config collector, so we only query and loop over these + // objects once. + + const packagesInConfigs = agentConfigs.items.map((agentConfig) => { + const packageConfigs: NewPackageConfig[] = agentConfig.package_configs as NewPackageConfig[]; + return packageConfigs + .map((packageConfig) => packageConfig.package?.name) + .filter((packageName): packageName is string => packageName !== undefined); + }); + + const enabledPackages = _.uniq(_.flatten(packagesInConfigs)); + + return packagesSavedObjects.saved_objects.map((p) => { + return { + name: p.attributes.name, + version: p.attributes.version, + enabled: enabledPackages.includes(p.attributes.name), + }; + }); +}; diff --git a/x-pack/plugins/ingest_manager/server/collectors/register.ts b/x-pack/plugins/ingest_manager/server/collectors/register.ts new file mode 100644 index 0000000000000..aad59ee74433c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/collectors/register.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CoreSetup } from 'kibana/server'; +import { getIsFleetEnabled } from './config_collectors'; +import { AgentUsage, getAgentUsage } from './agent_collectors'; +import { getInternalSavedObjectsClient } from './helpers'; +import { PackageUsage, getPackageUsage } from './package_collectors'; +import { IngestManagerConfigType } from '..'; + +interface Usage { + fleet_enabled: boolean; + agents: AgentUsage; + packages: PackageUsage[]; +} + +export function registerIngestManagerUsageCollector( + core: CoreSetup, + config: IngestManagerConfigType, + usageCollection: UsageCollectionSetup | undefined +): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + // if for any reason the saved objects client is not available, also return + if (!usageCollection) { + return; + } + + // create usage collector + const ingestManagerCollector = usageCollection.makeUsageCollector({ + type: 'ingest_manager', + isReady: () => true, + fetch: async () => { + const soClient = await getInternalSavedObjectsClient(core); + return { + fleet_enabled: getIsFleetEnabled(config), + agents: await getAgentUsage(soClient), + packages: await getPackageUsage(soClient), + }; + }, + // schema: { // temporarily disabled because of type errors + // fleet_enabled: { type: 'boolean' }, + // agents: { + // total: { type: 'number' }, + // online: { type: 'number' }, + // error: { type: 'number' }, + // offline: { type: 'number' }, + // }, + // packages: { + // name: { type: 'keyword' }, + // version: { type: 'keyword' }, + // enabled: { type: boolean }, + // }, + // }, + }); + + // register usage collector + usageCollection.registerCollector(ingestManagerCollector); +} diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 91201dbf9848b..d1adbd8b2f65d 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -14,6 +14,7 @@ import { SavedObjectsServiceStart, HttpServiceSetup, } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { EncryptedSavedObjectsPluginStart, @@ -62,6 +63,7 @@ import { } from './services/agents'; import { CloudSetup } from '../../cloud/server'; import { agentCheckinState } from './services/agents/checkin/state'; +import { registerIngestManagerUsageCollector } from './collectors/register'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; @@ -69,6 +71,7 @@ export interface IngestManagerSetupDeps { features?: FeaturesPluginSetup; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; cloud?: CloudSetup; + usageCollection?: UsageCollectionSetup; } export type IngestManagerStartDeps = object; @@ -198,6 +201,9 @@ export class IngestManagerPlugin const router = core.http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); + // Register usage collection + registerIngestManagerUsageCollector(core, config, deps.usageCollection); + // Always register app routes for permissions checking registerAppRoutes(router); diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index a50b3b13faeab..fe813f29b72e6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -14,6 +14,7 @@ import { GetLimitedPackagesResponse, } from '../../../common'; import { + GetCategoriesRequestSchema, GetPackagesRequestSchema, GetFileRequestSchema, GetInfoRequestSchema, @@ -30,9 +31,12 @@ import { getLimitedPackages, } from '../../services/epm/packages'; -export const getCategoriesHandler: RequestHandler = async (context, request, response) => { +export const getCategoriesHandler: RequestHandler< + undefined, + TypeOf +> = async (context, request, response) => { try { - const res = await getCategories(); + const res = await getCategories(request.query); const body: GetCategoriesResponse = { response: res, success: true, @@ -54,7 +58,7 @@ export const getListHandler: RequestHandler< const savedObjectsClient = context.core.savedObjects.client; const res = await getPackages({ savedObjectsClient, - category: request.query.category, + ...request.query, }); const body: GetPackagesResponse = { response: res, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index ffaf0ce46c89a..b524a7b33923e 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -15,6 +15,7 @@ import { deletePackageHandler, } from './handlers'; import { + GetCategoriesRequestSchema, GetPackagesRequestSchema, GetFileRequestSchema, GetInfoRequestSchema, @@ -26,7 +27,7 @@ export const registerRoutes = (router: IRouter) => { router.get( { path: EPM_API_ROUTES.CATEGORIES_PATTERN, - validate: false, + validate: GetCategoriesRequestSchema, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getCategoriesHandler diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index b47cf4f7e7c3b..a5b5cc4337908 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -64,7 +64,6 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { last_updated: { type: 'date' }, last_checkin: { type: 'date' }, config_revision: { type: 'integer' }, - config_newest_revision: { type: 'integer' }, default_api_key_id: { type: 'keyword' }, default_api_key: { type: 'binary', index: false }, updated_at: { type: 'date' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts index 1cca165906732..3d40d128afda8 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForConfigId } from './api_keys'; -import { updateAgentsForConfigId, unenrollForConfigId } from './agents'; +import { unenrollForConfigId } from './agents'; import { outputService } from './output'; export async function agentConfigUpdateEventHandler( @@ -26,10 +26,6 @@ export async function agentConfigUpdateEventHandler( }); } - if (action === 'updated') { - await updateAgentsForConfigId(soClient, configId); - } - if (action === 'deleted') { await unenrollForConfigId(soClient, configId); await deleteEnrollmentApiKeyForConfigId(soClient, configId); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts index f8142af376eb3..ecc2c987d04b6 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -23,6 +23,5 @@ export async function reassignAgent( await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { config_id: newConfigId, config_revision: null, - config_newest_revision: config.revision, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index ec7a42ff11b7a..11ad76fe81784 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -8,38 +8,6 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { unenrollAgent } from './unenroll'; -import { agentConfigService } from '../agent_config'; - -export async function updateAgentsForConfigId( - soClient: SavedObjectsClientContract, - configId: string -) { - const config = await agentConfigService.get(soClient, configId); - if (!config) { - throw new Error('Config not found'); - } - let hasMore = true; - let page = 1; - while (hasMore) { - const { agents } = await listAgents(soClient, { - kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`, - page: page++, - perPage: 1000, - showInactive: true, - }); - if (agents.length === 0) { - hasMore = false; - break; - } - const agentUpdate = agents.map((agent) => ({ - id: agent.id, - type: AGENT_SAVED_OBJECT_TYPE, - attributes: { config_newest_revision: config.revision }, - })); - - await soClient.bulkUpdate(agentUpdate); - } -} export async function unenrollForConfigId(soClient: SavedObjectsClientContract, configId: string) { let hasMore = true; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 848e65b7931eb..7437321163749 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -99,7 +99,8 @@ exports[`tests loading base.yml: base.yml 1`] = ` "package": { "name": "nginx" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; @@ -203,7 +204,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "package": { "name": "coredns" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; @@ -1691,7 +1693,8 @@ exports[`tests loading system.yml: system.yml 1`] = ` "package": { "name": "system" }, - "managed_by": "ingest-manager" + "managed_by": "ingest-manager", + "managed": true } } `; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index e7867532ed176..77ad96952269f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -317,6 +317,7 @@ function getBaseTemplate( name: packageName, }, managed_by: 'ingest-manager', + managed: true, }, }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index ad9635cc02e06..78aa513d1a1dc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -17,8 +17,8 @@ function nameAsTitle(name: string) { return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase(); } -export async function getCategories() { - return Registry.fetchCategories(); +export async function getCategories(options: Registry.CategoriesParams) { + return Registry.fetchCategories(options); } export async function getPackages( @@ -26,8 +26,8 @@ export async function getPackages( savedObjectsClient: SavedObjectsClientContract; } & Registry.SearchParams ) { - const { savedObjectsClient } = options; - const registryItems = await Registry.fetchList({ category: options.category }).then((items) => { + const { savedObjectsClient, experimental, category } = options; + const registryItems = await Registry.fetchList({ category, experimental }).then((items) => { return items.map((item) => Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }) ); @@ -56,7 +56,7 @@ export async function getLimitedPackages(options: { savedObjectsClient: SavedObjectsClientContract; }): Promise { const { savedObjectsClient } = options; - const allPackages = await getPackages({ savedObjectsClient }); + const allPackages = await getPackages({ savedObjectsClient, experimental: true }); const installedPackages = allPackages.filter( (pkg) => (pkg.status = InstallationStatus.installed) ); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 0393cabca8ba2..ea906517f6dec 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -26,6 +26,11 @@ export { ArchiveEntry } from './extract'; export interface SearchParams { category?: CategoryId; + experimental?: boolean; +} + +export interface CategoriesParams { + experimental?: boolean; } export const pkgToPkgKey = ({ name, version }: { name: string; version: string }) => @@ -34,19 +39,23 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string } export async function fetchList(params?: SearchParams): Promise { const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search`); - if (params && params.category) { - url.searchParams.set('category', params.category); + if (params) { + if (params.category) { + url.searchParams.set('category', params.category); + } + if (params.experimental) { + url.searchParams.set('experimental', params.experimental.toString()); + } } return fetchUrl(url.toString()).then(JSON.parse); } -export async function fetchFindLatestPackage( - packageName: string, - internal: boolean = true -): Promise { +export async function fetchFindLatestPackage(packageName: string): Promise { const registryUrl = getRegistryUrl(); - const url = new URL(`${registryUrl}/search?package=${packageName}&internal=${internal}`); + const url = new URL( + `${registryUrl}/search?package=${packageName}&internal=true&experimental=true` + ); const res = await fetchUrl(url.toString()); const searchResults = JSON.parse(res); if (searchResults.length) { @@ -66,9 +75,16 @@ export async function fetchFile(filePath: string): Promise { return getResponse(`${registryUrl}${filePath}`); } -export async function fetchCategories(): Promise { +export async function fetchCategories(params?: CategoriesParams): Promise { const registryUrl = getRegistryUrl(); - return fetchUrl(`${registryUrl}/categories`).then(JSON.parse); + const url = new URL(`${registryUrl}/categories`); + if (params) { + if (params.experimental) { + url.searchParams.set('experimental', params.experimental.toString()); + } + } + + return fetchUrl(url.toString()).then(JSON.parse); } export async function getArchiveInfo( diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 3ed6ee553a507..08f47a8f1caaa 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -5,9 +5,16 @@ */ import { schema } from '@kbn/config-schema'; +export const GetCategoriesRequestSchema = { + query: schema.object({ + experimental: schema.maybe(schema.boolean()), + }), +}; + export const GetPackagesRequestSchema = { query: schema.object({ category: schema.maybe(schema.string()), + experimental: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap index a3bb32337f9f8..096f26eb22fe3 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; -exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features, request an extension now.

"`; +exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"
Extend your trial

If you’d like to continue using machine learning, advanced security, and our other awesome subscription features, request an extension now.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap index cb2a41dadbe9e..0a5656aa266bc 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is about to expire 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; -exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when license is expired 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; -exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features.

"`; +exports[`RevertToBasic component should display when trial is active 1`] = `"
Revert to Basic license

You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features.

"`; diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap index 9370b77e29560..9da8bb958941b 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed display for basic license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; -exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other Platinum features have to offer.

"`; +exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"
Start a 30-day trial

Experience what machine learning, advanced security, and all our other subscription features have to offer.

"`; diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js index fb1ea026abaa0..77fb10c71091d 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/request_trial_extension/request_trial_extension.js @@ -19,13 +19,13 @@ export const RequestTrialExtension = ({ shouldShowRequestTrialExtension }) => { ), diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js index a1a46d8616554..24b51cccb4e45 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js @@ -82,13 +82,13 @@ export class RevertToBasic extends React.PureComponent { ), diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx index 65d40f1de2009..7220f377cf386 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -94,14 +94,14 @@ export class StartTrial extends Component {

), @@ -236,15 +236,15 @@ export class StartTrial extends Component { const description = ( ), diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx index 83e7b82986cf8..d71a180cd2206 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx @@ -11,13 +11,17 @@ import { mount } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; +import { UrlStateProvider } from '../../../util/url_state'; + import { SelectInterval } from './select_interval'; describe('SelectInterval', () => { test('creates correct initial selected value', () => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSelect); @@ -29,7 +33,9 @@ describe('SelectInterval', () => { test('currently selected value is updated correctly on click', (done) => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSelect).first(); diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx index 484a0c395f3f8..cb4f80bfe6809 100644 --- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx @@ -11,13 +11,17 @@ import { mount } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; +import { UrlStateProvider } from '../../../util/url_state'; + import { SelectSeverity } from './select_severity'; describe('SelectSeverity', () => { test('creates correct severity options and initial selected value', () => { const wrapper = mount( - + + + ); const select = wrapper.find(EuiSuperSelect); @@ -65,7 +69,9 @@ describe('SelectSeverity', () => { test('state for currently selected value is updated correctly on click', (done) => { const wrapper = mount( - + + + ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index aa637f71db1cc..618ea5184007d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -121,16 +121,24 @@ export interface DfAnalyticsExplainResponse { } export interface Eval { - meanSquaredError: number | string; + mse: number | string; + msle: number | string; + huber: number | string; rSquared: number | string; error: null | string; } export interface RegressionEvaluateResponse { regression: { + huber: { + value: number; + }; mse: { value: number; }; + msle: { + value: number; + }; r_squared: { value: number; }; @@ -414,19 +422,37 @@ export const useRefreshAnalyticsList = ( const DEFAULT_SIG_FIGS = 3; -export function getValuesFromResponse(response: RegressionEvaluateResponse) { - let meanSquaredError = response?.regression?.mse?.value; +interface RegressionEvaluateExtractedResponse { + mse: number | string; + msle: number | string; + huber: number | string; + r_squared: number | string; +} - if (meanSquaredError) { - meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); - } +export const EMPTY_STAT = '--'; - let rSquared = response?.regression?.r_squared?.value; - if (rSquared) { - rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS)); +export function getValuesFromResponse(response: RegressionEvaluateResponse) { + const results: RegressionEvaluateExtractedResponse = { + mse: EMPTY_STAT, + msle: EMPTY_STAT, + huber: EMPTY_STAT, + r_squared: EMPTY_STAT, + }; + + if (response?.regression) { + for (const statType in response.regression) { + if (response.regression.hasOwnProperty(statType)) { + let currentStatValue = + response.regression[statType as keyof RegressionEvaluateResponse['regression']]?.value; + if (currentStatValue) { + currentStatValue = Number(currentStatValue.toPrecision(DEFAULT_SIG_FIGS)); + } + results[statType as keyof RegressionEvaluateExtractedResponse] = currentStatValue; + } + } } - return { meanSquaredError, rSquared }; + return results; } interface ResultsSearchBoolQuery { bool: Dictionary; @@ -490,13 +516,22 @@ export function getEvalQueryBody({ return query; } +export enum REGRESSION_STATS { + MSE = 'mse', + MSLE = 'msle', + R_SQUARED = 'rSquared', + HUBER = 'huber', +} + interface EvaluateMetrics { classification: { multiclass_confusion_matrix: object; }; regression: { r_squared: object; - mean_squared_error: object; + mse: object; + msle: object; + huber: object; }; } @@ -541,7 +576,9 @@ export const loadEvalData = async ({ }, regression: { r_squared: {}, - mean_squared_error: {}, + mse: {}, + msle: {}, + huber: {}, }, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index b83dd2e4329e0..9dae54b6537b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -71,12 +71,8 @@ export const ConfigurationStepForm: FC = ({ EuiComboBoxOptionOption[] >([]); const [includesTableItems, setIncludesTableItems] = useState([]); - const [maxDistinctValuesError, setMaxDistinctValuesError] = useState( - undefined - ); - const [unsupportedFieldsError, setUnsupportedFieldsError] = useState( - undefined - ); + const [maxDistinctValuesError, setMaxDistinctValuesError] = useState(); + const [unsupportedFieldsError, setUnsupportedFieldsError] = useState(); const { setEstimatedModelMemoryLimit, setFormState } = actions; const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx index a4d86b48006e8..8a41eb4b8a865 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx @@ -26,7 +26,7 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ state, }) => { const { form, isJobCreated } = state; - const { description, jobId, destinationIndex } = form; + const { description, jobId, destinationIndex, resultsField } = form; const detailsFirstCol: ListItems[] = [ { @@ -37,6 +37,19 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ }, ]; + if ( + resultsField !== undefined && + typeof resultsField === 'string' && + resultsField.trim() !== '' + ) { + detailsFirstCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.resultsField', { + defaultMessage: 'Results field', + }), + description: resultsField, + }); + } + const detailsSecondCol: ListItems[] = [ { title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index d846ae95c2c7e..168d5e31f57c3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -47,6 +47,7 @@ export const DetailsStepForm: FC = ({ jobIdExists, jobIdInvalidMaxLength, jobIdValid, + resultsField, } = form; const forceInput = useRef(null); @@ -195,6 +196,22 @@ export const DetailsStepForm: FC = ({ data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput" /> + + setFormState({ resultsField: e.target.value })} + data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput" + /> + = ({ jobConfig, jobStatus, searchQuery }) => { const { @@ -82,18 +94,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) genErrorEval.eval && isRegressionEvaluateResponse(genErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingGeneralization(false); } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '--', - rSquared: '--', + ...EMPTY_STATS, error: genErrorEval.error, }); } @@ -118,18 +131,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) trainingErrorEval.eval && isRegressionEvaluateResponse(trainingErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingTraining(false); } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '--', - rSquared: '--', + ...EMPTY_STATS, error: trainingErrorEval.error, }); } @@ -274,22 +288,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - + + {/* First row stats */} - + + + + + + + + + {/* Second row stats */} - + + + + + + + + @@ -331,22 +371,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - + + {/* First row stats */} - + + + + + + + + + {/* Second row stats */} - + + + + + + + + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx index 1b4461b2bb075..114ec75efb2e7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx @@ -6,58 +6,99 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { REGRESSION_STATS } from '../../../../common/analytics'; interface Props { isLoading: boolean; title: number | string; - isMSE: boolean; + statType: REGRESSION_STATS; dataTestSubj: string; } -const meanSquaredErrorText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', - { - defaultMessage: 'Mean squared error', - } -); -const rSquaredText = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', - { - defaultMessage: 'R squared', - } -); -const meanSquaredErrorTooltipContent = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', - { - defaultMessage: - 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', - } -); -const rSquaredTooltipContent = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', - { - defaultMessage: - 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', - } -); +const statDescriptions = { + [REGRESSION_STATS.MSE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', + { + defaultMessage: 'Mean squared error', + } + ), + [REGRESSION_STATS.MSLE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.msleText', + { + defaultMessage: 'Mean squared logarithmic error', + } + ), + [REGRESSION_STATS.R_SQUARED]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText', + { + defaultMessage: 'R squared', + } + ), + [REGRESSION_STATS.HUBER]: ( + + {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.huberLinkText', { + defaultMessage: 'Pseudo Huber loss function', + })} + + ), + }} + /> + ), +}; + +const tooltipContent = { + [REGRESSION_STATS.MSE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent', + { + defaultMessage: + 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.', + } + ), + [REGRESSION_STATS.MSLE]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.msleTooltipContent', + { + defaultMessage: + 'Average squared difference between the logarithm of the predicted values and the logarithm of the actual (ground truth) value', + } + ), + [REGRESSION_STATS.R_SQUARED]: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent', + { + defaultMessage: + 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.', + } + ), +}; -export const EvaluateStat: FC = ({ isLoading, isMSE, title, dataTestSubj }) => ( +export const EvaluateStat: FC = ({ isLoading, statType, title, dataTestSubj }) => ( - + {statType !== REGRESSION_STATS.HUBER && ( + + )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts similarity index 98% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts index 4227c19fec5af..9db32e298691e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isAdvancedConfig } from './action_clone'; +import { isAdvancedConfig } from './clone_button'; describe('Analytics job clone action', () => { describe('isAdvancedConfig', () => { @@ -131,7 +131,7 @@ describe('Analytics job clone action', () => { }, analyzed_fields: { includes: [], - excludes: [], + excludes: ['excluded_field'], }, model_memory_limit: '350mb', allow_lazy_start: false, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx index bff54bc283296..280ec544c1e5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx @@ -19,7 +19,7 @@ import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; -import { DataFrameAnalyticsListRow } from './common'; +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; @@ -247,6 +247,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, + formKey: 'resultsField', defaultValue: DEFAULT_RESULTS_FIELD, }, }, @@ -343,7 +344,7 @@ export function getCloneAction(createAnalyticsForm: CreateAnalyticsFormProps) { }; } -interface CloneActionProps { +interface CloneButtonProps { item: DataFrameAnalyticsListRow; createAnalyticsForm: CreateAnalyticsFormProps; } @@ -353,7 +354,7 @@ interface CloneActionProps { * Replace with {@link getCloneAction} as soon as all the actions are refactored * to support EuiContext with a valid DOM structure without nested buttons. */ -export const CloneAction: FC = ({ createAnalyticsForm, item }) => { +export const CloneButton: FC = ({ createAnalyticsForm, item }) => { const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts new file mode 100644 index 0000000000000..b3d7189ff8cda --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + extractCloningConfig, + isAdvancedConfig, + CloneButton, + CloneDataFrameAnalyticsConfig, +} from './clone_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx similarity index 78% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx index 33217f127f998..8d6272c5df860 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/action_delete.test.tsx @@ -7,14 +7,17 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; -import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -import { DeleteAction } from './action_delete'; +import mockAnalyticsListItem from '../analytics_list/__mocks__/analytics_list_item.json'; import { I18nProvider } from '@kbn/i18n/react'; import { coreMock as mockCoreServices, i18nServiceMock, } from '../../../../../../../../../../src/core/public/mocks'; +import { DeleteButton } from './delete_button'; +import { DeleteButtonModal } from './delete_button_modal'; +import { useDeleteAction } from './use_delete_action'; + jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), @@ -41,14 +44,18 @@ describe('DeleteAction', () => { }); test('When canDeleteDataFrameAnalytics permission is false, button should be disabled.', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + {}} /> + ); expect(getByTestId('mlAnalyticsJobDeleteButton')).toHaveAttribute('disabled'); }); test('When canDeleteDataFrameAnalytics permission is true, button should not be disabled.', () => { const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); - const { getByTestId } = render(); + const { getByTestId } = render( + {}} /> + ); expect(getByTestId('mlAnalyticsJobDeleteButton')).not.toHaveAttribute('disabled'); @@ -57,11 +64,12 @@ describe('DeleteAction', () => { test('When job is running, delete button should be disabled.', () => { const { getByTestId } = render( - {}} /> ); @@ -72,9 +80,21 @@ describe('DeleteAction', () => { test('should allow to delete target index by default.', () => { const mock = jest.spyOn(CheckPrivilige, 'checkPermission'); mock.mockImplementation((p) => p === 'canDeleteDataFrameAnalytics'); + + const TestComponent = () => { + const deleteAction = useDeleteAction(); + + return ( + <> + {deleteAction.isModalVisible && } + + + ); + }; + const { getByTestId, queryByTestId } = render( - + ); const deleteButton = getByTestId('mlAnalyticsJobDeleteButton'); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx new file mode 100644 index 0000000000000..7da3bced48576 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; +import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from '../analytics_list/common'; + +interface DeleteButtonProps { + item: DataFrameAnalyticsListRow; + onClick: (item: DataFrameAnalyticsListRow) => void; +} + +export const DeleteButton: FC = ({ item, onClick }) => { + const disabled = isDataFrameAnalyticsRunning(item.stats.state); + const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); + + const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { + defaultMessage: 'Delete', + }); + + const buttonDisabled = disabled || !canDeleteDataFrameAnalytics; + let deleteButton = ( + onClick(item)} + aria-label={buttonDeleteText} + style={{ padding: 0 }} + > + {buttonDeleteText} + + ); + + if (disabled || !canDeleteDataFrameAnalytics) { + deleteButton = ( + + {deleteButton} + + ); + } + + return deleteButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx new file mode 100644 index 0000000000000..f94dccee479bd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_button_modal.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiConfirmModal, + EuiOverlayMask, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { DeleteAction } from './use_delete_action'; + +export const DeleteButtonModal: FC = ({ + closeModal, + deleteAndCloseModal, + deleteTargetIndex, + deleteIndexPattern, + indexPatternExists, + item, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, +}) => { + if (item === undefined) { + return null; + } + + const indexName = item.config.dest.index; + + return ( + + +

+ +

+ + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts new file mode 100644 index 0000000000000..ef891d7c4a128 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DeleteButton } from './delete_button'; +export { DeleteButtonModal } from './delete_button_modal'; +export { useDeleteAction } from './use_delete_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts new file mode 100644 index 0000000000000..f924cf3afcba5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { IIndexPattern } from 'src/plugins/data/common'; + +import { extractErrorMessage } from '../../../../../../../common/util/errors'; + +import { useMlKibana } from '../../../../../contexts/kibana'; + +import { + deleteAnalytics, + deleteAnalyticsAndDestIndex, + canDeleteIndex, +} from '../../services/analytics_service'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export type DeleteAction = ReturnType; +export const useDeleteAction = () => { + const [item, setItem] = useState(); + + const [isModalVisible, setModalVisible] = useState(false); + const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); + const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); + const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); + const [indexPatternExists, setIndexPatternExists] = useState(false); + + const { savedObjects, notifications } = useMlKibana().services; + const savedObjectsClient = savedObjects.client; + + const indexName = item?.config.dest.index ?? ''; + + const checkIndexPatternExists = async () => { + try { + const response = await savedObjectsClient.find({ + type: 'index-pattern', + perPage: 10, + search: `"${indexName}"`, + searchFields: ['title'], + fields: ['title'], + }); + const ip = response.savedObjects.find( + (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() + ); + if (ip !== undefined) { + setIndexPatternExists(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if index pattern {indexPattern} exists: {error}', + values: { indexPattern: indexName, error }, + } + ) + ); + } + }; + const checkUserIndexPermission = () => { + try { + const userCanDelete = canDeleteIndex(indexName); + if (userCanDelete) { + setUserCanDeleteIndex(true); + } + } catch (e) { + const { toasts } = notifications; + const error = extractErrorMessage(e); + + toasts.addDanger( + i18n.translate( + 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', + { + defaultMessage: + 'An error occurred checking if user can delete {destinationIndex}: {error}', + values: { destinationIndex: indexName, error }, + } + ) + ); + } + }; + + useEffect(() => { + // Check if an index pattern exists corresponding to current DFA job + // if pattern does exist, show it to user + checkIndexPatternExists(); + + // Check if an user has permission to delete the index & index pattern + checkUserIndexPermission(); + }, []); + + const closeModal = () => setModalVisible(false); + const deleteAndCloseModal = () => { + setModalVisible(false); + + if (item !== undefined) { + if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { + deleteAnalyticsAndDestIndex( + item, + deleteTargetIndex, + indexPatternExists && deleteIndexPattern + ); + } else { + deleteAnalytics(item); + } + } + }; + const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); + const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); + + const openModal = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setModalVisible(true); + }; + + return { + closeModal, + deleteAndCloseModal, + deleteTargetIndex, + deleteIndexPattern, + indexPatternExists, + isModalVisible, + item, + openModal, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx similarity index 55% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx index 041b52d0322c4..0acb215336faf 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button.tsx @@ -4,44 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, FC } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; -import { DataFrameAnalyticsListRow } from './common'; -import { EditAnalyticsFlyout } from './edit_analytics_flyout'; - -interface EditActionProps { - item: DataFrameAnalyticsListRow; +interface EditButtonProps { + onClick: () => void; } -export const EditAction: FC = ({ item }) => { +export const EditButton: FC = ({ onClick }) => { const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = () => setIsFlyoutVisible(true); - const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', { defaultMessage: 'Edit', }); + const buttonDisabled = !canCreateDataFrameAnalytics; const editButton = ( - - {buttonEditText} - + {buttonEditText} + ); if (!canCreateDataFrameAnalytics) { @@ -57,10 +50,5 @@ export const EditAction: FC = ({ item }) => { ); } - return ( - <> - {editButton} - {isFlyoutVisible && } - - ); + return editButton; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx index b6aed9321e4e3..728f53bf69ee2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/edit_button_flyout.tsx @@ -32,20 +32,17 @@ import { MemoryInputValidatorResult, } from '../../../../../../../common/util/validators'; import { extractErrorMessage } from '../../../../../../../common/util/errors'; -import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common'; +import { DATA_FRAME_TASK_STATE } from '../analytics_list/common'; import { useRefreshAnalyticsList, UpdateDataFrameAnalyticsConfig, } from '../../../../common/analytics'; -interface EditAnalyticsJobFlyoutProps { - closeFlyout: () => void; - item: DataFrameAnalyticsListRow; -} +import { EditAction } from './use_edit_action'; let mmLValidator: (value: any) => MemoryInputValidatorResult; -export const EditAnalyticsFlyout: FC = ({ closeFlyout, item }) => { +export const EditButtonFlyout: FC> = ({ closeFlyout, item }) => { const { id: jobId, config } = item; const { state } = item.stats; const initialAllowLazyStart = diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts new file mode 100644 index 0000000000000..cfb0bba16ca18 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditButton } from './edit_button'; +export { EditButtonFlyout } from './edit_button_flyout'; +export { isEditActionFlyoutVisible, useEditAction } from './use_edit_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts new file mode 100644 index 0000000000000..82a7bcc91997a --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_edit/use_edit_action.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +export const isEditActionFlyoutVisible = (editAction: any): editAction is Required => { + return editAction.isFlyoutVisible === true && editAction.item !== undefined; +}; + +export interface EditAction { + isFlyoutVisible: boolean; + item?: DataFrameAnalyticsListRow; + closeFlyout: () => void; + openFlyout: (newItem: DataFrameAnalyticsListRow) => void; +} +export const useEditAction = () => { + const [item, setItem] = useState(); + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const openFlyout = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setIsFlyoutVisible(true); + }; + + return { + isFlyoutVisible, + item, + closeFlyout, + openFlyout, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts new file mode 100644 index 0000000000000..df6bbb7c61908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StartButton } from './start_button'; +export { StartButtonModal } from './start_button_modal'; +export { useStartAction } from './use_start_action'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx new file mode 100644 index 0000000000000..279a335de8f42 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; + +import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from '../analytics_list/common'; + +interface StartButtonProps { + item: DataFrameAnalyticsListRow; + onClick: (item: DataFrameAnalyticsListRow) => void; +} + +export const StartButton: FC = ({ item, onClick }) => { + const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); + + const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for analytics jobs which have completed. + const completeAnalytics = isCompletedAnalyticsJob(item.stats); + + const disabled = !canStartStopDataFrameAnalytics || completeAnalytics; + + let startButton = ( + onClick(item)} + aria-label={buttonStartText} + data-test-subj="mlAnalyticsJobStartButton" + > + {buttonStartText} + + ); + + if (!canStartStopDataFrameAnalytics || completeAnalytics) { + startButton = ( + + {startButton} + + ); + } + + return startButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx new file mode 100644 index 0000000000000..664dbe5c62b2f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_button_modal.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +import { StartAction } from './use_start_action'; + +export const StartButtonModal: FC = ({ closeModal, item, startAndCloseModal }) => { + return ( + <> + {item !== undefined && ( + + +

+ {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { + defaultMessage: + 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', + })} +

+
+
+ )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts new file mode 100644 index 0000000000000..8eb6b990827ac --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/use_start_action.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; +import { startAnalytics } from '../../services/analytics_service'; + +export type StartAction = ReturnType; +export const useStartAction = () => { + const [isModalVisible, setModalVisible] = useState(false); + + const [item, setItem] = useState(); + + const closeModal = () => setModalVisible(false); + const startAndCloseModal = () => { + if (item !== undefined) { + setModalVisible(false); + startAnalytics(item); + } + }; + + const openModal = (newItem: DataFrameAnalyticsListRow) => { + setItem(newItem); + setModalVisible(true); + }; + + return { + closeModal, + isModalVisible, + item, + openModal, + startAndCloseModal, + }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts similarity index 84% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts index 41bc2aa258807..858b6c70501b3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/index.ts @@ -3,3 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +export { StopButton } from './stop_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx new file mode 100644 index 0000000000000..b8395f2f7c2a0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_button.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { + checkPermission, + createPermissionFailureMessage, +} from '../../../../../capabilities/check_capabilities'; + +import { stopAnalytics } from '../../services/analytics_service'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +const buttonStopText = i18n.translate('xpack.ml.dataframe.analyticsList.stopActionName', { + defaultMessage: 'Stop', +}); + +interface StopButtonProps { + item: DataFrameAnalyticsListRow; +} + +export const StopButton: FC = ({ item }) => { + const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); + + const stopButton = ( + stopAnalytics(item)} + aria-label={buttonStopText} + data-test-subj="mlAnalyticsJobStopButton" + > + {buttonStopText} + + ); + if (!canStartStopDataFrameAnalytics) { + return ( + + {stopButton} + + ); + } + + return stopButton; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx new file mode 100644 index 0000000000000..e31670ea42ceb --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/get_view_action.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableActionsColumnType } from '@elastic/eui'; + +import { DataFrameAnalyticsListRow } from '../analytics_list/common'; + +import { ViewButton } from './view_button'; + +export const getViewAction = ( + isManagementTable: boolean = false +): EuiTableActionsColumnType['actions'][number] => ({ + isPrimary: true, + render: (item: DataFrameAnalyticsListRow) => ( + + ), +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts new file mode 100644 index 0000000000000..5ac12c12071fd --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getViewAction } from './get_view_action'; +export { ViewButton } from './view_button'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx new file mode 100644 index 0000000000000..17a18c374dfa6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/view_button.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import { + getAnalysisType, + isRegressionAnalysis, + isOutlierAnalysis, + isClassificationAnalysis, +} from '../../../../common/analytics'; +import { useMlKibana } from '../../../../../contexts/kibana'; + +import { getResultsUrl, DataFrameAnalyticsListRow } from '../analytics_list/common'; + +interface ViewButtonProps { + item: DataFrameAnalyticsListRow; + isManagementTable: boolean; +} + +export const ViewButton: FC = ({ item, isManagementTable }) => { + const { + services: { + application: { navigateToUrl, navigateToApp }, + }, + } = useMlKibana(); + + const analysisType = getAnalysisType(item.config.analysis); + const isDisabled = + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); + + const url = getResultsUrl(item.id, analysisType); + const navigator = isManagementTable + ? () => navigateToApp('ml', { path: url }) + : () => navigateToUrl(url); + + return ( + + {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { + defaultMessage: 'View', + })} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx deleted file mode 100644 index 38ef00914e8fb..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useState, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EuiSwitch, - EuiFlexGroup, - EuiFlexItem, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/common'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; -import { - deleteAnalytics, - deleteAnalyticsAndDestIndex, - canDeleteIndex, -} from '../../services/analytics_service'; -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; -import { useMlKibana } from '../../../../../contexts/kibana'; -import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; - -interface DeleteActionProps { - item: DataFrameAnalyticsListRow; -} - -export const DeleteAction: FC = ({ item }) => { - const disabled = isDataFrameAnalyticsRunning(item.stats.state); - const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); - - const [isModalVisible, setModalVisible] = useState(false); - const [deleteTargetIndex, setDeleteTargetIndex] = useState(true); - const [deleteIndexPattern, setDeleteIndexPattern] = useState(true); - const [userCanDeleteIndex, setUserCanDeleteIndex] = useState(false); - const [indexPatternExists, setIndexPatternExists] = useState(false); - - const { savedObjects, notifications } = useMlKibana().services; - const savedObjectsClient = savedObjects.client; - - const indexName = item.config.dest.index; - - const checkIndexPatternExists = async () => { - try { - const response = await savedObjectsClient.find({ - type: 'index-pattern', - perPage: 10, - search: `"${indexName}"`, - searchFields: ['title'], - fields: ['title'], - }); - const ip = response.savedObjects.find( - (obj) => obj.attributes.title.toLowerCase() === indexName.toLowerCase() - ); - if (ip !== undefined) { - setIndexPatternExists(true); - } - } catch (e) { - const { toasts } = notifications; - const error = extractErrorMessage(e); - - toasts.addDanger( - i18n.translate( - 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfIndexPatternExistsNotificationErrorMessage', - { - defaultMessage: - 'An error occurred checking if index pattern {indexPattern} exists: {error}', - values: { indexPattern: indexName, error }, - } - ) - ); - } - }; - const checkUserIndexPermission = () => { - try { - const userCanDelete = canDeleteIndex(indexName); - if (userCanDelete) { - setUserCanDeleteIndex(true); - } - } catch (e) { - const { toasts } = notifications; - const error = extractErrorMessage(e); - - toasts.addDanger( - i18n.translate( - 'xpack.ml.dataframe.analyticsList.errorWithCheckingIfUserCanDeleteIndexNotificationErrorMessage', - { - defaultMessage: - 'An error occurred checking if user can delete {destinationIndex}: {error}', - values: { destinationIndex: indexName, error }, - } - ) - ); - } - }; - - useEffect(() => { - // Check if an index pattern exists corresponding to current DFA job - // if pattern does exist, show it to user - checkIndexPatternExists(); - - // Check if an user has permission to delete the index & index pattern - checkUserIndexPermission(); - }, []); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - - if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) { - deleteAnalyticsAndDestIndex( - item, - deleteTargetIndex, - indexPatternExists && deleteIndexPattern - ); - } else { - deleteAnalytics(item); - } - }; - const openModal = () => setModalVisible(true); - const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex); - const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern); - - const buttonDeleteText = i18n.translate('xpack.ml.dataframe.analyticsList.deleteActionName', { - defaultMessage: 'Delete', - }); - - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteDataFrameAnalytics) { - deleteButton = ( - - {deleteButton} - - ); - } - - return ( - - {deleteButton} - {isModalVisible && ( - - -

- -

- - - - {userCanDeleteIndex && ( - - )} - - - {userCanDeleteIndex && indexPatternExists && ( - - )} - - -
-
- )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx deleted file mode 100644 index 74eb1d0b02782..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; - -import { startAnalytics } from '../../services/analytics_service'; - -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; - -import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; - -interface StartActionProps { - item: DataFrameAnalyticsListRow; -} - -export const StartAction: FC = ({ item }) => { - const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const startAndCloseModal = () => { - setModalVisible(false); - startAnalytics(item); - }; - const openModal = () => setModalVisible(true); - - const buttonStartText = i18n.translate('xpack.ml.dataframe.analyticsList.startActionName', { - defaultMessage: 'Start', - }); - - // Disable start for analytics jobs which have completed. - const completeAnalytics = isCompletedAnalyticsJob(item.stats); - - let startButton = ( - - {buttonStartText} - - ); - - if (!canStartStopDataFrameAnalytics || completeAnalytics) { - startButton = ( - - {startButton} - - ); - } - - return ( - - {startButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { - defaultMessage: - 'A data frame analytics job will increase search and indexing load in your cluster. Please stop the analytics job if excessive load is experienced. Are you sure you want to start this analytics job?', - })} -

-
-
- )} -
- ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx deleted file mode 100644 index b03a3a4c4edb2..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; - -import { - checkPermission, - createPermissionFailureMessage, -} from '../../../../../capabilities/check_capabilities'; - -import { - getAnalysisType, - isRegressionAnalysis, - isOutlierAnalysis, - isClassificationAnalysis, -} from '../../../../common/analytics'; -import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; -import { useMlKibana } from '../../../../../contexts/kibana'; -import { CloneAction } from './action_clone'; - -import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; -import { stopAnalytics } from '../../services/analytics_service'; - -import { StartAction } from './action_start'; -import { EditAction } from './action_edit'; -import { DeleteAction } from './action_delete'; - -interface Props { - item: DataFrameAnalyticsListRow; - isManagementTable: boolean; -} - -const AnalyticsViewButton: FC = ({ item, isManagementTable }) => { - const { - services: { - application: { navigateToUrl, navigateToApp }, - }, - } = useMlKibana(); - - const analysisType = getAnalysisType(item.config.analysis); - const isDisabled = - !isRegressionAnalysis(item.config.analysis) && - !isOutlierAnalysis(item.config.analysis) && - !isClassificationAnalysis(item.config.analysis); - - const url = getResultsUrl(item.id, analysisType); - const navigator = isManagementTable - ? () => navigateToApp('ml', { path: url }) - : () => navigateToUrl(url); - - return ( - - {i18n.translate('xpack.ml.dataframe.analyticsList.viewActionName', { - defaultMessage: 'View', - })} - - ); -}; - -interface Action { - isPrimary?: boolean; - render: (item: DataFrameAnalyticsListRow) => any; -} - -export const getAnalyticsViewAction = (isManagementTable: boolean = false): Action => ({ - isPrimary: true, - render: (item: DataFrameAnalyticsListRow) => ( - - ), -}); - -export const getActions = ( - createAnalyticsForm: CreateAnalyticsFormProps, - isManagementTable: boolean -) => { - const canStartStopDataFrameAnalytics: boolean = checkPermission('canStartStopDataFrameAnalytics'); - const actions: Action[] = [getAnalyticsViewAction(isManagementTable)]; - - if (isManagementTable === false) { - actions.push( - ...[ - { - render: (item: DataFrameAnalyticsListRow) => { - if (!isDataFrameAnalyticsRunning(item.stats.state)) { - return ; - } - - const buttonStopText = i18n.translate( - 'xpack.ml.dataframe.analyticsList.stopActionName', - { - defaultMessage: 'Stop', - } - ); - - const stopButton = ( - stopAnalytics(item)} - aria-label={buttonStopText} - data-test-subj="mlAnalyticsJobStopButton" - > - {buttonStopText} - - ); - if (!canStartStopDataFrameAnalytics) { - return ( - - {stopButton} - - ); - } - - return stopButton; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - { - render: (item: DataFrameAnalyticsListRow) => { - return ; - }, - }, - ] - ); - } - - return actions; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index dac0de4c7a533..405231aef5774 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState, useEffect } from 'react'; +import React, { FC, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; @@ -25,7 +25,6 @@ import { ANALYSIS_CONFIG_TYPE, } from '../../../../common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; -import { getTaskStateBadge, getJobTypeBadge } from './columns'; import { DataFrameAnalyticsListColumn, @@ -38,7 +37,7 @@ import { FieldClause, } from './common'; import { getAnalyticsFactory } from '../../services/analytics_service'; -import { getColumns } from './columns'; +import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { ProgressBar, @@ -232,6 +231,14 @@ export const DataFrameAnalyticsList: FC = ({ setIsLoading(false); }; + const { columns, modals } = useColumns( + expandedRowItemIds, + setExpandedRowItemIds, + isManagementTable, + isMlEnabledInSpace, + createAnalyticsForm + ); + // Before the analytics have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No data frame analytics found' during the initial loading. if (!isInitialized) { @@ -240,7 +247,7 @@ export const DataFrameAnalyticsList: FC = ({ if (typeof errorMessage !== 'undefined') { return ( - + <> = ({ >
{JSON.stringify(errorMessage)}
-
+ ); } if (analytics.length === 0) { return ( - + <> = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - + ); } - const columns = getColumns( - expandedRowItemIds, - setExpandedRowItemIds, - isManagementTable, - isMlEnabledInSpace, - createAnalyticsForm - ); - const sorting = { sort: { field: sortField, @@ -349,26 +348,6 @@ export const DataFrameAnalyticsList: FC = ({ view: getTaskStateBadge(val), })), }, - // For now analytics jobs are batch only - /* - { - type: 'field_value_selection', - field: 'mode', - name: i18n.translate('xpack.ml.dataframe.analyticsList.modeFilter', { - defaultMessage: 'Mode', - }), - multiSelect: false, - options: Object.values(DATA_FRAME_MODE).map(val => ({ - value: val, - name: val, - view: ( - - {val} - - ), - })), - }, - */ ], }; @@ -386,7 +365,8 @@ export const DataFrameAnalyticsList: FC = ({ }; return ( - + <> + {modals} {analyticsStats && ( @@ -435,6 +415,6 @@ export const DataFrameAnalyticsList: FC = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 0ee57fe5be141..5276fedff0fde 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -24,11 +24,12 @@ import { loadEvalData, Eval, } from '../../../../common'; -import { getTaskStateBadge } from './columns'; +import { getTaskStateBadge } from './use_columns'; import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, ANALYSIS_CONFIG_TYPE, + REGRESSION_STATS, isRegressionEvaluateResponse, } from '../../../../common/analytics'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; @@ -44,7 +45,7 @@ function getItemDescription(value: any) { interface LoadedStatProps { isLoading: boolean; evalData: Eval; - resultProperty: 'meanSquaredError' | 'rSquared'; + resultProperty: REGRESSION_STATS; } const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => { @@ -61,7 +62,7 @@ interface Props { item: DataFrameAnalyticsListRow; } -const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; +const defaultEval: Eval = { mse: '', msle: '', huber: '', rSquared: '', error: null }; export const ExpandedRow: FC = ({ item }) => { const [trainingEval, setTrainingEval] = useState(defaultEval); @@ -94,17 +95,21 @@ export const ExpandedRow: FC = ({ item }) => { genErrorEval.eval && isRegressionEvaluateResponse(genErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingGeneralization(false); } else { setIsLoadingGeneralization(false); setGeneralizationEval({ - meanSquaredError: '', + mse: '', + msle: '', + huber: '', rSquared: '', error: genErrorEval.error, }); @@ -124,17 +129,21 @@ export const ExpandedRow: FC = ({ item }) => { trainingErrorEval.eval && isRegressionEvaluateResponse(trainingErrorEval.eval) ) { - const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ - meanSquaredError, - rSquared, + mse, + msle, + huber, + rSquared: r_squared, error: null, }); setIsLoadingTraining(false); } else { setIsLoadingTraining(false); setTrainingEval({ - meanSquaredError: '', + mse: '', + msle: '', + huber: '', rSquared: '', error: genErrorEval.error, }); @@ -221,7 +230,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'generalization mean squared logarithmic error', + description: ( + ), }, @@ -231,7 +250,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'generalization pseudo huber loss function', + description: ( + ), }, @@ -241,7 +270,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'training mean squared logarithmic error', + description: ( + ), }, @@ -251,7 +290,17 @@ export const ExpandedRow: FC = ({ item }) => { + ), + }, + { + title: 'training pseudo huber loss function', + description: ( + ), } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx new file mode 100644 index 0000000000000..e75d938116991 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableActionsColumnType } from '@elastic/eui'; + +import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; +import { CloneButton } from '../action_clone'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { + isEditActionFlyoutVisible, + useEditAction, + EditButton, + EditButtonFlyout, +} from '../action_edit'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; +import { getViewAction } from '../action_view'; + +import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; + +export const useActions = ( + createAnalyticsForm: CreateAnalyticsFormProps, + isManagementTable: boolean +): { + actions: EuiTableActionsColumnType['actions']; + modals: JSX.Element | null; +} => { + const deleteAction = useDeleteAction(); + const editAction = useEditAction(); + const startAction = useStartAction(); + + let modals: JSX.Element | null = null; + + const actions: EuiTableActionsColumnType['actions'] = [ + getViewAction(isManagementTable), + ]; + + if (isManagementTable === false) { + modals = ( + <> + {startAction.isModalVisible && } + {deleteAction.isModalVisible && } + {isEditActionFlyoutVisible(editAction) && } + + ); + actions.push( + ...[ + { + render: (item: DataFrameAnalyticsListRow) => { + if (!isDataFrameAnalyticsRunning(item.stats.state)) { + return ; + } + return ; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return editAction.openFlyout(item)} />; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, + ] + ); + } + + return { actions, modals }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx similarity index 93% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index a3d2e65386c19..fa88396461cd7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -33,7 +33,7 @@ import { DataFrameAnalyticsListRow, DataFrameAnalyticsStats, } from './common'; -import { getActions } from './actions'; +import { useActions } from './use_actions'; enum TASK_STATE_COLOR { analyzing = 'primary', @@ -141,14 +141,14 @@ export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => ( {item.id} ); -export const getColumns = ( +export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], setExpandedRowItemIds: React.Dispatch>, isManagementTable: boolean = false, isMlEnabledInSpace: boolean = true, createAnalyticsForm?: CreateAnalyticsFormProps ) => { - const actions = getActions(createAnalyticsForm!, isManagementTable); + const { actions, modals } = useActions(createAnalyticsForm!, isManagementTable); function toggleDetails(item: DataFrameAnalyticsListRow) { const index = expandedRowItemIds.indexOf(item.config.id); @@ -253,20 +253,6 @@ export const getColumns = ( width: '100px', 'data-test-subj': 'mlAnalyticsTableColumnStatus', }, - // For now there is batch mode only so we hide this column for now. - /* - { - name: i18n.translate('xpack.ml.dataframe.analyticsList.mode', { defaultMessage: 'Mode' }), - sortable: (item: DataFrameAnalyticsListRow) => item.mode, - truncateText: true, - render(item: DataFrameAnalyticsListRow) { - const mode = item.mode; - const color = 'hollow'; - return {mode}; - }, - width: '100px', - }, - */ progressColumn, { name: i18n.translate('xpack.ml.dataframe.analyticsList.tableActionLabel', { @@ -293,5 +279,5 @@ export const getColumns = ( } } - return columns; + return { columns, modals }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 81d35679443b8..b344e44c97d59 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -144,6 +144,11 @@ export const validateAdvancedEditor = (state: State): State => { const destinationIndexNameValid = isValidIndexName(destinationIndexName); const destinationIndexPatternTitleExists = state.indexPatternsMap[destinationIndexName] !== undefined; + + const resultsFieldEmptyString = + typeof jobConfig?.dest?.results_field === 'string' && + jobConfig?.dest?.results_field.trim() === ''; + const mml = jobConfig.model_memory_limit; const modelMemoryLimitEmpty = mml === '' || mml === undefined; if (!modelMemoryLimitEmpty && mml !== undefined) { @@ -292,6 +297,18 @@ export const validateAdvancedEditor = (state: State): State => { }); } + if (resultsFieldEmptyString) { + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString', + { + defaultMessage: 'The results field must not be an empty string.', + } + ), + message: '', + }); + } + if (dependentVariableEmpty) { state.advancedEditorMessages.push({ error: i18n.translate( @@ -336,6 +353,7 @@ export const validateAdvancedEditor = (state: State): State => { sourceIndexNameValid && !destinationIndexNameEmpty && destinationIndexNameValid && + !resultsFieldEmptyString && !dependentVariableEmpty && !modelMemoryLimitEmpty && numTopFeatureImportanceValuesValid && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index da6e2e440a26e..0d425c8ead4a2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -13,7 +13,7 @@ import { DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; -import { CloneDataFrameAnalyticsConfig } from '../../components/analytics_list/action_clone'; +import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', @@ -82,6 +82,7 @@ export interface State { previousJobType: null | AnalyticsJobType; requiredFieldsError: string | undefined; randomizeSeed: undefined | number; + resultsField: undefined | string; sourceIndex: EsIndexName; sourceIndexNameEmpty: boolean; sourceIndexNameValid: boolean; @@ -147,6 +148,7 @@ export const getInitialState = (): State => ({ previousJobType: null, requiredFieldsError: undefined, randomizeSeed: undefined, + resultsField: undefined, sourceIndex: '', sourceIndexNameEmpty: true, sourceIndexNameValid: false, @@ -198,6 +200,13 @@ export const getJobConfigFromFormState = ( model_memory_limit: formState.modelMemoryLimit, }; + const resultsFieldEmpty = + typeof formState?.resultsField === 'string' && formState?.resultsField.trim() === ''; + + if (jobConfig.dest && !resultsFieldEmpty) { + jobConfig.dest.results_field = formState.resultsField; + } + if ( formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION @@ -277,6 +286,7 @@ export function getCloneFormStateFromJobConfig( const resultState: Partial = { jobType, description: analyticsJobConfig.description ?? '', + resultsField: analyticsJobConfig.dest.results_field, sourceIndex: Array.isArray(analyticsJobConfig.source.index) ? analyticsJobConfig.source.index.join(',') : analyticsJobConfig.source.index, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index f95d2f572a406..4c312be26613c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -18,10 +18,7 @@ import { DataFrameAnalyticsId, DataFrameAnalyticsConfig, } from '../../../../common'; -import { - extractCloningConfig, - isAdvancedConfig, -} from '../../components/analytics_list/action_clone'; +import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone'; import { ActionDispatchers, ACTION } from './actions'; import { reducer } from './reducer'; diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index 068f43a140c90..f356d79c0a8e1 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -9,12 +9,10 @@ import { useUrlState } from '../../util/url_state'; import { SWIMLANE_TYPE } from '../explorer_constants'; import { AppStateSelectedCells } from '../explorer_utils'; -export const useSelectedCells = (): [ - AppStateSelectedCells | undefined, - (swimlaneSelectedCells: AppStateSelectedCells) => void -] => { - const [appState, setAppState] = useUrlState('_a'); - +export const useSelectedCells = ( + appState: any, + setAppState: ReturnType[1] +): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => { // keep swimlane selection, restore selectedCells from AppState const selectedCells = useMemo(() => { return appState?.mlExplorerSwimlane?.selectedType !== undefined diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx index f2e6ff7885b16..1eeff6287867d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/table.tsx @@ -22,8 +22,8 @@ import { import { getTaskStateBadge, progressColumn, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns'; -import { getAnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions'; +} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns'; +import { getViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/action_view'; import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; const MlInMemoryTable = mlInMemoryTableFactory(); @@ -82,7 +82,7 @@ export const AnalyticsTable: FC = ({ items }) => { name: i18n.translate('xpack.ml.overview.analyticsList.tableActionLabel', { defaultMessage: 'Actions', }), - actions: [getAnalyticsViewAction()], + actions: [getViewAction()], width: '100px', }, ]; diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 281493c4e31b7..f1b8083f19ccf 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -12,6 +12,7 @@ import { IUiSettingsClient, ChromeStart } from 'kibana/public'; import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; import { MlContext, MlContextValue } from '../contexts/ml'; +import { UrlStateProvider } from '../util/url_state'; import * as routes from './routes'; @@ -48,21 +49,23 @@ export const MlRouter: FC<{ pageDeps: PageDependencies }> = ({ pageDeps }) => { return ( -
- {Object.entries(routes).map(([name, route]) => ( - { - window.setTimeout(() => { - setBreadcrumbs(route.breadcrumbs); - }); - return route.render(props, pageDeps); - }} - /> - ))} -
+ +
+ {Object.entries(routes).map(([name, route]) => ( + { + window.setTimeout(() => { + setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, pageDeps); + }} + /> + ))} +
+
); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 52b4408d1ac5b..7a7865c9bd738 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -152,7 +152,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); - const [selectedCells, setSelectedCells] = useSelectedCells(); + const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState); useEffect(() => { explorerService.setSelectedCells(selectedCells); }, [JSON.stringify(selectedCells)]); diff --git a/x-pack/plugins/ml/public/application/util/url_state.test.ts b/x-pack/plugins/ml/public/application/util/url_state.test.tsx similarity index 82% rename from x-pack/plugins/ml/public/application/util/url_state.test.ts rename to x-pack/plugins/ml/public/application/util/url_state.test.tsx index 0813f2e3da97f..9c03369648554 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.test.ts +++ b/x-pack/plugins/ml/public/application/util/url_state.test.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook, act } from '@testing-library/react-hooks'; -import { getUrlState, useUrlState } from './url_state'; +import React, { FC } from 'react'; +import { render, act } from '@testing-library/react'; +import { parseUrlState, useUrlState, UrlStateProvider } from './url_state'; const mockHistoryPush = jest.fn(); @@ -22,7 +23,7 @@ jest.mock('react-router-dom', () => ({ describe('getUrlState', () => { test('properly decode url with _g and _a', () => { expect( - getUrlState( + parseUrlState( "?_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFieldName:action),query:(query_string:(analyze_wildcard:!t,query:'*')))&_g=(ml:(jobIds:!(dec-2)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2019-01-01T00:03:40.000Z',mode:absolute,to:'2019-08-30T11:55:07.000Z'))&savedSearchId=571aaf70-4c88-11e8-b3d7-01146121b73d" ) ).toEqual({ @@ -64,13 +65,19 @@ describe('useUrlState', () => { }); test('pushes a properly encoded search string to history', () => { - const { result } = renderHook(() => useUrlState('_a')); + const TestComponent: FC = () => { + const [, setUrlState] = useUrlState('_a'); + return ; + }; + + const { getByText } = render( + + + + ); act(() => { - const [, setUrlState] = result.current; - setUrlState({ - query: {}, - }); + getByText('ButtonText').click(); }); expect(mockHistoryPush).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/ml/public/application/util/url_state.ts b/x-pack/plugins/ml/public/application/util/url_state.tsx similarity index 54% rename from x-pack/plugins/ml/public/application/util/url_state.ts rename to x-pack/plugins/ml/public/application/util/url_state.tsx index beff5340ce7e4..c288a00bb06da 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.ts +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -5,7 +5,7 @@ */ import { parse, stringify } from 'query-string'; -import { useCallback } from 'react'; +import React, { createContext, useCallback, useContext, useMemo, FC } from 'react'; import { isEqual } from 'lodash'; import { decode, encode } from 'rison-node'; import { useHistory, useLocation } from 'react-router-dom'; @@ -14,8 +14,16 @@ import { Dictionary } from '../../../common/types/common'; import { getNestedProperty } from './object_utils'; -export type SetUrlState = (attribute: string | Dictionary, value?: any) => void; -export type UrlState = [Dictionary, SetUrlState]; +type Accessor = '_a' | '_g'; +export type SetUrlState = ( + accessor: Accessor, + attribute: string | Dictionary, + value?: any +) => void; +export interface UrlState { + searchString: string; + setUrlState: SetUrlState; +} /** * Set of URL query parameters that require the rison serialization. @@ -30,7 +38,7 @@ function isRisonSerializationRequired(queryParam: string): boolean { return risonSerializedParams.has(queryParam); } -export function getUrlState(search: string): Dictionary { +export function parseUrlState(search: string): Dictionary { const urlState: Dictionary = {}; const parsedQueryString = parse(search, { sort: false }); @@ -56,14 +64,23 @@ export function getUrlState(search: string): Dictionary { // - `history.push()` is the successor of `save`. // - The exposed state and set call make use of the above and make sure that // different urlStates(e.g. `_a` / `_g`) don't overwrite each other. -export const useUrlState = (accessor: string): UrlState => { +// This uses a context to be able to maintain only one instance +// of the url state. It gets passed down with `UrlStateProvider` +// and can be used via `useUrlState`. +export const urlStateStore = createContext({ + searchString: '', + setUrlState: () => {}, +}); +const { Provider } = urlStateStore; +export const UrlStateProvider: FC = ({ children }) => { const history = useHistory(); - const { search } = useLocation(); + const { search: searchString } = useLocation(); - const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any) => { - const urlState = getUrlState(search); - const parsedQueryString = parse(search, { sort: false }); + const setUrlState: SetUrlState = useCallback( + (accessor: Accessor, attribute: string | Dictionary, value?: any) => { + const prevSearchString = searchString; + const urlState = parseUrlState(prevSearchString); + const parsedQueryString = parse(prevSearchString, { sort: false }); if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) { urlState[accessor] = {}; @@ -71,7 +88,7 @@ export const useUrlState = (accessor: string): UrlState => { if (typeof attribute === 'string') { if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) { - return; + return prevSearchString; } urlState[accessor][attribute] = value; @@ -83,7 +100,10 @@ export const useUrlState = (accessor: string): UrlState => { } try { - const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); + const oldLocationSearchString = stringify(parsedQueryString, { + sort: false, + encode: false, + }); Object.keys(urlState).forEach((a) => { if (isRisonSerializationRequired(a)) { @@ -92,20 +112,41 @@ export const useUrlState = (accessor: string): UrlState => { parsedQueryString[a] = urlState[a]; } }); - const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); + const newLocationSearchString = stringify(parsedQueryString, { + sort: false, + encode: false, + }); - if (oldLocationSearch !== newLocationSearch) { - history.push({ - search: stringify(parsedQueryString, { sort: false }), - }); + if (oldLocationSearchString !== newLocationSearchString) { + const newSearchString = stringify(parsedQueryString, { sort: false }); + history.push({ search: newSearchString }); } } catch (error) { // eslint-disable-next-line no-console console.error('Could not save url state', error); } }, - [search] + [searchString] ); - return [getUrlState(search)[accessor], setUrlState]; + return {children}; +}; + +export const useUrlState = (accessor: Accessor) => { + const { searchString, setUrlState: setUrlStateContext } = useContext(urlStateStore); + + const urlState = useMemo(() => { + const fullUrlState = parseUrlState(searchString); + if (typeof fullUrlState === 'object') { + return fullUrlState[accessor]; + } + return undefined; + }, [searchString]); + + const setUrlState = useCallback( + (attribute: string | Dictionary, value?: any) => + setUrlStateContext(accessor, attribute, value), + [accessor, setUrlStateContext] + ); + return [urlState, setUrlState]; }; diff --git a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index 16eaab20fe8cb..196e17d0984f9 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -70,6 +70,7 @@ export const anomalyDetectionUpdateJobSchema = schema.object({ ), groups: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), model_snapshot_retention_days: schema.maybe(schema.number()), + daily_model_snapshot_retention_after_days: schema.maybe(schema.number()), }); export const analysisConfigSchema = schema.object({ diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 21a9fabf445f1..5bc8d96656ed4 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -3,23 +3,64 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import { createHashHistory } from 'history'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; -import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; +import { Route, Router, Switch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; -import { Home } from '../pages/home'; +import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; import { PluginContext } from '../context/plugin_context'; +import { useUrlParams } from '../hooks/use_url_params'; +import { routes } from '../routes'; +import { usePluginContext } from '../hooks/use_plugin_context'; + +const App = () => { + return ( + <> + + {Object.keys(routes).map((key) => { + const path = key as keyof typeof routes; + const route = routes[path]; + const Wrapper = () => { + const { core } = usePluginContext(); + useEffect(() => { + core.chrome.setBreadcrumbs([ + { + text: i18n.translate('xpack.observability.observability.breadcrumb.', { + defaultMessage: 'Observability', + }), + }, + ...route.breadcrumb, + ]); + }, [core]); + + const { query, path: pathParams } = useUrlParams(route.params); + return route.handler({ query, path: pathParams }); + }; + return ; + })} + + + ); +}; export const renderApp = (core: CoreStart, { element }: AppMountParameters) => { const i18nCore = core.i18n; const isDarkMode = core.uiSettings.get('theme:darkMode'); + const history = createHashHistory(); ReactDOM.render( - - - - - + + + + + + + + + , element ); diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx new file mode 100644 index 0000000000000..d09d535a49340 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { ChartContainer } from './'; + +describe('chart container', () => { + it('shows loading indicator', () => { + const component = render( + +
My amazing component
+
+ ); + expect(component.getByTestId('loading')).toBeInTheDocument(); + expect(component.queryByText('My amazing component')).not.toBeInTheDocument(); + }); + it("doesn't show loading indicator", () => { + const component = render( + +
My amazing component
+
+ ); + expect(component.queryByTestId('loading')).not.toBeInTheDocument(); + expect(component.getByText('My amazing component')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/chart_container/index.tsx b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx new file mode 100644 index 0000000000000..2a0c25773eae5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/chart_container/index.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Chart } from '@elastic/charts'; +import { EuiLoadingChart } from '@elastic/eui'; +import { EuiLoadingChartSize } from '@elastic/eui/src/components/loading/loading_chart'; +import React from 'react'; + +interface Props { + isInitialLoad: boolean; + height?: number; + width?: number; + iconSize?: EuiLoadingChartSize; + children: React.ReactNode; +} + +const CHART_HEIGHT = 170; + +export const ChartContainer = ({ + isInitialLoad, + children, + iconSize = 'xl', + height = CHART_HEIGHT, +}: Props) => { + if (isInitialLoad) { + return ( +
+ +
+ ); + } + return {children}; +}; diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx new file mode 100644 index 0000000000000..e04e8f050006a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ISection } from '../../../typings/section'; +import { render } from '../../../utils/test_helper'; +import { EmptySection } from './'; + +describe('EmptySection', () => { + it('renders without action button', () => { + const section: ISection = { + id: 'apm', + title: 'APM', + icon: 'logoAPM', + description: 'foo bar', + }; + const { getByText, queryAllByText } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('foo bar')).toBeInTheDocument(); + expect(queryAllByText('Install agent')).toEqual([]); + }); + it('renders with action button', () => { + const section: ISection = { + id: 'apm', + title: 'APM', + icon: 'logoAPM', + description: 'foo bar', + linkTitle: 'install agent', + href: 'https://www.elastic.co', + }; + const { getByText, getByTestId } = render(); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('foo bar')).toBeInTheDocument(); + const linkButton = getByTestId('empty-apm') as HTMLAnchorElement; + expect(linkButton.href).toEqual('https://www.elastic.co/'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/empty_section/index.tsx b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx new file mode 100644 index 0000000000000..e19bf1678bc01 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/empty_section/index.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiButton, EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import React from 'react'; +import { ISection } from '../../../typings/section'; + +interface Props { + section: ISection; +} + +export const EmptySection = ({ section }: Props) => { + return ( + {section.title}} + titleSize="xs" + body={{section.description}} + actions={ + <> + {section.linkTitle && ( + + {section.linkTitle} + + )} + + } + /> + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/header/index.test.tsx b/x-pack/plugins/observability/public/components/app/header/index.test.tsx new file mode 100644 index 0000000000000..59b6fbe9caf7a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/header/index.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render } from '../../../utils/test_helper'; +import { Header } from './'; + +describe('Header', () => { + it('renders without add data button', () => { + const { getByText, queryAllByText, getByTestId } = render(
); + expect(getByTestId('observability-logo')).toBeInTheDocument(); + expect(getByText('Observability')).toBeInTheDocument(); + expect(queryAllByText('Add data')).toEqual([]); + }); + it('renders with add data button', () => { + const { getByText, getByTestId } = render(
); + expect(getByTestId('observability-logo')).toBeInTheDocument(); + expect(getByText('Observability')).toBeInTheDocument(); + expect(getByText('Add data')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx new file mode 100644 index 0000000000000..1c6ce766d0901 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiBetaBadge, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +const Container = styled.div<{ color: string }>` + background: ${(props) => props.color}; + border-bottom: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const Wrapper = styled.div<{ restrictWidth?: number }>` + width: 100%; + max-width: ${(props) => `${props.restrictWidth}px`}; + margin: 0 auto; + overflow: hidden; + padding: ${(props) => (props.restrictWidth ? 0 : '0 24px')}; +`; + +interface Props { + color: string; + showAddData?: boolean; + restrictWidth?: number; + showGiveFeedback?: boolean; +} + +export const Header = ({ + color, + restrictWidth, + showAddData = false, + showGiveFeedback = false, +}: Props) => { + const { core } = usePluginContext(); + return ( + + + + + + + + + +

+ {i18n.translate('xpack.observability.home.title', { + defaultMessage: 'Observability', + })}{' '} + +

+
+
+ {showGiveFeedback && ( + + + {i18n.translate('xpack.observability.home.feedback', { + defaultMessage: 'Give us feedback', + })} + + + )} + {showAddData && ( + + + {i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })} + + + )} +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx new file mode 100644 index 0000000000000..27b25f0056055 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { Header } from '../header/index'; + +const getPaddingSize = (props: EuiPageProps) => (props.restrictWidth ? 0 : '24px'); + +const Page = styled(EuiPage)` + background: transparent; + padding-right: ${getPaddingSize}; + padding-left: ${getPaddingSize}; +`; + +const Container = styled.div<{ color?: string }>` + overflow-y: hidden; + min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); + background: ${(props) => props.color}; +`; + +interface Props { + headerColor: string; + bodyColor: string; + children?: React.ReactNode; + restrictWidth?: number; + showAddData?: boolean; + showGiveFeedback?: boolean; +} + +export const WithHeaderLayout = ({ + headerColor, + bodyColor, + children, + restrictWidth, + showAddData, + showGiveFeedback, +}: Props) => ( + +
+ + {children} + + +); diff --git a/x-pack/plugins/observability/public/components/app/news/index.scss b/x-pack/plugins/observability/public/components/app/news/index.scss new file mode 100644 index 0000000000000..1222fe489c732 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news/index.scss @@ -0,0 +1,3 @@ +.obsNewsFeed__itemImg{ + @include euiBottomShadowSmall; +} \ No newline at end of file diff --git a/x-pack/plugins/observability/public/components/app/news/index.test.tsx b/x-pack/plugins/observability/public/components/app/news/index.test.tsx new file mode 100644 index 0000000000000..cae6b4aec0c62 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news/index.test.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render } from '../../../utils/test_helper'; +import { News } from './'; + +describe('News', () => { + it('renders resources with all elements', () => { + const { getByText, getAllByText } = render(); + expect(getByText("What's new")).toBeInTheDocument(); + expect(getAllByText('Read full story')).not.toEqual([]); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/news/index.tsx b/x-pack/plugins/observability/public/components/app/news/index.tsx new file mode 100644 index 0000000000000..41a4074f47976 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news/index.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import './index.scss'; +import { truncate } from 'lodash'; +import { news as newsMockData } from './mock/news.mock.data'; + +interface NewsItem { + title: string; + description: string; + link_url: string; + image_url: string; +} + +export const News = () => { + const newsItems: NewsItem[] = newsMockData; + return ( + + + +

+ {i18n.translate('xpack.observability.news.title', { + defaultMessage: "What's new", + })} +

+
+
+ {newsItems.map((item, index) => ( + + + + ))} +
+ ); +}; + +const limitString = (string: string, limit: number) => truncate(string, { length: limit }); + +const NewsItem = ({ item }: { item: NewsItem }) => { + const theme = useContext(ThemeContext); + + return ( + + + +

{item.title}

+
+
+ + + + + + + {limitString(item.description, 128)} + + + + + + {i18n.translate('xpack.observability.news.readFullStory', { + defaultMessage: 'Read full story', + })} + + + + + + + {item.title} + + + + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts b/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts new file mode 100644 index 0000000000000..5c623bb9134eb --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/news/mock/news.mock.data.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const news = [ + { + title: 'Have SIEM questions?', + description: + 'Join our growing community of Elastic SIEM users to discuss the configuration and use of Elastic SIEM for threat detection and response.', + link_url: 'https://discuss.elastic.co/c/security/siem/?blade=securitysolutionfeed', + image_url: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + }, + { + title: 'Elastic SIEM on-demand training course — free for a limited time', + description: + 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.', + link_url: + 'https://training.elastic.co/elearning/security-analytics/elastic-siem-fundamentals-promo?blade=securitysolutionfeed', + image_url: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed', + }, + { + title: 'New to Elastic SIEM? Take our on-demand training course', + description: + 'With this self-paced, on-demand course, you will learn how to leverage Elastic SIEM to drive your security operations and threat hunting. This course is designed for security analysts and practitioners who have used other SIEMs or are familiar with SIEM concepts.', + link_url: + 'https://www.elastic.co/training/specializations/security-analytics/elastic-siem-fundamentals?blade=securitysolutionfeed', + image_url: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt50f58e0358ebea9d/5c30508693d9791a70cd73ad/illustration-specialization-course-page-security.svg?blade=securitysolutionfeed', + }, +]; diff --git a/x-pack/plugins/observability/public/components/app/resources/index.test.tsx b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx new file mode 100644 index 0000000000000..570aa3954424f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/resources/index.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { render } from '@testing-library/react'; +import React from 'react'; +import { Resources } from './'; + +describe('Resources', () => { + it('renders resources with all elements', () => { + const { getByText } = render(); + expect(getByText('Documentation')).toBeInTheDocument(); + expect(getByText('Discuss forum')).toBeInTheDocument(); + expect(getByText('Observability fundamentals')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/resources/index.tsx b/x-pack/plugins/observability/public/components/app/resources/index.tsx new file mode 100644 index 0000000000000..c330c358d022a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/resources/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +const resources = [ + { + iconType: 'documents', + label: i18n.translate('xpack.observability.resources.documentation', { + defaultMessage: 'Documentation', + }), + href: 'https://www.elastic.co/guide/en/observability/current/observability-ui.html', + }, + { + iconType: 'editorComment', + label: i18n.translate('xpack.observability.resources.forum', { + defaultMessage: 'Discuss forum', + }), + href: 'https://discuss.elastic.co/c/observability/', + }, + { + iconType: 'training', + label: i18n.translate('xpack.observability.resources.training', { + defaultMessage: 'Observability fundamentals', + }), + href: 'https://www.elastic.co/training/observability-fundamentals', + }, +]; + +export const Resources = () => { + return ( + + + +

+ {i18n.translate('xpack.observability.resources.title', { + defaultMessage: 'Resources', + })} +

+
+
+ +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx new file mode 100644 index 0000000000000..4c80195d33ace --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIconTip, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useState } from 'react'; +import { EuiSelect } from '@elastic/eui'; +import { uniqBy } from 'lodash'; +import { Alert } from '../../../../../../alerts/common'; +import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { SectionContainer } from '..'; + +const ALL_TYPES = 'ALL_TYPES'; +const allTypes = { + value: ALL_TYPES, + text: i18n.translate('xpack.observability.overview.alert.allTypes', { + defaultMessage: 'All types', + }), +}; + +interface Props { + alerts: Alert[]; +} + +export const AlertsSection = ({ alerts }: Props) => { + const { core } = usePluginContext(); + const [filter, setFilter] = useState(ALL_TYPES); + + const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({ + value: consumer, + text: consumer, + })); + + return ( + + + + + + setFilter(e.target.value)} + prepend={i18n.translate('xpack.observability.overview.alert.view', { + defaultMessage: 'View', + })} + /> + + + + + + {alerts + .filter((alert) => filter === ALL_TYPES || alert.consumer === filter) + .map((alert, index) => { + const isLastElement = index === alerts.length - 1; + return ( + + + + {alert.name} + + + + + + {alert.alertTypeId} + + {alert.tags.map((tag, idx) => { + return ( + + {tag} + + ); + })} + + + + + + + Updated {moment.duration(moment().diff(alert.updatedAt)).humanize()} ago + + + {alert.muteAll && ( + + + + )} + + + {!isLastElement && } + + ); + })} + + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx new file mode 100644 index 0000000000000..d4b8236e0ef49 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import * as fetcherHook from '../../../../hooks/use_fetcher'; +import { render } from '../../../../utils/test_helper'; +import { APMSection } from './'; +import { response } from './mock_data/apm.mock'; + +describe('APMSection', () => { + it('renders with transaction series and stats', () => { + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: response, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }); + const { getByText, queryAllByTestId } = render( + + ); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByText('View in app')).toBeInTheDocument(); + expect(getByText('Services 11')).toBeInTheDocument(); + expect(getByText('Transactions per minute 312.00k')).toBeInTheDocument(); + expect(queryAllByTestId('loading')).toEqual([]); + }); + it('shows loading state', () => { + jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({ + data: undefined, + status: fetcherHook.FETCH_STATUS.LOADING, + refetch: jest.fn(), + }); + const { getByText, queryAllByText, getByTestId } = render( + + ); + + expect(getByText('APM')).toBeInTheDocument(); + expect(getByTestId('loading')).toBeInTheDocument(); + expect(queryAllByText('View in app')).toEqual([]); + expect(queryAllByText('Services 11')).toEqual([]); + expect(queryAllByText('Transactions per minute 312.00k')).toEqual([]); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx new file mode 100644 index 0000000000000..697d4adfa0b75 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +function formatTpm(value?: number) { + return numeral(value).format('0.00a'); +} + +export const APMSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('apm')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const { title = 'APM', appLink, stats, series } = data || {}; + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + + const formatter = niceTimeFormatter([min, max]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const transactionsColor = theme.eui.euiColorVis1; + + return ( + + + + + + + + + + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend={false} + xDomain={{ min, max }} + /> + {series?.transactions.coordinates && ( + <> + + `${formatTpm(value)} tpm`} + /> + + + )} + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts new file mode 100644 index 0000000000000..5857021b1537f --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/apm/mock_data/apm.mock.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ApmFetchDataResponse } from '../../../../../typings'; + +export const response: ApmFetchDataResponse = { + title: 'APM', + + appLink: '/app/apm', + stats: { + services: { value: 11, type: 'number' }, + transactions: { value: 312000, type: 'number' }, + }, + series: { + transactions: { + coordinates: [ + { x: 1591365600000, y: 32 }, + { x: 1591366200000, y: 43 }, + { x: 1591366800000, y: 22 }, + { x: 1591367400000, y: 29 }, + { x: 1591368000000, y: 39 }, + { x: 1591368600000, y: 36 }, + { x: 1591369200000, y: 50 }, + { x: 1591369800000, y: 31 }, + { x: 1591370400000, y: 39 }, + { x: 1591371000000, y: 26 }, + { x: 1591371600000, y: 45 }, + { x: 1591372200000, y: 27 }, + { x: 1591372800000, y: 37 }, + { x: 1591373400000, y: 55 }, + { x: 1591374000000, y: 31 }, + { x: 1591374600000, y: 26 }, + { x: 1591375200000, y: 57 }, + { x: 1591375800000, y: 25 }, + { x: 1591376400000, y: 28 }, + { x: 1591377000000, y: 40 }, + { x: 1591377600000, y: 33 }, + { x: 1591378200000, y: 33 }, + { x: 1591378800000, y: 31 }, + { x: 1591379400000, y: 32 }, + { x: 1591380000000, y: 34 }, + { x: 1591380600000, y: 31 }, + { x: 1591381200000, y: 16 }, + { x: 1591381800000, y: 34 }, + { x: 1591382400000, y: 33 }, + { x: 1591383000000, y: 35 }, + { x: 1591383600000, y: 47 }, + { x: 1591384200000, y: 44 }, + { x: 1591384800000, y: 21 }, + { x: 1591385400000, y: 25 }, + { x: 1591386000000, y: 34 }, + { x: 1591386600000, y: 37 }, + { x: 1591387200000, y: 38 }, + { x: 1591387800000, y: 28 }, + { x: 1591388400000, y: 32 }, + { x: 1591389000000, y: 37 }, + { x: 1591389600000, y: 25 }, + { x: 1591390200000, y: 33 }, + { x: 1591390800000, y: 34 }, + { x: 1591391400000, y: 30 }, + { x: 1591392000000, y: 45 }, + { x: 1591392600000, y: 42 }, + { x: 1591393200000, y: 23 }, + { x: 1591393800000, y: 33 }, + { x: 1591394400000, y: 38 }, + { x: 1591395000000, y: 30 }, + { x: 1591395600000, y: 25 }, + { x: 1591396200000, y: 33 }, + { x: 1591396800000, y: 37 }, + { x: 1591397400000, y: 43 }, + { x: 1591398000000, y: 30 }, + { x: 1591398600000, y: 36 }, + { x: 1591399200000, y: 28 }, + { x: 1591399800000, y: 39 }, + { x: 1591400400000, y: 27 }, + { x: 1591401000000, y: 41 }, + { x: 1591401600000, y: 25 }, + { x: 1591402200000, y: 31 }, + { x: 1591402800000, y: 28 }, + { x: 1591403400000, y: 29 }, + { x: 1591404000000, y: 49 }, + { x: 1591404600000, y: 24 }, + { x: 1591405200000, y: 41 }, + { x: 1591405800000, y: 30 }, + { x: 1591406400000, y: 36 }, + { x: 1591407000000, y: 39 }, + { x: 1591407600000, y: 23 }, + { x: 1591408200000, y: 40 }, + { x: 1591408800000, y: 34 }, + { x: 1591409400000, y: 28 }, + { x: 1591410000000, y: 33 }, + { x: 1591410600000, y: 31 }, + { x: 1591411200000, y: 39 }, + { x: 1591411800000, y: 33 }, + { x: 1591412400000, y: 35 }, + { x: 1591413000000, y: 31 }, + { x: 1591413600000, y: 35 }, + { x: 1591414200000, y: 37 }, + { x: 1591414800000, y: 26 }, + { x: 1591415400000, y: 27 }, + { x: 1591416000000, y: 26 }, + { x: 1591416600000, y: 34 }, + { x: 1591417200000, y: 33 }, + { x: 1591417800000, y: 38 }, + { x: 1591418400000, y: 34 }, + { x: 1591419000000, y: 37 }, + { x: 1591419600000, y: 24 }, + { x: 1591420200000, y: 25 }, + { x: 1591420800000, y: 20 }, + { x: 1591421400000, y: 35 }, + { x: 1591422000000, y: 41 }, + { x: 1591422600000, y: 40 }, + { x: 1591423200000, y: 33 }, + { x: 1591423800000, y: 24 }, + { x: 1591424400000, y: 44 }, + { x: 1591425000000, y: 24 }, + { x: 1591425600000, y: 32 }, + { x: 1591426200000, y: 37 }, + { x: 1591426800000, y: 34 }, + { x: 1591427400000, y: 28 }, + { x: 1591428000000, y: 26 }, + { x: 1591428600000, y: 37 }, + { x: 1591429200000, y: 36 }, + { x: 1591429800000, y: 37 }, + { x: 1591430400000, y: 23 }, + { x: 1591431000000, y: 47 }, + { x: 1591431600000, y: 41 }, + { x: 1591432200000, y: 24 }, + { x: 1591432800000, y: 34 }, + { x: 1591433400000, y: 27 }, + { x: 1591434000000, y: 34 }, + { x: 1591434600000, y: 44 }, + { x: 1591435200000, y: 20 }, + { x: 1591435800000, y: 34 }, + { x: 1591436400000, y: 29 }, + { x: 1591437000000, y: 28 }, + { x: 1591437600000, y: 36 }, + { x: 1591438200000, y: 34 }, + { x: 1591438800000, y: 26 }, + { x: 1591439400000, y: 29 }, + { x: 1591440000000, y: 45 }, + { x: 1591440600000, y: 34 }, + { x: 1591441200000, y: 25 }, + { x: 1591441800000, y: 34 }, + { x: 1591442400000, y: 28 }, + { x: 1591443000000, y: 34 }, + { x: 1591443600000, y: 31 }, + { x: 1591444200000, y: 24 }, + { x: 1591444800000, y: 34 }, + { x: 1591445400000, y: 21 }, + { x: 1591446000000, y: 40 }, + { x: 1591446600000, y: 37 }, + { x: 1591447200000, y: 31 }, + { x: 1591447800000, y: 21 }, + { x: 1591448400000, y: 24 }, + { x: 1591449000000, y: 30 }, + { x: 1591449600000, y: 22 }, + { x: 1591450200000, y: 27 }, + { x: 1591450800000, y: 30 }, + { x: 1591451400000, y: 22 }, + { x: 1591452000000, y: 9 }, + ], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx new file mode 100644 index 0000000000000..8f0781b8f0269 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/error_panel/index.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const ErrorPanel = () => { + return ( + + + + {i18n.translate('xpack.observability.section.errorPanel', { + defaultMessage: 'An error happened when trying to fetch data. Please try again', + })} + + + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/helper.test.ts b/x-pack/plugins/observability/public/components/app/section/helper.test.ts new file mode 100644 index 0000000000000..6a8cd27753a8d --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/helper.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { onBrushEnd } from './helper'; +import { History } from 'history'; + +describe('Chart helper', () => { + describe('onBrushEnd', () => { + const history = ({ + push: jest.fn(), + location: { + search: '', + }, + } as unknown) as History; + it("doesn't push a new history when x is not defined", () => { + onBrushEnd({ x: undefined, history }); + expect(history.push).not.toBeCalled(); + }); + + it('pushes a new history with time range converted to ISO', () => { + onBrushEnd({ x: [1593409448167, 1593415727797], history }); + expect(history.push).toBeCalledWith({ + search: 'rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z', + }); + }); + + it('pushes a new history keeping current search', () => { + history.location.search = '?foo=bar'; + onBrushEnd({ x: [1593409448167, 1593415727797], history }); + expect(history.push).toBeCalledWith({ + search: 'foo=bar&rangeFrom=2020-06-29T05:44:08.167Z&rangeTo=2020-06-29T07:28:47.797Z', + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/helper.ts b/x-pack/plugins/observability/public/components/app/section/helper.ts new file mode 100644 index 0000000000000..81fa92cb87782 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/helper.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { XYBrushArea } from '@elastic/charts'; +import { History } from 'history'; +import { fromQuery, toQuery } from '../../../utils/url'; + +export const onBrushEnd = ({ x, history }: { x: XYBrushArea['x']; history: History }) => { + if (x) { + const start = x[0]; + const end = x[1]; + + const currentSearch = toQuery(history.location.search); + const nextSearch = { + rangeFrom: new Date(start).toISOString(), + rangeTo: new Date(end).toISOString(), + }; + history.push({ + ...history.location, + search: fromQuery({ + ...currentSearch, + ...nextSearch, + }), + }); + } +}; diff --git a/x-pack/plugins/observability/public/components/app/section/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/index.test.tsx new file mode 100644 index 0000000000000..49cb175d0c094 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/index.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render } from '../../../utils/test_helper'; +import { SectionContainer } from './'; + +describe('SectionContainer', () => { + it('renders section without app link', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.getByText('I am a very nice component')).toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect(component.queryAllByText('View in app')).toEqual([]); + }); + it('renders section with app link', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.getByText('I am a very nice component')).toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect(component.getByText('View in app')).toBeInTheDocument(); + }); + it('renders section with error', () => { + const component = render( + +
I am a very nice component
+
+ ); + expect(component.queryByText('I am a very nice component')).not.toBeInTheDocument(); + expect(component.getByText('Foo')).toBeInTheDocument(); + expect( + component.getByText('An error happened when trying to fetch data. Please try again') + ).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx new file mode 100644 index 0000000000000..3556e8c01ab30 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/index.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { ErrorPanel } from './error_panel'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +interface Props { + title: string; + hasError: boolean; + children: React.ReactNode; + minHeight?: number; + appLink?: string; + appLinkName?: string; +} + +export const SectionContainer = ({ title, appLink, children, hasError, appLinkName }: Props) => { + const { core } = usePluginContext(); + return ( + +
{title}
+ + } + extraAction={ + appLink && ( + + + {appLinkName + ? appLinkName + : i18n.translate('xpack.observability.chart.viewInAppLabel', { + defaultMessage: 'View in app', + })} + + + ) + } + > + <> + + + {hasError ? ( + + ) : ( + <> + + {children} + + )} + + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx new file mode 100644 index 0000000000000..f3ba2ef6fa83a --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { isEmpty } from 'lodash'; +import moment from 'moment'; +import React, { Fragment } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { LogsFetchDataResponse } from '../../../../typings'; +import { formatStatValue } from '../../../../utils/format_stat_value'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +function getColorPerItem(series?: LogsFetchDataResponse['series']) { + if (!series) { + return {}; + } + const availableColors = euiPaletteColorBlind({ + rotations: Math.ceil(Object.keys(series).length / 10), + }); + const colorsPerItem = Object.keys(series).reduce((acc: Record, key, index) => { + acc[key] = availableColors[index]; + return acc; + }, {}); + + return colorsPerItem; +} + +export const LogsSection = ({ startTime, endTime, bucketSize }: Props) => { + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('infra_logs')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + + const formatter = niceTimeFormatter([min, max]); + + const { title, appLink, stats, series } = data || {}; + + const colorsPerItem = getColorPerItem(series); + + const isLoading = status === FETCH_STATUS.LOADING; + + return ( + + +

+ {i18n.translate('xpack.observability.overview.logs.subtitle', { + defaultMessage: 'Logs rate per minute', + })} +

+
+ + + {!stats || isEmpty(stats) ? ( + + + + ) : ( + Object.keys(stats).map((key) => { + const stat = stats[key]; + return ( + + + + ); + }) + )} + + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend + legendPosition={Position.Right} + xDomain={{ min, max }} + showLegendExtra + /> + {series && + Object.keys(series).map((key) => { + const serie = series[key]; + const chartData = serie.coordinates.map((coordinate) => ({ + ...coordinate, + g: serie.label, + })); + return ( + + + + numeral(d).format('0a')} + /> + + ); + })} + +
+ ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx new file mode 100644 index 0000000000000..6276e1ba1baca --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/metrics/index.tsx @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AreaSeries, ScaleType, Settings } from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { Series } from '../../../../typings'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +/** + * EuiProgress doesn't support custom color, when it does this component can be removed. + */ +const StyledProgress = styled.div<{ color?: string }>` + progress { + &.euiProgress--native { + &::-webkit-progress-value { + background-color: ${(props) => props.color}; + } + + &::-moz-progress-bar { + background-color: ${(props) => props.color}; + } + } + + &.euiProgress--indeterminate { + &:before { + background-color: ${(props) => props.color}; + } + } + } +`; + +export const MetricsSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('infra_metrics')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const { title = 'Metrics', appLink, stats, series } = data || {}; + + const cpuColor = theme.eui.euiColorVis7; + const memoryColor = theme.eui.euiColorVis0; + const inboundTrafficColor = theme.eui.euiColorVis3; + const outboundTrafficColor = theme.eui.euiColorVis2; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const AreaChart = ({ + serie, + isLoading, + color, +}: { + serie?: Series; + isLoading: boolean; + color: string; +}) => { + const chartTheme = useChartTheme(); + + return ( + + + {serie && ( + + )} + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx new file mode 100644 index 0000000000000..1f8ca6e61f132 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Axis, + BarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + TickFormatter, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ThemeContext } from 'styled-components'; +import { SectionContainer } from '../'; +import { getDataHandler } from '../../../../data_handler'; +import { useChartTheme } from '../../../../hooks/use_chart_theme'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { Series } from '../../../../typings'; +import { ChartContainer } from '../../chart_container'; +import { StyledStat } from '../../styled_stat'; +import { onBrushEnd } from '../helper'; + +interface Props { + startTime?: string; + endTime?: string; + bucketSize?: string; +} + +export const UptimeSection = ({ startTime, endTime, bucketSize }: Props) => { + const theme = useContext(ThemeContext); + const history = useHistory(); + + const { data, status } = useFetcher(() => { + if (startTime && endTime && bucketSize) { + return getDataHandler('uptime')?.fetchData({ startTime, endTime, bucketSize }); + } + }, [startTime, endTime, bucketSize]); + + const min = moment.utc(startTime).valueOf(); + const max = moment.utc(endTime).valueOf(); + const formatter = niceTimeFormatter([min, max]); + + const isLoading = status === FETCH_STATUS.LOADING; + + const { title = 'Uptime', appLink, stats, series } = data || {}; + + const downColor = theme.eui.euiColorVis2; + const upColor = theme.eui.euiColorLightShade; + + return ( + + + {/* Stats section */} + + + + + + + + + + + + {/* Chart section */} + + onBrushEnd({ x, history })} + theme={useChartTheme()} + showLegend={false} + legendPosition={Position.Right} + xDomain={{ min, max }} + /> + + + + + ); +}; + +const UptimeBarSeries = ({ + id, + label, + series, + color, + ticktFormatter, +}: { + id: string; + label: string; + series?: Series; + color: string; + ticktFormatter: TickFormatter; +}) => { + if (!series) { + return null; + } + const chartData = series.coordinates.map((coordinate) => ({ + ...coordinate, + g: label, + })); + return ( + <> + + + numeral(x).format('0a')} + /> + + ); +}; diff --git a/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx new file mode 100644 index 0000000000000..fe38df6484c29 --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/styled_stat/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import styled from 'styled-components'; +import { EuiStat } from '@elastic/eui'; +import React from 'react'; +import { EuiStatProps } from '@elastic/eui/src/components/stat/stat'; + +const Stat = styled(EuiStat)` + .euiStat__title { + color: ${(props) => props.color}; + } +`; + +interface Props extends Partial { + children?: React.ReactNode; + color?: string; +} + +const EMPTY_VALUE = '--'; + +export const StyledStat = (props: Props) => { + const { description = EMPTY_VALUE, title = EMPTY_VALUE, ...rest } = props; + return ; +}; diff --git a/x-pack/plugins/observability/public/components/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/action_menu/index.tsx similarity index 100% rename from x-pack/plugins/observability/public/components/action_menu.tsx rename to x-pack/plugins/observability/public/components/shared/action_menu/index.tsx diff --git a/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx new file mode 100644 index 0000000000000..cc77c1ed72b4a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/data_picker/index.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSuperDatePicker } from '@elastic/eui'; +import React from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { UI_SETTINGS, useKibanaUISettings } from '../../../hooks/use_kibana_ui_settings'; +import { fromQuery, toQuery } from '../../../utils/url'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +export interface TimePickerRefreshInterval { + pause: boolean; + value: number; +} + +interface Props { + rangeFrom: string; + rangeTo: string; + refreshPaused: boolean; + refreshInterval: number; +} + +export const DatePicker = ({ rangeFrom, rangeTo, refreshPaused, refreshInterval }: Props) => { + const location = useLocation(); + const history = useHistory(); + + const timePickerQuickRanges = useKibanaUISettings( + UI_SETTINGS.TIMEPICKER_QUICK_RANGES + ); + + const commonlyUsedRanges = timePickerQuickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + + function updateUrl(nextQuery: { + rangeFrom?: string; + rangeTo?: string; + refreshPaused?: boolean; + refreshInterval?: number; + }) { + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + ...nextQuery, + }), + }); + } + + function onRefreshChange({ + isPaused, + refreshInterval: interval, + }: { + isPaused: boolean; + refreshInterval: number; + }) { + updateUrl({ refreshPaused: isPaused, refreshInterval: interval }); + } + + function onTimeChange({ start, end }: { start: string; end: string }) { + updateUrl({ rangeFrom: start, rangeTo: end }); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 39e702a332a8e..d7f8c471ad9aa 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -17,9 +17,20 @@ export function registerDataHandler({ dataHandlers[appName] = { fetchData, hasData }; } +export function unregisterDataHandler({ appName }: { appName: T }) { + delete dataHandlers[appName]; +} + export function getDataHandler(appName: T) { const dataHandler = dataHandlers[appName]; if (dataHandler) { return dataHandler as DataHandler; } } + +export async function fetchHasData() { + const apps: ObservabilityApp[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics']; + const promises = apps.map((app) => getDataHandler(app)?.hasData()); + const [apm, uptime, logs, metrics] = await Promise.all(promises); + return { apm, uptime, infra_logs: logs, infra_metrics: metrics }; +} diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx new file mode 100644 index 0000000000000..13f7159ba6043 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; + +export function useChartTheme() { + const theme = useContext(ThemeContext); + return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; +} diff --git a/x-pack/plugins/observability/public/hooks/use_fetcher.tsx b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx new file mode 100644 index 0000000000000..88a8ad264e737 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_fetcher.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, useMemo } from 'react'; + +export enum FETCH_STATUS { + LOADING = 'loading', + SUCCESS = 'success', + FAILURE = 'failure', + PENDING = 'pending', +} + +export interface FetcherResult { + data?: Data; + status: FETCH_STATUS; + error?: Error; +} + +// fetcher functions can return undefined OR a promise. Previously we had a more simple type +// but it led to issues when using object destructuring with default values +type InferResponseType = Exclude extends Promise + ? TResponseType + : unknown; + +export function useFetcher( + fn: () => TReturn, + fnDeps: any[], + options: { + preservePreviousData?: boolean; + } = {} +): FetcherResult> & { refetch: () => void } { + const { preservePreviousData = true } = options; + + const [result, setResult] = useState>>({ + data: undefined, + status: FETCH_STATUS.PENDING, + }); + const [counter, setCounter] = useState(0); + useEffect(() => { + async function doFetch() { + const promise = fn(); + if (!promise) { + return; + } + + setResult((prevResult) => ({ + data: preservePreviousData ? prevResult.data : undefined, + status: FETCH_STATUS.LOADING, + error: undefined, + })); + + try { + const data = await promise; + setResult({ + data, + status: FETCH_STATUS.SUCCESS, + error: undefined, + } as FetcherResult>); + } catch (e) { + setResult((prevResult) => ({ + data: preservePreviousData ? prevResult.data : undefined, + status: FETCH_STATUS.FAILURE, + error: e, + })); + } + } + + doFetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [counter, ...fnDeps]); + + return useMemo(() => { + return { + ...result, + refetch: () => { + // this will invalidate the deps to `useEffect` and will result in a new request + setCounter((count) => count + 1); + }, + }; + }, [result]); +} diff --git a/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx new file mode 100644 index 0000000000000..884d74db391ee --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_kibana_ui_settings.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { usePluginContext } from './use_plugin_context'; +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; + +export { UI_SETTINGS }; + +type SettingKeys = keyof typeof UI_SETTINGS; +type SettingValues = typeof UI_SETTINGS[SettingKeys]; + +export function useKibanaUISettings(key: SettingValues): T { + const { core } = usePluginContext(); + return core.uiSettings.get(key); +} diff --git a/x-pack/plugins/observability/public/hooks/use_url_params.tsx b/x-pack/plugins/observability/public/hooks/use_url_params.tsx new file mode 100644 index 0000000000000..680a32fb49677 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_url_params.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { useLocation, useParams } from 'react-router-dom'; +import { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { Params } from '../routes'; + +function getQueryParams(location: ReturnType) { + const urlSearchParms = new URLSearchParams(location.search); + const queryParams: Record = {}; + urlSearchParms.forEach((value, key) => { + queryParams[key] = value; + }); + return queryParams; +} + +/** + * Extracts query and path params from the url and validate it against the type defined in the route file. + * It removes any aditional item which is not declared in the type. + * @param params + */ +export function useUrlParams(params: Params) { + const location = useLocation(); + const pathParams = useParams(); + const queryParams = getQueryParams(location); + + const rts = { + queryRt: params.query ? t.exact(params.query) : t.strict({}), + pathRt: params.path ? t.exact(params.path) : t.strict({}), + }; + + const queryResult = rts.queryRt.decode(queryParams); + const pathResult = rts.pathRt.decode(pathParams); + if (isLeft(queryResult)) { + // eslint-disable-next-line no-console + console.error(PathReporter.report(queryResult)[0]); + } + + if (isLeft(pathResult)) { + // eslint-disable-next-line no-console + console.error(PathReporter.report(pathResult)[0]); + } + + return { + query: isLeft(queryResult) ? {} : queryResult.right, + path: isLeft(pathResult) ? {} : pathResult.right, + }; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index d2f1d246f79ec..03939736b64ae 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -15,7 +15,7 @@ export const plugin: PluginInitializer props.theme.eui.euiColorEmptyShade}; -`; - -const Title = styled.div` - background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; - border-bottom: ${(props) => props.theme.eui.euiBorderThin}; -`; - -const Page = styled.div` - width: 100%; - max-width: 1200px; - margin: 0 auto; - overflow: hidden; -} -`; - -const EuiCardWithoutPadding = styled(EuiCard)` - padding: 0; -`; - -export const Home = () => { - const { core } = usePluginContext(); - - useEffect(() => { - core.chrome.setBreadcrumbs([ - { - text: i18n.translate('xpack.observability.home.breadcrumb.observability', { - defaultMessage: 'Observability', - }), - }, - { - text: i18n.translate('xpack.observability.home.breadcrumb.gettingStarted', { - defaultMessage: 'Getting started', - }), - }, - ]); - }, [core]); - - return ( - - - <Page> - <EuiSpacer size="xxl" /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiIcon type="logoObservability" size="xxl" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size="m"> - <h1> - {i18n.translate('xpack.observability.home.title', { - defaultMessage: 'Observability', - })} - </h1> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xxl" /> - </Page> - - - - - {/* title and description */} - - -

- {i18n.translate('xpack.observability.home.sectionTitle', { - defaultMessage: 'Unified visibility across your entire ecosystem', - })} -

-
- - - {i18n.translate('xpack.observability.home.sectionsubtitle', { - defaultMessage: - 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.', - })} - -
- - {/* Apps sections */} - - - - - - {appsSection.map((app) => ( - - } - title={ - -

{app.title}

-
- } - description={app.description} - /> -
- ))} -
-
- - - -
-
- - {/* Get started button */} - - - - - {i18n.translate('xpack.observability.home.getStatedButton', { - defaultMessage: 'Get started', - })} - - - - -
-
-
- ); +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { fetchHasData } from '../../data_handler'; +import { useFetcher } from '../../hooks/use_fetcher'; + +export const HomePage = () => { + const history = useHistory(); + const { data = {} } = useFetcher(() => fetchHasData(), []); + + const values = Object.values(data); + const hasSomeData = values.length ? values.some((hasData) => hasData) : null; + + if (hasSomeData === true) { + history.push({ pathname: '/overview' }); + } + if (hasSomeData === false) { + history.push({ pathname: '/landing' }); + } + + return <>; }; diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts index d33571a16ccb7..8c87f17c16b3d 100644 --- a/x-pack/plugins/observability/public/pages/home/section.ts +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -4,19 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; - -interface ISection { - id: string; - title: string; - icon: string; - description: string; - href?: string; - target?: '_blank'; -} +import { ISection } from '../../typings/section'; export const appsSection: ISection[] = [ { - id: 'logs', + id: 'infra_logs', title: i18n.translate('xpack.observability.section.apps.logs.title', { defaultMessage: 'Logs', }), @@ -25,6 +17,7 @@ export const appsSection: ISection[] = [ defaultMessage: 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.', }), + href: 'https://www.elastic.co', }, { id: 'apm', @@ -36,9 +29,10 @@ export const appsSection: ISection[] = [ defaultMessage: 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', }), + href: 'https://www.elastic.co', }, { - id: 'metrics', + id: 'infra_metrics', title: i18n.translate('xpack.observability.section.apps.metrics.title', { defaultMessage: 'Metrics', }), @@ -47,6 +41,7 @@ export const appsSection: ISection[] = [ defaultMessage: 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.', }), + href: 'https://www.elastic.co', }, { id: 'uptime', @@ -58,5 +53,6 @@ export const appsSection: ISection[] = [ defaultMessage: 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.', }), + href: 'https://www.elastic.co', }, ]; diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx new file mode 100644 index 0000000000000..b614095641250 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiCard, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiImage, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { appsSection } from '../home/section'; + +const EuiCardWithoutPadding = styled(EuiCard)` + padding: 0; +`; + +export const LandingPage = () => { + const { core } = usePluginContext(); + const theme = useContext(ThemeContext); + + return ( + + + {/* title and description */} + + +

+ {i18n.translate('xpack.observability.home.sectionTitle', { + defaultMessage: 'Unified visibility across your entire ecosystem', + })} +

+
+ + + {i18n.translate('xpack.observability.home.sectionsubtitle', { + defaultMessage: + 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.', + })} + +
+ + {/* Apps sections */} + + + + + + {appsSection.map((app) => ( + + } + title={ + +

{app.title}

+
+ } + description={app.description} + /> +
+ ))} +
+
+ + + +
+
+ + + + {/* Get started button */} + + + + + {i18n.translate('xpack.observability.home.getStatedButton', { + defaultMessage: 'Get started', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts new file mode 100644 index 0000000000000..61456bc88bd3e --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { AppMountContext } from 'kibana/public'; +import { ISection } from '../../typings/section'; + +export const getEmptySections = ({ core }: { core: AppMountContext['core'] }): ISection[] => { + return [ + { + id: 'infra_logs', + title: i18n.translate('xpack.observability.emptySection.apps.logs.title', { + defaultMessage: 'Logs', + }), + icon: 'logoLogging', + description: i18n.translate('xpack.observability.emptySection.apps.logs.description', { + defaultMessage: + 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.logs.link', { + defaultMessage: 'Install Filebeat', + }), + href: 'https://www.elastic.co', + }, + { + id: 'apm', + title: i18n.translate('xpack.observability.emptySection.apps.apm.title', { + defaultMessage: 'APM', + }), + icon: 'logoAPM', + description: i18n.translate('xpack.observability.emptySection.apps.apm.description', { + defaultMessage: + 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.apm.link', { + defaultMessage: 'Install agent', + }), + href: 'https://www.elastic.co', + }, + { + id: 'infra_metrics', + title: i18n.translate('xpack.observability.emptySection.apps.metrics.title', { + defaultMessage: 'Metrics', + }), + icon: 'logoMetrics', + description: i18n.translate('xpack.observability.emptySection.apps.metrics.description', { + defaultMessage: + 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.metrics.link', { + defaultMessage: 'Install metrics module', + }), + href: 'https://www.elastic.co', + }, + { + id: 'uptime', + title: i18n.translate('xpack.observability.emptySection.apps.uptime.title', { + defaultMessage: 'Uptime', + }), + icon: 'logoUptime', + description: i18n.translate('xpack.observability.emptySection.apps.uptime.description', { + defaultMessage: + 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', { + defaultMessage: 'Install Heartbeat', + }), + href: 'https://www.elastic.co', + }, + { + id: 'alert', + title: i18n.translate('xpack.observability.emptySection.apps.alert.title', { + defaultMessage: 'No alerts found.', + }), + icon: 'watchesApp', + description: i18n.translate('xpack.observability.emptySection.apps.alert.description', { + defaultMessage: + '503 errors stacking up. Applications not responding. CPU and RAM utilization jumping. See these warnings as they happen - not as part of the post-mortem.', + }), + linkTitle: i18n.translate('xpack.observability.emptySection.apps.alert.link', { + defaultMessage: 'Create alert', + }), + href: core.http.basePath.prepend( + '/app/management/insightsAndAlerting/triggersActions/alerts' + ), + }, + ]; +}; diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx new file mode 100644 index 0000000000000..9caac7f9d86f4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import moment from 'moment'; +import React, { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { EmptySection } from '../../components/app/empty_section'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { Resources } from '../../components/app/resources'; +import { AlertsSection } from '../../components/app/section/alerts'; +import { APMSection } from '../../components/app/section/apm'; +import { LogsSection } from '../../components/app/section/logs'; +import { MetricsSection } from '../../components/app/section/metrics'; +import { UptimeSection } from '../../components/app/section/uptime'; +import { DatePicker, TimePickerTime } from '../../components/shared/data_picker'; +import { fetchHasData } from '../../data_handler'; +import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; +import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { RouteParams } from '../../routes'; +import { getObservabilityAlerts } from '../../services/get_observability_alerts'; +import { getParsedDate } from '../../utils/date'; +import { getBucketSize } from '../../utils/get_bucket_size'; +import { getEmptySections } from './empty_section'; +import { LoadingObservability } from './loading_observability'; + +interface Props { + routeParams: RouteParams<'/overview'>; +} + +function calculatetBucketSize({ startTime, endTime }: { startTime?: string; endTime?: string }) { + if (startTime && endTime) { + return getBucketSize({ + start: moment.utc(startTime).valueOf(), + end: moment.utc(endTime).valueOf(), + minInterval: '60s', + }); + } +} + +export const OverviewPage = ({ routeParams }: Props) => { + const { core } = usePluginContext(); + + const { data: alerts = [], status: alertStatus } = useFetcher(() => { + return getObservabilityAlerts({ core }); + }, []); + + const theme = useContext(ThemeContext); + const timePickerTime = useKibanaUISettings(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + + const result = useFetcher(() => fetchHasData(), []); + const hasData = result.data; + + if (!hasData) { + return ; + } + + const { + rangeFrom = timePickerTime.from, + rangeTo = timePickerTime.to, + refreshInterval = 10000, + refreshPaused = true, + } = routeParams.query; + + const startTime = getParsedDate(rangeFrom); + const endTime = getParsedDate(rangeTo, { roundUp: true }); + const bucketSize = calculatetBucketSize({ startTime, endTime }); + + const appEmptySections = getEmptySections({ core }).filter(({ id }) => { + if (id === 'alert') { + return alertStatus !== FETCH_STATUS.FAILURE && !alerts.length; + } + return !hasData[id]; + }); + + // Hides the data section when all 'hasData' is false or undefined + const showDataSections = Object.values(hasData).some((hasPluginData) => hasPluginData); + + return ( + + + + + + + + + + + + {/* Data sections */} + {showDataSections && ( + + {hasData.infra_logs && ( + + + + )} + {hasData.infra_metrics && ( + + + + )} + {hasData.apm && ( + + + + )} + {hasData.uptime && ( + + + + )} + + )} + + {/* Empty sections */} + {!!appEmptySections.length && ( + + + 2 ? 2 : 1 + } + gutterSize="s" + > + {appEmptySections.map((app) => { + return ( + + + + ); + })} + + + )} + + + {/* Alert section */} + {!!alerts.length && ( + + + + )} + + {/* Resources section */} + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx new file mode 100644 index 0000000000000..90e3104443e6b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useContext } from 'react'; +import styled, { ThemeContext } from 'styled-components'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { WithHeaderLayout } from '../../components/app/layout/with_header'; + +const CentralizedFlexGroup = styled(EuiFlexGroup)` + justify-content: center; + align-items: center; + // place the element in the center of the page + min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); +`; + +export const LoadingObservability = () => { + const theme = useContext(ThemeContext); + + return ( + + + + + + + + + + + {i18n.translate('xpack.observability.overview.loadingObservability', { + defaultMessage: 'Loading Observability', + })} + + + + + + + + ); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts new file mode 100644 index 0000000000000..759b8b5fdae4f --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/alerts.mock.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const alertsFetchData = async () => { + return Promise.resolve({ + data: [ + { + id: '1', + consumer: 'apm', + name: 'Error rate | opbeans-java', + alertTypeId: 'apm.error_rate', + tags: ['apm', 'service.name:opbeans-java'], + updatedAt: '2020-07-03T14:27:51.488Z', + muteAll: true, + }, + { + id: '2', + consumer: 'apm', + name: 'Transaction duration | opbeans-java', + alertTypeId: 'apm.transaction_duration', + tags: ['apm', 'service.name:opbeans-java'], + updatedAt: '2020-07-02T14:27:51.488Z', + muteAll: true, + }, + { + id: '3', + consumer: 'logs', + name: 'Logs obs test', + alertTypeId: 'logs.alert.document.count', + tags: ['logs', 'observability'], + updatedAt: '2020-06-30T14:27:51.488Z', + muteAll: true, + }, + { + id: '4', + consumer: 'metrics', + name: 'Metrics obs test', + alertTypeId: 'metrics.alert.inventory.threshold', + tags: ['metrics', 'observability'], + updatedAt: '2020-03-20T14:27:51.488Z', + muteAll: true, + }, + { + id: '5', + consumer: 'uptime', + name: 'Uptime obs test', + alertTypeId: 'xpack.uptime.alerts.monitorStatus', + tags: ['uptime', 'observability'], + updatedAt: '2020-03-25T17:27:51.488Z', + muteAll: true, + }, + ], + }); +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts new file mode 100644 index 0000000000000..7303b78cc0132 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/apm.mock.ts @@ -0,0 +1,627 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ApmFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchApmData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: ApmFetchDataResponse = { + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + value: 7, + }, + transactions: { + type: 'number', + value: 125808, + }, + }, + series: { + transactions: { + coordinates: [ + { + x: 1593295200000, + y: 891, + }, + { + x: 1593297000000, + y: 902, + }, + { + x: 1593298800000, + y: 924, + }, + { + x: 1593300600000, + y: 944, + }, + { + x: 1593302400000, + y: 935, + }, + { + x: 1593304200000, + y: 915, + }, + { + x: 1593306000000, + y: 917, + }, + { + x: 1593307800000, + y: 941, + }, + { + x: 1593309600000, + y: 906, + }, + { + x: 1593311400000, + y: 939, + }, + { + x: 1593313200000, + y: 961, + }, + { + x: 1593315000000, + y: 911, + }, + { + x: 1593316800000, + y: 958, + }, + { + x: 1593318600000, + y: 861, + }, + { + x: 1593320400000, + y: 906, + }, + { + x: 1593322200000, + y: 899, + }, + { + x: 1593324000000, + y: 785, + }, + { + x: 1593325800000, + y: 952, + }, + { + x: 1593327600000, + y: 910, + }, + { + x: 1593329400000, + y: 869, + }, + { + x: 1593331200000, + y: 895, + }, + { + x: 1593333000000, + y: 924, + }, + { + x: 1593334800000, + y: 930, + }, + { + x: 1593336600000, + y: 947, + }, + { + x: 1593338400000, + y: 905, + }, + { + x: 1593340200000, + y: 963, + }, + { + x: 1593342000000, + y: 877, + }, + { + x: 1593343800000, + y: 839, + }, + { + x: 1593345600000, + y: 884, + }, + { + x: 1593347400000, + y: 934, + }, + { + x: 1593349200000, + y: 908, + }, + { + x: 1593351000000, + y: 982, + }, + { + x: 1593352800000, + y: 897, + }, + { + x: 1593354600000, + y: 903, + }, + { + x: 1593356400000, + y: 877, + }, + { + x: 1593358200000, + y: 893, + }, + { + x: 1593360000000, + y: 919, + }, + { + x: 1593361800000, + y: 844, + }, + { + x: 1593363600000, + y: 940, + }, + { + x: 1593365400000, + y: 951, + }, + { + x: 1593367200000, + y: 869, + }, + { + x: 1593369000000, + y: 901, + }, + { + x: 1593370800000, + y: 940, + }, + { + x: 1593372600000, + y: 942, + }, + { + x: 1593374400000, + y: 881, + }, + { + x: 1593376200000, + y: 935, + }, + { + x: 1593378000000, + y: 892, + }, + { + x: 1593379800000, + y: 861, + }, + { + x: 1593381600000, + y: 868, + }, + { + x: 1593383400000, + y: 990, + }, + { + x: 1593385200000, + y: 931, + }, + { + x: 1593387000000, + y: 898, + }, + { + x: 1593388800000, + y: 906, + }, + { + x: 1593390600000, + y: 928, + }, + { + x: 1593392400000, + y: 975, + }, + { + x: 1593394200000, + y: 842, + }, + { + x: 1593396000000, + y: 940, + }, + { + x: 1593397800000, + y: 922, + }, + { + x: 1593399600000, + y: 962, + }, + { + x: 1593401400000, + y: 940, + }, + { + x: 1593403200000, + y: 974, + }, + { + x: 1593405000000, + y: 887, + }, + { + x: 1593406800000, + y: 920, + }, + { + x: 1593408600000, + y: 854, + }, + { + x: 1593410400000, + y: 898, + }, + { + x: 1593412200000, + y: 952, + }, + { + x: 1593414000000, + y: 987, + }, + { + x: 1593415800000, + y: 932, + }, + { + x: 1593417600000, + y: 1009, + }, + { + x: 1593419400000, + y: 989, + }, + { + x: 1593421200000, + y: 939, + }, + { + x: 1593423000000, + y: 929, + }, + { + x: 1593424800000, + y: 929, + }, + { + x: 1593426600000, + y: 864, + }, + { + x: 1593428400000, + y: 895, + }, + { + x: 1593430200000, + y: 876, + }, + { + x: 1593432000000, + y: 68, + }, + { + x: 1593433800000, + y: 0, + }, + { + x: 1593435600000, + y: 0, + }, + { + x: 1593437400000, + y: 0, + }, + { + x: 1593439200000, + y: 0, + }, + { + x: 1593441000000, + y: 0, + }, + { + x: 1593442800000, + y: 700, + }, + { + x: 1593444600000, + y: 930, + }, + { + x: 1593446400000, + y: 953, + }, + { + x: 1593448200000, + y: 995, + }, + { + x: 1593450000000, + y: 883, + }, + { + x: 1593451800000, + y: 902, + }, + { + x: 1593453600000, + y: 988, + }, + { + x: 1593455400000, + y: 947, + }, + { + x: 1593457200000, + y: 889, + }, + { + x: 1593459000000, + y: 982, + }, + { + x: 1593460800000, + y: 919, + }, + { + x: 1593462600000, + y: 854, + }, + { + x: 1593464400000, + y: 894, + }, + { + x: 1593466200000, + y: 901, + }, + { + x: 1593468000000, + y: 970, + }, + { + x: 1593469800000, + y: 840, + }, + { + x: 1593471600000, + y: 857, + }, + { + x: 1593473400000, + y: 943, + }, + { + x: 1593475200000, + y: 825, + }, + { + x: 1593477000000, + y: 955, + }, + { + x: 1593478800000, + y: 959, + }, + { + x: 1593480600000, + y: 921, + }, + { + x: 1593482400000, + y: 924, + }, + { + x: 1593484200000, + y: 840, + }, + { + x: 1593486000000, + y: 943, + }, + { + x: 1593487800000, + y: 919, + }, + { + x: 1593489600000, + y: 882, + }, + { + x: 1593491400000, + y: 900, + }, + { + x: 1593493200000, + y: 930, + }, + { + x: 1593495000000, + y: 854, + }, + { + x: 1593496800000, + y: 905, + }, + { + x: 1593498600000, + y: 922, + }, + { + x: 1593500400000, + y: 863, + }, + { + x: 1593502200000, + y: 966, + }, + { + x: 1593504000000, + y: 910, + }, + { + x: 1593505800000, + y: 851, + }, + { + x: 1593507600000, + y: 867, + }, + { + x: 1593509400000, + y: 904, + }, + { + x: 1593511200000, + y: 913, + }, + { + x: 1593513000000, + y: 889, + }, + { + x: 1593514800000, + y: 907, + }, + { + x: 1593516600000, + y: 965, + }, + { + x: 1593518400000, + y: 868, + }, + { + x: 1593520200000, + y: 919, + }, + { + x: 1593522000000, + y: 945, + }, + { + x: 1593523800000, + y: 883, + }, + { + x: 1593525600000, + y: 902, + }, + { + x: 1593527400000, + y: 900, + }, + { + x: 1593529200000, + y: 829, + }, + { + x: 1593531000000, + y: 919, + }, + { + x: 1593532800000, + y: 942, + }, + { + x: 1593534600000, + y: 924, + }, + { + x: 1593536400000, + y: 958, + }, + { + x: 1593538200000, + y: 867, + }, + { + x: 1593540000000, + y: 844, + }, + { + x: 1593541800000, + y: 976, + }, + { + x: 1593543600000, + y: 937, + }, + { + x: 1593545400000, + y: 891, + }, + { + x: 1593547200000, + y: 936, + }, + { + x: 1593549000000, + y: 895, + }, + { + x: 1593550800000, + y: 850, + }, + { + x: 1593552600000, + y: 899, + }, + ], + }, + }, +}; + +export const emptyResponse: ApmFetchDataResponse = { + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + value: 0, + }, + transactions: { + type: 'number', + value: 0, + }, + }, + series: { + transactions: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts new file mode 100644 index 0000000000000..5bea1fbf19ace --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/logs.mock.ts @@ -0,0 +1,2326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FetchData, LogsFetchDataResponse } from '../../../typings'; + +export const fetchLogsData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: LogsFetchDataResponse = { + title: 'Logs', + appLink: + "/app/logs/stream?logPosition=(end:'2020-06-30T21:30:00.000Z',start:'2020-06-27T22:00:00.000Z')", + stats: { + 'haproxy.log': { + type: 'number', + label: 'haproxy.log', + value: 145.84289044289045, + }, + 'nginx.access': { + type: 'number', + label: 'nginx.access', + value: 94.67039627039627, + }, + 'kibana.log': { + type: 'number', + label: 'kibana.log', + value: 11.018181818181818, + }, + 'nginx.error': { + type: 'number', + label: 'nginx.error', + value: 8.218181818181819, + }, + }, + series: { + 'haproxy.log': { + label: 'haproxy.log', + coordinates: [ + { + x: 1593295200000, + y: 146.83333333333334, + }, + { + x: 1593297000000, + y: 146.96666666666667, + }, + { + x: 1593298800000, + y: 146.96666666666667, + }, + { + x: 1593300600000, + y: 146.86666666666667, + }, + { + x: 1593302400000, + y: 146.96666666666667, + }, + { + x: 1593304200000, + y: 147.03333333333333, + }, + { + x: 1593306000000, + y: 147.16666666666666, + }, + { + x: 1593307800000, + y: 146.96666666666667, + }, + { + x: 1593309600000, + y: 146.96666666666667, + }, + { + x: 1593311400000, + y: 146.96666666666667, + }, + { + x: 1593313200000, + y: 147.03333333333333, + }, + { + x: 1593315000000, + y: 147.13333333333333, + }, + { + x: 1593316800000, + y: 146.96666666666667, + }, + { + x: 1593318600000, + y: 146.96666666666667, + }, + { + x: 1593320400000, + y: 146.93333333333334, + }, + { + x: 1593322200000, + y: 147.06666666666666, + }, + { + x: 1593324000000, + y: 146.9, + }, + { + x: 1593325800000, + y: 147.06666666666666, + }, + { + x: 1593327600000, + y: 147.06666666666666, + }, + { + x: 1593329400000, + y: 146.93333333333334, + }, + { + x: 1593331200000, + y: 146.86666666666667, + }, + { + x: 1593333000000, + y: 146.86666666666667, + }, + { + x: 1593334800000, + y: 147, + }, + { + x: 1593336600000, + y: 146.66666666666666, + }, + { + x: 1593338400000, + y: 146.83333333333334, + }, + { + x: 1593340200000, + y: 146.9, + }, + { + x: 1593342000000, + y: 146.96666666666667, + }, + { + x: 1593343800000, + y: 146.86666666666667, + }, + { + x: 1593345600000, + y: 146.83333333333334, + }, + { + x: 1593347400000, + y: 146.86666666666667, + }, + { + x: 1593349200000, + y: 146.93333333333334, + }, + { + x: 1593351000000, + y: 146.8, + }, + { + x: 1593352800000, + y: 146.83333333333334, + }, + { + x: 1593354600000, + y: 146.83333333333334, + }, + { + x: 1593356400000, + y: 146.73333333333332, + }, + { + x: 1593358200000, + y: 146.9, + }, + { + x: 1593360000000, + y: 146.73333333333332, + }, + { + x: 1593361800000, + y: 146.63333333333333, + }, + { + x: 1593363600000, + y: 146.6, + }, + { + x: 1593365400000, + y: 147.06666666666666, + }, + { + x: 1593367200000, + y: 147, + }, + { + x: 1593369000000, + y: 146.93333333333334, + }, + { + x: 1593370800000, + y: 146.73333333333332, + }, + { + x: 1593372600000, + y: 147.06666666666666, + }, + { + x: 1593374400000, + y: 147, + }, + { + x: 1593376200000, + y: 147.06666666666666, + }, + { + x: 1593378000000, + y: 147.2, + }, + { + x: 1593379800000, + y: 147.1, + }, + { + x: 1593381600000, + y: 147, + }, + { + x: 1593383400000, + y: 147.06666666666666, + }, + { + x: 1593385200000, + y: 147.13333333333333, + }, + { + x: 1593387000000, + y: 147.2, + }, + { + x: 1593388800000, + y: 146.96666666666667, + }, + { + x: 1593390600000, + y: 146.83333333333334, + }, + { + x: 1593392400000, + y: 146.8, + }, + { + x: 1593394200000, + y: 144.3, + }, + { + x: 1593396000000, + y: 147.3, + }, + { + x: 1593397800000, + y: 147.2, + }, + { + x: 1593399600000, + y: 147.33333333333334, + }, + { + x: 1593401400000, + y: 147.1, + }, + { + x: 1593403200000, + y: 147.13333333333333, + }, + { + x: 1593405000000, + y: 147.16666666666666, + }, + { + x: 1593406800000, + y: 147.1, + }, + { + x: 1593408600000, + y: 147.3, + }, + { + x: 1593410400000, + y: 147.26666666666668, + }, + { + x: 1593412200000, + y: 147.2, + }, + { + x: 1593414000000, + y: 147.03333333333333, + }, + { + x: 1593415800000, + y: 146.9, + }, + { + x: 1593417600000, + y: 146.96666666666667, + }, + { + x: 1593419400000, + y: 147.1, + }, + { + x: 1593421200000, + y: 147.13333333333333, + }, + { + x: 1593423000000, + y: 147.03333333333333, + }, + { + x: 1593424800000, + y: 141.36666666666667, + }, + { + x: 1593426600000, + y: 144.63333333333333, + }, + { + x: 1593428400000, + y: 153.66666666666666, + }, + { + x: 1593430200000, + y: 136.76666666666668, + }, + { + x: 1593432000000, + y: 123.43333333333334, + }, + { + x: 1593433800000, + y: 123.5, + }, + { + x: 1593435600000, + y: 123.26666666666667, + }, + { + x: 1593437400000, + y: 123.23333333333333, + }, + { + x: 1593439200000, + y: 123.13333333333334, + }, + { + x: 1593441000000, + y: 123.2, + }, + { + x: 1593442800000, + y: 144.23333333333332, + }, + { + x: 1593444600000, + y: 147.06666666666666, + }, + { + x: 1593446400000, + y: 146.9, + }, + { + x: 1593448200000, + y: 146.7, + }, + { + x: 1593450000000, + y: 146.8, + }, + { + x: 1593451800000, + y: 146.73333333333332, + }, + { + x: 1593453600000, + y: 146.7, + }, + { + x: 1593455400000, + y: 146.7, + }, + { + x: 1593457200000, + y: 146.56666666666666, + }, + { + x: 1593459000000, + y: 146.8, + }, + { + x: 1593460800000, + y: 146.8, + }, + { + x: 1593462600000, + y: 146.83333333333334, + }, + { + x: 1593464400000, + y: 146.7, + }, + { + x: 1593466200000, + y: 146.9, + }, + { + x: 1593468000000, + y: 147.03333333333333, + }, + { + x: 1593469800000, + y: 146.76666666666668, + }, + { + x: 1593471600000, + y: 146.7, + }, + { + x: 1593473400000, + y: 146.63333333333333, + }, + { + x: 1593475200000, + y: 146.93333333333334, + }, + { + x: 1593477000000, + y: 146.5, + }, + { + x: 1593478800000, + y: 146.76666666666668, + }, + { + x: 1593480600000, + y: 144.83333333333334, + }, + { + x: 1593482400000, + y: 146.96666666666667, + }, + { + x: 1593484200000, + y: 147.1, + }, + { + x: 1593486000000, + y: 147.1, + }, + { + x: 1593487800000, + y: 147.3, + }, + { + x: 1593489600000, + y: 147.1, + }, + { + x: 1593491400000, + y: 147.03333333333333, + }, + { + x: 1593493200000, + y: 147.2, + }, + { + x: 1593495000000, + y: 147.06666666666666, + }, + { + x: 1593496800000, + y: 147.1, + }, + { + x: 1593498600000, + y: 147.2, + }, + { + x: 1593500400000, + y: 147.06666666666666, + }, + { + x: 1593502200000, + y: 147.06666666666666, + }, + { + x: 1593504000000, + y: 147.06666666666666, + }, + { + x: 1593505800000, + y: 147.06666666666666, + }, + { + x: 1593507600000, + y: 146.96666666666667, + }, + { + x: 1593509400000, + y: 147.16666666666666, + }, + { + x: 1593511200000, + y: 147.03333333333333, + }, + { + x: 1593513000000, + y: 147, + }, + { + x: 1593514800000, + y: 147.03333333333333, + }, + { + x: 1593516600000, + y: 146.96666666666667, + }, + { + x: 1593518400000, + y: 146.63333333333333, + }, + { + x: 1593520200000, + y: 146.43333333333334, + }, + { + x: 1593522000000, + y: 147.13333333333333, + }, + { + x: 1593523800000, + y: 147.13333333333333, + }, + { + x: 1593525600000, + y: 146.93333333333334, + }, + { + x: 1593527400000, + y: 147, + }, + { + x: 1593529200000, + y: 147.03333333333333, + }, + { + x: 1593531000000, + y: 147.2, + }, + { + x: 1593532800000, + y: 147.13333333333333, + }, + { + x: 1593534600000, + y: 147.13333333333333, + }, + { + x: 1593536400000, + y: 147.13333333333333, + }, + { + x: 1593538200000, + y: 147.1, + }, + { + x: 1593540000000, + y: 147, + }, + { + x: 1593541800000, + y: 147.26666666666668, + }, + { + x: 1593543600000, + y: 146.73333333333332, + }, + { + x: 1593545400000, + y: 147.03333333333333, + }, + { + x: 1593547200000, + y: 147, + }, + { + x: 1593549000000, + y: 146.9, + }, + { + x: 1593550800000, + y: 147.03333333333333, + }, + ], + }, + 'nginx.access': { + label: 'nginx.access', + coordinates: [ + { + x: 1593295200000, + y: 94.06666666666666, + }, + { + x: 1593297000000, + y: 91.4, + }, + { + x: 1593298800000, + y: 95.03333333333333, + }, + { + x: 1593300600000, + y: 94.5, + }, + { + x: 1593302400000, + y: 94.06666666666666, + }, + { + x: 1593304200000, + y: 93.3, + }, + { + x: 1593306000000, + y: 91.16666666666667, + }, + { + x: 1593307800000, + y: 94.5, + }, + { + x: 1593309600000, + y: 93.53333333333333, + }, + { + x: 1593311400000, + y: 118.9, + }, + { + x: 1593313200000, + y: 110.66666666666667, + }, + { + x: 1593315000000, + y: 95.66666666666667, + }, + { + x: 1593316800000, + y: 99.53333333333333, + }, + { + x: 1593318600000, + y: 123.36666666666666, + }, + { + x: 1593320400000, + y: 94.13333333333334, + }, + { + x: 1593322200000, + y: 95.53333333333333, + }, + { + x: 1593324000000, + y: 93.93333333333334, + }, + { + x: 1593325800000, + y: 94.06666666666666, + }, + { + x: 1593327600000, + y: 118.16666666666667, + }, + { + x: 1593329400000, + y: 108.6, + }, + { + x: 1593331200000, + y: 93.53333333333333, + }, + { + x: 1593333000000, + y: 93.06666666666666, + }, + { + x: 1593334800000, + y: 93.76666666666667, + }, + { + x: 1593336600000, + y: 95.3, + }, + { + x: 1593338400000, + y: 96.4, + }, + { + x: 1593340200000, + y: 121.93333333333334, + }, + { + x: 1593342000000, + y: 134.43333333333334, + }, + { + x: 1593343800000, + y: 160.4, + }, + { + x: 1593345600000, + y: 129.7, + }, + { + x: 1593347400000, + y: 119.16666666666667, + }, + { + x: 1593349200000, + y: 133.06666666666666, + }, + { + x: 1593351000000, + y: 212.4, + }, + { + x: 1593352800000, + y: 95.36666666666666, + }, + { + x: 1593354600000, + y: 93.6, + }, + { + x: 1593356400000, + y: 93.4, + }, + { + x: 1593358200000, + y: 95.1, + }, + { + x: 1593360000000, + y: 94.36666666666666, + }, + { + x: 1593361800000, + y: 97.23333333333333, + }, + { + x: 1593363600000, + y: 94.03333333333333, + }, + { + x: 1593365400000, + y: 94.53333333333333, + }, + { + x: 1593367200000, + y: 93.56666666666666, + }, + { + x: 1593369000000, + y: 98.43333333333334, + }, + { + x: 1593370800000, + y: 92.3, + }, + { + x: 1593372600000, + y: 93.13333333333334, + }, + { + x: 1593374400000, + y: 93.16666666666667, + }, + { + x: 1593376200000, + y: 93.7, + }, + { + x: 1593378000000, + y: 94.46666666666667, + }, + { + x: 1593379800000, + y: 97.16666666666667, + }, + { + x: 1593381600000, + y: 94.36666666666666, + }, + { + x: 1593383400000, + y: 93.7, + }, + { + x: 1593385200000, + y: 93.4, + }, + { + x: 1593387000000, + y: 91.3, + }, + { + x: 1593388800000, + y: 92.66666666666667, + }, + { + x: 1593390600000, + y: 93.73333333333333, + }, + { + x: 1593392400000, + y: 94.33333333333333, + }, + { + x: 1593394200000, + y: 93.23333333333333, + }, + { + x: 1593396000000, + y: 93.9, + }, + { + x: 1593397800000, + y: 92.83333333333333, + }, + { + x: 1593399600000, + y: 93, + }, + { + x: 1593401400000, + y: 91.2, + }, + { + x: 1593403200000, + y: 91.96666666666667, + }, + { + x: 1593405000000, + y: 93.83333333333333, + }, + { + x: 1593406800000, + y: 93.16666666666667, + }, + { + x: 1593408600000, + y: 95.36666666666666, + }, + { + x: 1593410400000, + y: 92.5, + }, + { + x: 1593412200000, + y: 93.16666666666667, + }, + { + x: 1593414000000, + y: 92.8, + }, + { + x: 1593415800000, + y: 95.83333333333333, + }, + { + x: 1593417600000, + y: 96.96666666666667, + }, + { + x: 1593419400000, + y: 94.63333333333334, + }, + { + x: 1593421200000, + y: 98.7, + }, + { + x: 1593423000000, + y: 100.03333333333333, + }, + { + x: 1593424800000, + y: 108.66666666666667, + }, + { + x: 1593426600000, + y: 110.9, + }, + { + x: 1593428400000, + y: 88.56666666666666, + }, + { + x: 1593430200000, + y: 1, + }, + { + x: 1593442800000, + y: 74.53333333333333, + }, + { + x: 1593444600000, + y: 99.03333333333333, + }, + { + x: 1593446400000, + y: 98.03333333333333, + }, + { + x: 1593448200000, + y: 91.26666666666667, + }, + { + x: 1593450000000, + y: 107.76666666666667, + }, + { + x: 1593451800000, + y: 98.26666666666667, + }, + { + x: 1593453600000, + y: 99.46666666666667, + }, + { + x: 1593455400000, + y: 102.33333333333333, + }, + { + x: 1593457200000, + y: 108.13333333333334, + }, + { + x: 1593459000000, + y: 95.36666666666666, + }, + { + x: 1593460800000, + y: 98.23333333333333, + }, + { + x: 1593462600000, + y: 91.46666666666667, + }, + { + x: 1593464400000, + y: 115.63333333333334, + }, + { + x: 1593466200000, + y: 116.23333333333333, + }, + { + x: 1593468000000, + y: 91.66666666666667, + }, + { + x: 1593469800000, + y: 94.33333333333333, + }, + { + x: 1593471600000, + y: 96.43333333333334, + }, + { + x: 1593473400000, + y: 94.7, + }, + { + x: 1593475200000, + y: 93.76666666666667, + }, + { + x: 1593477000000, + y: 91.5, + }, + { + x: 1593478800000, + y: 91.9, + }, + { + x: 1593480600000, + y: 91.3, + }, + { + x: 1593482400000, + y: 98.3, + }, + { + x: 1593484200000, + y: 95.53333333333333, + }, + { + x: 1593486000000, + y: 95.66666666666667, + }, + { + x: 1593487800000, + y: 92.73333333333333, + }, + { + x: 1593489600000, + y: 93.6, + }, + { + x: 1593491400000, + y: 94.3, + }, + { + x: 1593493200000, + y: 93.13333333333334, + }, + { + x: 1593495000000, + y: 104.36666666666666, + }, + { + x: 1593496800000, + y: 107.26666666666667, + }, + { + x: 1593498600000, + y: 101.83333333333333, + }, + { + x: 1593500400000, + y: 105.46666666666667, + }, + { + x: 1593502200000, + y: 111.86666666666666, + }, + { + x: 1593504000000, + y: 111.56666666666666, + }, + { + x: 1593505800000, + y: 103.76666666666667, + }, + { + x: 1593507600000, + y: 93.9, + }, + { + x: 1593509400000, + y: 97.16666666666667, + }, + { + x: 1593511200000, + y: 93.03333333333333, + }, + { + x: 1593513000000, + y: 94.4, + }, + { + x: 1593514800000, + y: 94.76666666666667, + }, + { + x: 1593516600000, + y: 94.96666666666667, + }, + { + x: 1593518400000, + y: 101.3, + }, + { + x: 1593520200000, + y: 98.63333333333334, + }, + { + x: 1593522000000, + y: 94.8, + }, + { + x: 1593523800000, + y: 97.46666666666667, + }, + { + x: 1593525600000, + y: 95.86666666666666, + }, + { + x: 1593527400000, + y: 97.3, + }, + { + x: 1593529200000, + y: 96.1, + }, + { + x: 1593531000000, + y: 97.1, + }, + { + x: 1593532800000, + y: 97.56666666666666, + }, + { + x: 1593534600000, + y: 107.6, + }, + { + x: 1593536400000, + y: 97.46666666666667, + }, + { + x: 1593538200000, + y: 96.46666666666667, + }, + { + x: 1593540000000, + y: 93.83333333333333, + }, + { + x: 1593541800000, + y: 98.73333333333333, + }, + { + x: 1593543600000, + y: 99.86666666666666, + }, + { + x: 1593545400000, + y: 98.66666666666667, + }, + { + x: 1593547200000, + y: 102.8, + }, + { + x: 1593549000000, + y: 96.13333333333334, + }, + { + x: 1593550800000, + y: 94.53333333333333, + }, + ], + }, + 'kibana.log': { + label: 'kibana.log', + coordinates: [ + { + x: 1593295200000, + y: 11.8, + }, + { + x: 1593297000000, + y: 11.833333333333334, + }, + { + x: 1593298800000, + y: 12.1, + }, + { + x: 1593300600000, + y: 12.133333333333333, + }, + { + x: 1593302400000, + y: 11.2, + }, + { + x: 1593304200000, + y: 11.933333333333334, + }, + { + x: 1593306000000, + y: 11.466666666666667, + }, + { + x: 1593307800000, + y: 12.066666666666666, + }, + { + x: 1593309600000, + y: 11.9, + }, + { + x: 1593311400000, + y: 11.766666666666667, + }, + { + x: 1593313200000, + y: 12.066666666666666, + }, + { + x: 1593315000000, + y: 11.7, + }, + { + x: 1593316800000, + y: 11.6, + }, + { + x: 1593318600000, + y: 11.766666666666667, + }, + { + x: 1593320400000, + y: 11.633333333333333, + }, + { + x: 1593322200000, + y: 11.833333333333334, + }, + { + x: 1593324000000, + y: 11.8, + }, + { + x: 1593325800000, + y: 11.7, + }, + { + x: 1593327600000, + y: 11.666666666666666, + }, + { + x: 1593329400000, + y: 11.8, + }, + { + x: 1593331200000, + y: 11.966666666666667, + }, + { + x: 1593333000000, + y: 11.766666666666667, + }, + { + x: 1593334800000, + y: 11.766666666666667, + }, + { + x: 1593336600000, + y: 11.866666666666667, + }, + { + x: 1593338400000, + y: 11.433333333333334, + }, + { + x: 1593340200000, + y: 12.033333333333333, + }, + { + x: 1593342000000, + y: 12.1, + }, + { + x: 1593343800000, + y: 12.1, + }, + { + x: 1593345600000, + y: 11.8, + }, + { + x: 1593347400000, + y: 12.366666666666667, + }, + { + x: 1593349200000, + y: 12.033333333333333, + }, + { + x: 1593351000000, + y: 12, + }, + { + x: 1593352800000, + y: 11.8, + }, + { + x: 1593354600000, + y: 11.5, + }, + { + x: 1593356400000, + y: 12.1, + }, + { + x: 1593358200000, + y: 11.966666666666667, + }, + { + x: 1593360000000, + y: 11.9, + }, + { + x: 1593361800000, + y: 12.233333333333333, + }, + { + x: 1593363600000, + y: 11.533333333333333, + }, + { + x: 1593365400000, + y: 11.633333333333333, + }, + { + x: 1593367200000, + y: 11.866666666666667, + }, + { + x: 1593369000000, + y: 12, + }, + { + x: 1593370800000, + y: 11.7, + }, + { + x: 1593372600000, + y: 11.8, + }, + { + x: 1593374400000, + y: 11.4, + }, + { + x: 1593376200000, + y: 11.766666666666667, + }, + { + x: 1593378000000, + y: 12.033333333333333, + }, + { + x: 1593379800000, + y: 11.833333333333334, + }, + { + x: 1593381600000, + y: 11.9, + }, + { + x: 1593383400000, + y: 11.966666666666667, + }, + { + x: 1593385200000, + y: 11.8, + }, + { + x: 1593387000000, + y: 12, + }, + { + x: 1593388800000, + y: 11.933333333333334, + }, + { + x: 1593390600000, + y: 12.033333333333333, + }, + { + x: 1593392400000, + y: 12, + }, + { + x: 1593394200000, + y: 11.533333333333333, + }, + { + x: 1593396000000, + y: 11.4, + }, + { + x: 1593397800000, + y: 11.666666666666666, + }, + { + x: 1593399600000, + y: 11.633333333333333, + }, + { + x: 1593401400000, + y: 11.166666666666666, + }, + { + x: 1593403200000, + y: 11.3, + }, + { + x: 1593405000000, + y: 11.2, + }, + { + x: 1593406800000, + y: 10.966666666666667, + }, + { + x: 1593408600000, + y: 11.5, + }, + { + x: 1593410400000, + y: 11.1, + }, + { + x: 1593412200000, + y: 11.2, + }, + { + x: 1593414000000, + y: 11.4, + }, + { + x: 1593415800000, + y: 10.8, + }, + { + x: 1593417600000, + y: 11.066666666666666, + }, + { + x: 1593419400000, + y: 11.8, + }, + { + x: 1593421200000, + y: 11.266666666666667, + }, + { + x: 1593423000000, + y: 11.333333333333334, + }, + { + x: 1593424800000, + y: 11.233333333333333, + }, + { + x: 1593426600000, + y: 11.5, + }, + { + x: 1593428400000, + y: 8.2, + }, + { + x: 1593442800000, + y: 8.2, + }, + { + x: 1593444600000, + y: 11.4, + }, + { + x: 1593446400000, + y: 10.733333333333333, + }, + { + x: 1593448200000, + y: 10.833333333333334, + }, + { + x: 1593450000000, + y: 11.3, + }, + { + x: 1593451800000, + y: 11.633333333333333, + }, + { + x: 1593453600000, + y: 11.266666666666667, + }, + { + x: 1593455400000, + y: 11.3, + }, + { + x: 1593457200000, + y: 11.333333333333334, + }, + { + x: 1593459000000, + y: 11.133333333333333, + }, + { + x: 1593460800000, + y: 10.933333333333334, + }, + { + x: 1593462600000, + y: 11.2, + }, + { + x: 1593464400000, + y: 11.166666666666666, + }, + { + x: 1593466200000, + y: 11.766666666666667, + }, + { + x: 1593468000000, + y: 11.433333333333334, + }, + { + x: 1593469800000, + y: 10.8, + }, + { + x: 1593471600000, + y: 11.266666666666667, + }, + { + x: 1593473400000, + y: 11.333333333333334, + }, + { + x: 1593475200000, + y: 11.133333333333333, + }, + { + x: 1593477000000, + y: 11.133333333333333, + }, + { + x: 1593478800000, + y: 10.9, + }, + { + x: 1593480600000, + y: 11.3, + }, + { + x: 1593482400000, + y: 12.166666666666666, + }, + { + x: 1593484200000, + y: 11.433333333333334, + }, + { + x: 1593486000000, + y: 12.133333333333333, + }, + { + x: 1593487800000, + y: 11.666666666666666, + }, + { + x: 1593489600000, + y: 11.533333333333333, + }, + { + x: 1593491400000, + y: 11.833333333333334, + }, + { + x: 1593493200000, + y: 11.766666666666667, + }, + { + x: 1593495000000, + y: 11.9, + }, + { + x: 1593496800000, + y: 11.433333333333334, + }, + { + x: 1593498600000, + y: 12, + }, + { + x: 1593500400000, + y: 12.1, + }, + { + x: 1593502200000, + y: 11.6, + }, + { + x: 1593504000000, + y: 12, + }, + { + x: 1593505800000, + y: 12.233333333333333, + }, + { + x: 1593507600000, + y: 11.633333333333333, + }, + { + x: 1593509400000, + y: 11.2, + }, + { + x: 1593511200000, + y: 11.766666666666667, + }, + { + x: 1593513000000, + y: 11.9, + }, + { + x: 1593514800000, + y: 11.366666666666667, + }, + { + x: 1593516600000, + y: 11.833333333333334, + }, + { + x: 1593518400000, + y: 11.5, + }, + { + x: 1593520200000, + y: 12, + }, + { + x: 1593522000000, + y: 12.033333333333333, + }, + { + x: 1593523800000, + y: 11.733333333333333, + }, + { + x: 1593525600000, + y: 11.566666666666666, + }, + { + x: 1593527400000, + y: 11.6, + }, + { + x: 1593529200000, + y: 11.333333333333334, + }, + { + x: 1593531000000, + y: 11.833333333333334, + }, + { + x: 1593532800000, + y: 11.233333333333333, + }, + { + x: 1593534600000, + y: 11.833333333333334, + }, + { + x: 1593536400000, + y: 11.266666666666667, + }, + { + x: 1593538200000, + y: 12, + }, + { + x: 1593540000000, + y: 11.633333333333333, + }, + { + x: 1593541800000, + y: 11.9, + }, + { + x: 1593543600000, + y: 11.966666666666667, + }, + { + x: 1593545400000, + y: 11.5, + }, + { + x: 1593547200000, + y: 11.466666666666667, + }, + { + x: 1593549000000, + y: 11.4, + }, + { + x: 1593550800000, + y: 11.833333333333334, + }, + ], + }, + 'nginx.error': { + label: 'nginx.error', + coordinates: [ + { + x: 1593295200000, + y: 9.266666666666667, + }, + { + x: 1593297000000, + y: 8.833333333333334, + }, + { + x: 1593298800000, + y: 9.033333333333333, + }, + { + x: 1593300600000, + y: 8.933333333333334, + }, + { + x: 1593302400000, + y: 8.9, + }, + { + x: 1593304200000, + y: 9.6, + }, + { + x: 1593306000000, + y: 9.066666666666666, + }, + { + x: 1593307800000, + y: 8.966666666666667, + }, + { + x: 1593309600000, + y: 8.933333333333334, + }, + { + x: 1593311400000, + y: 8.5, + }, + { + x: 1593313200000, + y: 8.133333333333333, + }, + { + x: 1593315000000, + y: 8.233333333333333, + }, + { + x: 1593316800000, + y: 8.433333333333334, + }, + { + x: 1593318600000, + y: 8.4, + }, + { + x: 1593320400000, + y: 9.266666666666667, + }, + { + x: 1593322200000, + y: 8.566666666666666, + }, + { + x: 1593324000000, + y: 8.966666666666667, + }, + { + x: 1593325800000, + y: 8.833333333333334, + }, + { + x: 1593327600000, + y: 7.5, + }, + { + x: 1593329400000, + y: 8.033333333333333, + }, + { + x: 1593331200000, + y: 8.633333333333333, + }, + { + x: 1593333000000, + y: 8.5, + }, + { + x: 1593334800000, + y: 8.866666666666667, + }, + { + x: 1593336600000, + y: 8.3, + }, + { + x: 1593338400000, + y: 8.966666666666667, + }, + { + x: 1593340200000, + y: 8.2, + }, + { + x: 1593342000000, + y: 7.566666666666666, + }, + { + x: 1593343800000, + y: 7.5, + }, + { + x: 1593345600000, + y: 7.933333333333334, + }, + { + x: 1593347400000, + y: 7.866666666666666, + }, + { + x: 1593349200000, + y: 7.566666666666666, + }, + { + x: 1593351000000, + y: 7.533333333333333, + }, + { + x: 1593352800000, + y: 8.866666666666667, + }, + { + x: 1593354600000, + y: 8.566666666666666, + }, + { + x: 1593356400000, + y: 8.233333333333333, + }, + { + x: 1593358200000, + y: 8.9, + }, + { + x: 1593360000000, + y: 8.533333333333333, + }, + { + x: 1593361800000, + y: 8.733333333333333, + }, + { + x: 1593363600000, + y: 9.333333333333334, + }, + { + x: 1593365400000, + y: 9.133333333333333, + }, + { + x: 1593367200000, + y: 9.166666666666666, + }, + { + x: 1593369000000, + y: 9.266666666666667, + }, + { + x: 1593370800000, + y: 8.966666666666667, + }, + { + x: 1593372600000, + y: 9.2, + }, + { + x: 1593374400000, + y: 9.433333333333334, + }, + { + x: 1593376200000, + y: 9.166666666666666, + }, + { + x: 1593378000000, + y: 9.266666666666667, + }, + { + x: 1593379800000, + y: 9.5, + }, + { + x: 1593381600000, + y: 9.333333333333334, + }, + { + x: 1593383400000, + y: 8.8, + }, + { + x: 1593385200000, + y: 8.733333333333333, + }, + { + x: 1593387000000, + y: 8.633333333333333, + }, + { + x: 1593388800000, + y: 8.9, + }, + { + x: 1593390600000, + y: 8.533333333333333, + }, + { + x: 1593392400000, + y: 9.3, + }, + { + x: 1593394200000, + y: 9.266666666666667, + }, + { + x: 1593396000000, + y: 8.966666666666667, + }, + { + x: 1593397800000, + y: 8.666666666666666, + }, + { + x: 1593399600000, + y: 9.166666666666666, + }, + { + x: 1593401400000, + y: 8.733333333333333, + }, + { + x: 1593403200000, + y: 8.866666666666667, + }, + { + x: 1593405000000, + y: 8.633333333333333, + }, + { + x: 1593406800000, + y: 8.8, + }, + { + x: 1593408600000, + y: 8.466666666666667, + }, + { + x: 1593410400000, + y: 8.966666666666667, + }, + { + x: 1593412200000, + y: 8.166666666666666, + }, + { + x: 1593414000000, + y: 8.7, + }, + { + x: 1593415800000, + y: 8.333333333333334, + }, + { + x: 1593417600000, + y: 8.666666666666666, + }, + { + x: 1593419400000, + y: 8.533333333333333, + }, + { + x: 1593421200000, + y: 8.233333333333333, + }, + { + x: 1593423000000, + y: 8.3, + }, + { + x: 1593424800000, + y: 7.7, + }, + { + x: 1593426600000, + y: 7.7, + }, + { + x: 1593428400000, + y: 6.133333333333334, + }, + { + x: 1593430200000, + y: 0.4666666666666667, + }, + { + x: 1593442800000, + y: 7.233333333333333, + }, + { + x: 1593444600000, + y: 8.333333333333334, + }, + { + x: 1593446400000, + y: 8.666666666666666, + }, + { + x: 1593448200000, + y: 8.466666666666667, + }, + { + x: 1593450000000, + y: 8.666666666666666, + }, + { + x: 1593451800000, + y: 8.5, + }, + { + x: 1593453600000, + y: 8.6, + }, + { + x: 1593455400000, + y: 8.5, + }, + { + x: 1593457200000, + y: 8.6, + }, + { + x: 1593459000000, + y: 8.866666666666667, + }, + { + x: 1593460800000, + y: 9.166666666666666, + }, + { + x: 1593462600000, + y: 8.4, + }, + { + x: 1593464400000, + y: 8.533333333333333, + }, + { + x: 1593466200000, + y: 8.066666666666666, + }, + { + x: 1593468000000, + y: 8.666666666666666, + }, + { + x: 1593469800000, + y: 8.966666666666667, + }, + { + x: 1593471600000, + y: 8.4, + }, + { + x: 1593473400000, + y: 8.833333333333334, + }, + { + x: 1593475200000, + y: 8.533333333333333, + }, + { + x: 1593477000000, + y: 8.066666666666666, + }, + { + x: 1593478800000, + y: 8.533333333333333, + }, + { + x: 1593480600000, + y: 8.633333333333333, + }, + { + x: 1593482400000, + y: 8.933333333333334, + }, + { + x: 1593484200000, + y: 8.833333333333334, + }, + { + x: 1593486000000, + y: 8.4, + }, + { + x: 1593487800000, + y: 8.633333333333333, + }, + { + x: 1593489600000, + y: 9.333333333333334, + }, + { + x: 1593491400000, + y: 9.366666666666667, + }, + { + x: 1593493200000, + y: 8.333333333333334, + }, + { + x: 1593495000000, + y: 9.266666666666667, + }, + { + x: 1593496800000, + y: 8.2, + }, + { + x: 1593498600000, + y: 8.4, + }, + { + x: 1593500400000, + y: 8.433333333333334, + }, + { + x: 1593502200000, + y: 7.633333333333334, + }, + { + x: 1593504000000, + y: 7.766666666666667, + }, + { + x: 1593505800000, + y: 8.4, + }, + { + x: 1593507600000, + y: 8.3, + }, + { + x: 1593509400000, + y: 8.833333333333334, + }, + { + x: 1593511200000, + y: 8.433333333333334, + }, + { + x: 1593513000000, + y: 8.766666666666667, + }, + { + x: 1593514800000, + y: 9.066666666666666, + }, + { + x: 1593516600000, + y: 8.4, + }, + { + x: 1593518400000, + y: 8.4, + }, + { + x: 1593520200000, + y: 8.8, + }, + { + x: 1593522000000, + y: 8.466666666666667, + }, + { + x: 1593523800000, + y: 8.633333333333333, + }, + { + x: 1593525600000, + y: 9.133333333333333, + }, + { + x: 1593527400000, + y: 8.7, + }, + { + x: 1593529200000, + y: 8.566666666666666, + }, + { + x: 1593531000000, + y: 9.033333333333333, + }, + { + x: 1593532800000, + y: 8.9, + }, + { + x: 1593534600000, + y: 8.7, + }, + { + x: 1593536400000, + y: 8.7, + }, + { + x: 1593538200000, + y: 8.8, + }, + { + x: 1593540000000, + y: 9.166666666666666, + }, + { + x: 1593541800000, + y: 9.033333333333333, + }, + { + x: 1593543600000, + y: 8.733333333333333, + }, + { + x: 1593545400000, + y: 9.2, + }, + { + x: 1593547200000, + y: 8.933333333333334, + }, + { + x: 1593549000000, + y: 9.2, + }, + { + x: 1593550800000, + y: 9.333333333333334, + }, + ], + }, + sample_web_logs: { + label: 'sample_web_logs', + coordinates: [ + { + x: 1593430200000, + y: 0.5666666666666667, + }, + { + x: 1593432000000, + y: 0.36666666666666664, + }, + { + x: 1593433800000, + y: 0.5666666666666667, + }, + { + x: 1593435600000, + y: 0.4666666666666667, + }, + { + x: 1593437400000, + y: 0.36666666666666664, + }, + { + x: 1593439200000, + y: 0.3, + }, + { + x: 1593441000000, + y: 0.13333333333333333, + }, + ], + }, + 'postgresql.log': { + label: 'postgresql.log', + coordinates: [ + { + x: 1593439200000, + y: 0.1, + }, + { + x: 1593441000000, + y: 0.1, + }, + ], + }, + }, +}; + +export const emptyResponse: LogsFetchDataResponse = { + title: 'Logs', + appLink: '/app/logs', + stats: {}, + series: {}, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts new file mode 100644 index 0000000000000..37233b4f6342c --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/metrics.mock.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchMetricsData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: MetricsFetchDataResponse = { + title: 'Metrics', + appLink: '/app/apm', + stats: { + hosts: { value: 11, type: 'number' }, + cpu: { value: 0.8, type: 'percent' }, + memory: { value: 0.362, type: 'percent' }, + inboundTraffic: { value: 1024, type: 'bytesPerSecond' }, + outboundTraffic: { value: 1024, type: 'bytesPerSecond' }, + }, + series: { + outboundTraffic: { + coordinates: [ + { + x: 1589805437549, + y: 331514, + }, + { + x: 1590047357549, + y: 319208, + }, + { + x: 1590289277549, + y: 309648, + }, + { + x: 1590531197549, + y: 280568, + }, + { + x: 1590773117549, + y: 337180, + }, + { + x: 1591015037549, + y: 122468, + }, + { + x: 1591256957549, + y: 184164, + }, + { + x: 1591498877549, + y: 316323, + }, + { + x: 1591740797549, + y: 307351, + }, + { + x: 1591982717549, + y: 290262, + }, + ], + }, + inboundTraffic: { + coordinates: [ + { + x: 1589805437549, + y: 331514, + }, + { + x: 1590047357549, + y: 319208, + }, + { + x: 1590289277549, + y: 309648, + }, + { + x: 1590531197549, + y: 280568, + }, + { + x: 1590773117549, + y: 337180, + }, + { + x: 1591015037549, + y: 122468, + }, + { + x: 1591256957549, + y: 184164, + }, + { + x: 1591498877549, + y: 316323, + }, + { + x: 1591740797549, + y: 307351, + }, + { + x: 1591982717549, + y: 290262, + }, + ], + }, + }, +}; + +export const emptyResponse: MetricsFetchDataResponse = { + title: 'Metrics', + appLink: '/app/apm', + stats: { + hosts: { value: 0, type: 'number' }, + cpu: { value: 0, type: 'percent' }, + memory: { value: 0, type: 'percent' }, + inboundTraffic: { value: 0, type: 'bytesPerSecond' }, + outboundTraffic: { value: 0, type: 'bytesPerSecond' }, + }, + series: { + outboundTraffic: { + coordinates: [], + }, + inboundTraffic: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts new file mode 100644 index 0000000000000..ab5874f8bfcd4 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/mock/uptime.mock.ts @@ -0,0 +1,1218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UptimeFetchDataResponse, FetchData } from '../../../typings'; + +export const fetchUptimeData: FetchData = () => { + return Promise.resolve(response); +}; + +const response: UptimeFetchDataResponse = { + title: 'Uptime', + appLink: '/app/uptime#/', + stats: { + monitors: { + type: 'number', + value: 26, + }, + up: { + type: 'number', + value: 20, + }, + down: { + type: 'number', + value: 6, + }, + }, + series: { + up: { + coordinates: [ + { + x: 1593295200000, + y: 1170, + }, + { + x: 1593297000000, + y: 1170, + }, + { + x: 1593298800000, + y: 1170, + }, + { + x: 1593300600000, + y: 1170, + }, + { + x: 1593302400000, + y: 1170, + }, + { + x: 1593304200000, + y: 1170, + }, + { + x: 1593306000000, + y: 1170, + }, + { + x: 1593307800000, + y: 1170, + }, + { + x: 1593309600000, + y: 1170, + }, + { + x: 1593311400000, + y: 1170, + }, + { + x: 1593313200000, + y: 1170, + }, + { + x: 1593315000000, + y: 1170, + }, + { + x: 1593316800000, + y: 1170, + }, + { + x: 1593318600000, + y: 1170, + }, + { + x: 1593320400000, + y: 1170, + }, + { + x: 1593322200000, + y: 1170, + }, + { + x: 1593324000000, + y: 1170, + }, + { + x: 1593325800000, + y: 1170, + }, + { + x: 1593327600000, + y: 1170, + }, + { + x: 1593329400000, + y: 1170, + }, + { + x: 1593331200000, + y: 1170, + }, + { + x: 1593333000000, + y: 1170, + }, + { + x: 1593334800000, + y: 1170, + }, + { + x: 1593336600000, + y: 1170, + }, + { + x: 1593338400000, + y: 1170, + }, + { + x: 1593340200000, + y: 1170, + }, + { + x: 1593342000000, + y: 1170, + }, + { + x: 1593343800000, + y: 1170, + }, + { + x: 1593345600000, + y: 1170, + }, + { + x: 1593347400000, + y: 1170, + }, + { + x: 1593349200000, + y: 1170, + }, + { + x: 1593351000000, + y: 1170, + }, + { + x: 1593352800000, + y: 1170, + }, + { + x: 1593354600000, + y: 1170, + }, + { + x: 1593356400000, + y: 1170, + }, + { + x: 1593358200000, + y: 1170, + }, + { + x: 1593360000000, + y: 1170, + }, + { + x: 1593361800000, + y: 1170, + }, + { + x: 1593363600000, + y: 1170, + }, + { + x: 1593365400000, + y: 1170, + }, + { + x: 1593367200000, + y: 1170, + }, + { + x: 1593369000000, + y: 1170, + }, + { + x: 1593370800000, + y: 1170, + }, + { + x: 1593372600000, + y: 1170, + }, + { + x: 1593374400000, + y: 1169, + }, + { + x: 1593376200000, + y: 1170, + }, + { + x: 1593378000000, + y: 1170, + }, + { + x: 1593379800000, + y: 1170, + }, + { + x: 1593381600000, + y: 1170, + }, + { + x: 1593383400000, + y: 1170, + }, + { + x: 1593385200000, + y: 1170, + }, + { + x: 1593387000000, + y: 1170, + }, + { + x: 1593388800000, + y: 1170, + }, + { + x: 1593390600000, + y: 1170, + }, + { + x: 1593392400000, + y: 1170, + }, + { + x: 1593394200000, + y: 1239, + }, + { + x: 1593396000000, + y: 1170, + }, + { + x: 1593397800000, + y: 1170, + }, + { + x: 1593399600000, + y: 1170, + }, + { + x: 1593401400000, + y: 1170, + }, + { + x: 1593403200000, + y: 1170, + }, + { + x: 1593405000000, + y: 1170, + }, + { + x: 1593406800000, + y: 1170, + }, + { + x: 1593408600000, + y: 1170, + }, + { + x: 1593410400000, + y: 1170, + }, + { + x: 1593412200000, + y: 1170, + }, + { + x: 1593414000000, + y: 1170, + }, + { + x: 1593415800000, + y: 1170, + }, + { + x: 1593417600000, + y: 1170, + }, + { + x: 1593419400000, + y: 1170, + }, + { + x: 1593421200000, + y: 1170, + }, + { + x: 1593423000000, + y: 1170, + }, + { + x: 1593424800000, + y: 1166, + }, + { + x: 1593426600000, + y: 1206, + }, + { + x: 1593428400000, + y: 1143, + }, + { + x: 1593430200000, + y: 1170, + }, + { + x: 1593432000000, + y: 1170, + }, + { + x: 1593433800000, + y: 1170, + }, + { + x: 1593435600000, + y: 1170, + }, + { + x: 1593437400000, + y: 1170, + }, + { + x: 1593439200000, + y: 1170, + }, + { + x: 1593441000000, + y: 1170, + }, + { + x: 1593442800000, + y: 1170, + }, + { + x: 1593444600000, + y: 1170, + }, + { + x: 1593446400000, + y: 1170, + }, + { + x: 1593448200000, + y: 1170, + }, + { + x: 1593450000000, + y: 1170, + }, + { + x: 1593451800000, + y: 1170, + }, + { + x: 1593453600000, + y: 1170, + }, + { + x: 1593455400000, + y: 1170, + }, + { + x: 1593457200000, + y: 1170, + }, + { + x: 1593459000000, + y: 1170, + }, + { + x: 1593460800000, + y: 1170, + }, + { + x: 1593462600000, + y: 1170, + }, + { + x: 1593464400000, + y: 1170, + }, + { + x: 1593466200000, + y: 1170, + }, + { + x: 1593468000000, + y: 1170, + }, + { + x: 1593469800000, + y: 1170, + }, + { + x: 1593471600000, + y: 1170, + }, + { + x: 1593473400000, + y: 1170, + }, + { + x: 1593475200000, + y: 1170, + }, + { + x: 1593477000000, + y: 1170, + }, + { + x: 1593478800000, + y: 1170, + }, + { + x: 1593480600000, + y: 1201, + }, + { + x: 1593482400000, + y: 1139, + }, + { + x: 1593484200000, + y: 1140, + }, + { + x: 1593486000000, + y: 1140, + }, + { + x: 1593487800000, + y: 1140, + }, + { + x: 1593489600000, + y: 1140, + }, + { + x: 1593491400000, + y: 1140, + }, + { + x: 1593493200000, + y: 1140, + }, + { + x: 1593495000000, + y: 1140, + }, + { + x: 1593496800000, + y: 1140, + }, + { + x: 1593498600000, + y: 1140, + }, + { + x: 1593500400000, + y: 1140, + }, + { + x: 1593502200000, + y: 1140, + }, + { + x: 1593504000000, + y: 1140, + }, + { + x: 1593505800000, + y: 1140, + }, + { + x: 1593507600000, + y: 1140, + }, + { + x: 1593509400000, + y: 1140, + }, + { + x: 1593511200000, + y: 1140, + }, + { + x: 1593513000000, + y: 1140, + }, + { + x: 1593514800000, + y: 1140, + }, + { + x: 1593516600000, + y: 1140, + }, + { + x: 1593518400000, + y: 1140, + }, + { + x: 1593520200000, + y: 1140, + }, + { + x: 1593522000000, + y: 1140, + }, + { + x: 1593523800000, + y: 1140, + }, + { + x: 1593525600000, + y: 1140, + }, + { + x: 1593527400000, + y: 1140, + }, + { + x: 1593529200000, + y: 1140, + }, + { + x: 1593531000000, + y: 1140, + }, + { + x: 1593532800000, + y: 1140, + }, + { + x: 1593534600000, + y: 1140, + }, + { + x: 1593536400000, + y: 1140, + }, + { + x: 1593538200000, + y: 1140, + }, + { + x: 1593540000000, + y: 1140, + }, + { + x: 1593541800000, + y: 1139, + }, + { + x: 1593543600000, + y: 1140, + }, + { + x: 1593545400000, + y: 1140, + }, + { + x: 1593547200000, + y: 1140, + }, + { + x: 1593549000000, + y: 1140, + }, + { + x: 1593550800000, + y: 1140, + }, + { + x: 1593552600000, + y: 1140, + }, + ], + }, + down: { + coordinates: [ + { + x: 1593295200000, + y: 234, + }, + { + x: 1593297000000, + y: 234, + }, + { + x: 1593298800000, + y: 234, + }, + { + x: 1593300600000, + y: 234, + }, + { + x: 1593302400000, + y: 234, + }, + { + x: 1593304200000, + y: 234, + }, + { + x: 1593306000000, + y: 234, + }, + { + x: 1593307800000, + y: 234, + }, + { + x: 1593309600000, + y: 234, + }, + { + x: 1593311400000, + y: 234, + }, + { + x: 1593313200000, + y: 234, + }, + { + x: 1593315000000, + y: 234, + }, + { + x: 1593316800000, + y: 234, + }, + { + x: 1593318600000, + y: 234, + }, + { + x: 1593320400000, + y: 234, + }, + { + x: 1593322200000, + y: 234, + }, + { + x: 1593324000000, + y: 234, + }, + { + x: 1593325800000, + y: 234, + }, + { + x: 1593327600000, + y: 234, + }, + { + x: 1593329400000, + y: 234, + }, + { + x: 1593331200000, + y: 234, + }, + { + x: 1593333000000, + y: 234, + }, + { + x: 1593334800000, + y: 234, + }, + { + x: 1593336600000, + y: 234, + }, + { + x: 1593338400000, + y: 234, + }, + { + x: 1593340200000, + y: 234, + }, + { + x: 1593342000000, + y: 234, + }, + { + x: 1593343800000, + y: 234, + }, + { + x: 1593345600000, + y: 234, + }, + { + x: 1593347400000, + y: 234, + }, + { + x: 1593349200000, + y: 234, + }, + { + x: 1593351000000, + y: 234, + }, + { + x: 1593352800000, + y: 234, + }, + { + x: 1593354600000, + y: 234, + }, + { + x: 1593356400000, + y: 234, + }, + { + x: 1593358200000, + y: 234, + }, + { + x: 1593360000000, + y: 234, + }, + { + x: 1593361800000, + y: 234, + }, + { + x: 1593363600000, + y: 234, + }, + { + x: 1593365400000, + y: 234, + }, + { + x: 1593367200000, + y: 234, + }, + { + x: 1593369000000, + y: 234, + }, + { + x: 1593370800000, + y: 234, + }, + { + x: 1593372600000, + y: 234, + }, + { + x: 1593374400000, + y: 235, + }, + { + x: 1593376200000, + y: 234, + }, + { + x: 1593378000000, + y: 234, + }, + { + x: 1593379800000, + y: 234, + }, + { + x: 1593381600000, + y: 234, + }, + { + x: 1593383400000, + y: 234, + }, + { + x: 1593385200000, + y: 234, + }, + { + x: 1593387000000, + y: 234, + }, + { + x: 1593388800000, + y: 234, + }, + { + x: 1593390600000, + y: 234, + }, + { + x: 1593392400000, + y: 234, + }, + { + x: 1593394200000, + y: 246, + }, + { + x: 1593396000000, + y: 234, + }, + { + x: 1593397800000, + y: 234, + }, + { + x: 1593399600000, + y: 234, + }, + { + x: 1593401400000, + y: 234, + }, + { + x: 1593403200000, + y: 234, + }, + { + x: 1593405000000, + y: 234, + }, + { + x: 1593406800000, + y: 234, + }, + { + x: 1593408600000, + y: 234, + }, + { + x: 1593410400000, + y: 234, + }, + { + x: 1593412200000, + y: 234, + }, + { + x: 1593414000000, + y: 234, + }, + { + x: 1593415800000, + y: 234, + }, + { + x: 1593417600000, + y: 234, + }, + { + x: 1593419400000, + y: 234, + }, + { + x: 1593421200000, + y: 234, + }, + { + x: 1593423000000, + y: 234, + }, + { + x: 1593424800000, + y: 240, + }, + { + x: 1593426600000, + y: 254, + }, + { + x: 1593428400000, + y: 231, + }, + { + x: 1593430200000, + y: 234, + }, + { + x: 1593432000000, + y: 234, + }, + { + x: 1593433800000, + y: 234, + }, + { + x: 1593435600000, + y: 234, + }, + { + x: 1593437400000, + y: 234, + }, + { + x: 1593439200000, + y: 234, + }, + { + x: 1593441000000, + y: 234, + }, + { + x: 1593442800000, + y: 234, + }, + { + x: 1593444600000, + y: 234, + }, + { + x: 1593446400000, + y: 234, + }, + { + x: 1593448200000, + y: 234, + }, + { + x: 1593450000000, + y: 234, + }, + { + x: 1593451800000, + y: 234, + }, + { + x: 1593453600000, + y: 234, + }, + { + x: 1593455400000, + y: 234, + }, + { + x: 1593457200000, + y: 234, + }, + { + x: 1593459000000, + y: 234, + }, + { + x: 1593460800000, + y: 234, + }, + { + x: 1593462600000, + y: 234, + }, + { + x: 1593464400000, + y: 234, + }, + { + x: 1593466200000, + y: 234, + }, + { + x: 1593468000000, + y: 234, + }, + { + x: 1593469800000, + y: 234, + }, + { + x: 1593471600000, + y: 234, + }, + { + x: 1593473400000, + y: 234, + }, + { + x: 1593475200000, + y: 234, + }, + { + x: 1593477000000, + y: 234, + }, + { + x: 1593478800000, + y: 234, + }, + { + x: 1593480600000, + y: 254, + }, + { + x: 1593482400000, + y: 265, + }, + { + x: 1593484200000, + y: 264, + }, + { + x: 1593486000000, + y: 264, + }, + { + x: 1593487800000, + y: 264, + }, + { + x: 1593489600000, + y: 264, + }, + { + x: 1593491400000, + y: 264, + }, + { + x: 1593493200000, + y: 264, + }, + { + x: 1593495000000, + y: 264, + }, + { + x: 1593496800000, + y: 264, + }, + { + x: 1593498600000, + y: 264, + }, + { + x: 1593500400000, + y: 264, + }, + { + x: 1593502200000, + y: 264, + }, + { + x: 1593504000000, + y: 264, + }, + { + x: 1593505800000, + y: 264, + }, + { + x: 1593507600000, + y: 264, + }, + { + x: 1593509400000, + y: 264, + }, + { + x: 1593511200000, + y: 264, + }, + { + x: 1593513000000, + y: 264, + }, + { + x: 1593514800000, + y: 264, + }, + { + x: 1593516600000, + y: 264, + }, + { + x: 1593518400000, + y: 264, + }, + { + x: 1593520200000, + y: 264, + }, + { + x: 1593522000000, + y: 264, + }, + { + x: 1593523800000, + y: 264, + }, + { + x: 1593525600000, + y: 264, + }, + { + x: 1593527400000, + y: 264, + }, + { + x: 1593529200000, + y: 264, + }, + { + x: 1593531000000, + y: 264, + }, + { + x: 1593532800000, + y: 264, + }, + { + x: 1593534600000, + y: 264, + }, + { + x: 1593536400000, + y: 264, + }, + { + x: 1593538200000, + y: 264, + }, + { + x: 1593540000000, + y: 264, + }, + { + x: 1593541800000, + y: 265, + }, + { + x: 1593543600000, + y: 264, + }, + { + x: 1593545400000, + y: 264, + }, + { + x: 1593547200000, + y: 264, + }, + { + x: 1593549000000, + y: 264, + }, + { + x: 1593550800000, + y: 264, + }, + { + x: 1593552600000, + y: 264, + }, + ], + }, + }, +}; + +export const emptyResponse: UptimeFetchDataResponse = { + title: 'Uptime', + appLink: '/app/uptime#/', + stats: { + monitors: { + type: 'number', + value: 0, + }, + up: { + type: 'number', + value: 0, + }, + down: { + type: 'number', + value: 0, + }, + }, + series: { + up: { + coordinates: [], + }, + down: { + coordinates: [], + }, + }, +}; diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx new file mode 100644 index 0000000000000..b88614b22e81a --- /dev/null +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -0,0 +1,534 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import { AppMountContext } from 'kibana/public'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; +import { PluginContext } from '../../context/plugin_context'; +import { registerDataHandler, unregisterDataHandler } from '../../data_handler'; +import { emptyResponse as emptyAPMResponse, fetchApmData } from './mock/apm.mock'; +import { fetchLogsData, emptyResponse as emptyLogsResponse } from './mock/logs.mock'; +import { fetchMetricsData, emptyResponse as emptyMetricsResponse } from './mock/metrics.mock'; +import { fetchUptimeData, emptyResponse as emptyUptimeResponse } from './mock/uptime.mock'; +import { EuiThemeProvider } from '../../typings'; +import { OverviewPage } from './'; +import { alertsFetchData } from './mock/alerts.mock'; + +const core = { + http: { + basePath: { + prepend: (link) => `http://localhost:5601${link}`, + }, + }, + uiSettings: { + get: (key: string) => { + const euiSettings = { + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: true, + value: 1000, + }, + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + }; + // @ts-expect-error + return euiSettings[key]; + }, + }, +} as AppMountContext['core']; + +const coreWithAlerts = ({ + ...core, + http: { + ...core.http, + get: alertsFetchData, + }, +} as unknown) as AppMountContext['core']; + +function unregisterAll() { + unregisterDataHandler({ appName: 'apm' }); + unregisterDataHandler({ appName: 'infra_logs' }); + unregisterDataHandler({ appName: 'infra_metrics' }); + unregisterDataHandler({ appName: 'uptime' }); +} + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('Empty state', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => false, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => false, + }); + + return ; + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('single panel', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs and metrics', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM and Uptime', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('logs, metrics, APM, Uptime and Alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('no data', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: async () => emptyAPMResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => emptyLogsResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => emptyMetricsResponse, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: async () => emptyUptimeResponse, + hasData: async () => true, + }); + return ( + + ); + }); + +const coreAlertsThrowsError = ({ + ...core, + http: { + ...core.http, + get: async () => { + throw new Error('Error fetching Alerts data'); + }, + }, +} as unknown) as AppMountContext['core']; +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('fetch data with error', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: async () => { + throw new Error('Error fetching APM data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => { + throw new Error('Error fetching Logs data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => { + throw new Error('Error fetching Metric data'); + }, + hasData: async () => true, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: async () => { + throw new Error('Error fetching Uptime data'); + }, + hasData: async () => true, + }); + return ( + + ); + }); + +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('hasData with error and alerts', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + return ( + + ); + }); +storiesOf('app/Overview', module) + .addDecorator((storyFn) => ( + + + {storyFn()}) + + + )) + .add('hasData with error', () => { + unregisterAll(); + registerDataHandler({ + appName: 'apm', + fetchData: fetchApmData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_logs', + fetchData: fetchLogsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'infra_metrics', + fetchData: fetchMetricsData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + registerDataHandler({ + appName: 'uptime', + fetchData: fetchUptimeData, + // @ts-ignore thows an error instead + hasData: async () => { + new Error('Error has data'); + }, + }); + return ( + + ); + }); diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx new file mode 100644 index 0000000000000..10f9b4dc42723 --- /dev/null +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import * as t from 'io-ts'; +import { i18n } from '@kbn/i18n'; +import { HomePage } from '../pages/home'; +import { LandingPage } from '../pages/landing'; +import { OverviewPage } from '../pages/overview'; +import { jsonRt } from './json_rt'; + +export type RouteParams = DecodeParams; + +type DecodeParams = { + [key in keyof TParams]: TParams[key] extends t.Any ? t.TypeOf : never; +}; + +export interface Params { + query?: t.HasProps; + path?: t.HasProps; +} +export const routes = { + '/': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.home.breadcrumb', { + defaultMessage: 'Overview', + }), + }, + ], + }, + '/landing': { + handler: () => { + return ; + }, + params: {}, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.landing.breadcrumb', { + defaultMessage: 'Getting started', + }), + }, + ], + }, + '/overview': { + handler: ({ query }: any) => { + return ; + }, + params: { + query: t.partial({ + rangeFrom: t.string, + rangeTo: t.string, + refreshPaused: jsonRt.pipe(t.boolean), + refreshInterval: jsonRt.pipe(t.number), + }), + }, + breadcrumb: [ + { + text: i18n.translate('xpack.observability.overview.breadcrumb', { + defaultMessage: 'Overview', + }), + }, + ], + }, +}; diff --git a/x-pack/plugins/observability/public/routes/json_rt.ts b/x-pack/plugins/observability/public/routes/json_rt.ts new file mode 100644 index 0000000000000..fcc73547a686b --- /dev/null +++ b/x-pack/plugins/observability/public/routes/json_rt.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; + +export const jsonRt = new t.Type( + 'JSON', + t.any.is, + (input, context) => + either.chain(t.string.validate(input, context), (str) => { + try { + return t.success(JSON.parse(str)); + } catch (e) { + return t.failure(input, context); + } + }), + (a) => JSON.stringify(a) +); diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts new file mode 100644 index 0000000000000..dd3f476fe7d53 --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AppMountContext } from 'kibana/public'; +import { getObservabilityAlerts } from './get_observability_alerts'; + +describe('getObservabilityAlerts', () => { + it('Returns empty array when api throws exception', async () => { + const core = ({ + http: { + get: async () => { + throw new Error('Boom'); + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([]); + }); + + it('Returns empty array when api return undefined', async () => { + const core = ({ + http: { + get: async () => { + return { + data: undefined, + }; + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([]); + }); + + it('Shows alerts from Observability', async () => { + const core = ({ + http: { + get: async () => { + return { + data: [ + { + id: 1, + consumer: 'siem', + }, + { + id: 2, + consumer: 'apm', + }, + { + id: 3, + consumer: 'uptime', + }, + { + id: 4, + consumer: 'logs', + }, + { + id: 5, + consumer: 'metrics', + }, + ], + }; + }, + }, + } as unknown) as AppMountContext['core']; + + const alerts = await getObservabilityAlerts({ core }); + expect(alerts).toEqual([ + { + id: 2, + consumer: 'apm', + }, + { + id: 3, + consumer: 'uptime', + }, + { + id: 4, + consumer: 'logs', + }, + { + id: 5, + consumer: 'metrics', + }, + ]); + }); +}); diff --git a/x-pack/plugins/observability/public/services/get_observability_alerts.ts b/x-pack/plugins/observability/public/services/get_observability_alerts.ts new file mode 100644 index 0000000000000..1bbabbad2834a --- /dev/null +++ b/x-pack/plugins/observability/public/services/get_observability_alerts.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AppMountContext } from 'kibana/public'; +import { Alert } from '../../../alerts/common'; + +export async function getObservabilityAlerts({ core }: { core: AppMountContext['core'] }) { + try { + const { data = [] }: { data: Alert[] } = await core.http.get('/api/alerts/_find', { + query: { + page: 1, + per_page: 20, + }, + }); + + return data.filter(({ consumer }) => { + return ( + consumer === 'apm' || consumer === 'uptime' || consumer === 'logs' || consumer === 'metrics' + ); + }); + } catch (e) { + return []; + } +} diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index e57dfebb36419..2dafd70896cc5 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -6,11 +6,9 @@ import { ObservabilityApp } from '../../../typings/common'; -interface Stat { +export interface Stat { type: 'number' | 'percent' | 'bytesPerSecond'; - label: string; value: number; - color?: string; } export interface Coordinates { @@ -18,10 +16,8 @@ export interface Coordinates { y?: number; } -interface Series { - label: string; +export interface Series { coordinates: Coordinates[]; - color?: string; } export interface FetchDataParams { @@ -50,8 +46,8 @@ export interface FetchDataResponse { } export interface LogsFetchDataResponse extends FetchDataResponse { - stats: Record; - series: Record; + stats: Record; + series: Record; } export interface MetricsFetchDataResponse extends FetchDataResponse { diff --git a/x-pack/plugins/observability/public/typings/section/index.ts b/x-pack/plugins/observability/public/typings/section/index.ts new file mode 100644 index 0000000000000..f336b6b981687 --- /dev/null +++ b/x-pack/plugins/observability/public/typings/section/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ObservabilityApp } from '../../../typings/common'; + +export interface ISection { + id: ObservabilityApp | 'alert'; + title: string; + icon: string; + description: string; + href?: string; + linkTitle?: string; + target?: '_blank'; +} diff --git a/x-pack/plugins/observability/public/utils/date.ts b/x-pack/plugins/observability/public/utils/date.ts new file mode 100644 index 0000000000000..fc0bbdae20cb9 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/date.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import datemath from '@elastic/datemath'; + +export function getParsedDate(range?: string, opts = {}) { + if (range) { + const parsed = datemath.parse(range, opts); + if (parsed) { + return parsed.toISOString(); + } + } +} diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.test.ts b/x-pack/plugins/observability/public/utils/format_stat_value.test.ts new file mode 100644 index 0000000000000..6643692e02dd4 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/format_stat_value.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { formatStatValue } from './format_stat_value'; +import { Stat } from '../typings'; + +describe('formatStatValue', () => { + it('formats value as number', () => { + const stat = { + type: 'number', + label: 'numeral stat', + value: 1000, + } as Stat; + expect(formatStatValue(stat)).toEqual('1k'); + }); + it('formats value as bytes', () => { + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1, + } as Stat) + ).toEqual('1.0B/s'); + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1048576, + } as Stat) + ).toEqual('1.0MB/s'); + expect( + formatStatValue({ + type: 'bytesPerSecond', + label: 'bytes stat', + value: 1073741824, + } as Stat) + ).toEqual('1.0GB/s'); + }); + it('formats value as percent', () => { + const stat = { + type: 'percent', + label: 'percent stat', + value: 0.841, + } as Stat; + expect(formatStatValue(stat)).toEqual('84.1%'); + }); +}); diff --git a/x-pack/plugins/observability/public/utils/format_stat_value.ts b/x-pack/plugins/observability/public/utils/format_stat_value.ts new file mode 100644 index 0000000000000..c200d94d5699e --- /dev/null +++ b/x-pack/plugins/observability/public/utils/format_stat_value.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import numeral from '@elastic/numeral'; +import { Stat } from '../typings'; + +export function formatStatValue(stat: Stat) { + const { value, type } = stat; + switch (type) { + case 'bytesPerSecond': + return `${numeral(value).format('0.0b')}/s`; + case 'number': + return numeral(value).format('0a'); + case 'percent': + return numeral(value).format('0.0%'); + } +} diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js new file mode 100644 index 0000000000000..1608003641596 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/calculate_auto.js @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +const d = moment.duration; + +const roundingRules = [ + [d(500, 'ms'), d(100, 'ms')], + [d(5, 'second'), d(1, 'second')], + [d(7.5, 'second'), d(5, 'second')], + [d(15, 'second'), d(10, 'second')], + [d(45, 'second'), d(30, 'second')], + [d(3, 'minute'), d(1, 'minute')], + [d(9, 'minute'), d(5, 'minute')], + [d(20, 'minute'), d(10, 'minute')], + [d(45, 'minute'), d(30, 'minute')], + [d(2, 'hour'), d(1, 'hour')], + [d(6, 'hour'), d(3, 'hour')], + [d(24, 'hour'), d(12, 'hour')], + [d(1, 'week'), d(1, 'd')], + [d(3, 'week'), d(1, 'week')], + [d(1, 'year'), d(1, 'month')], + [Infinity, d(1, 'year')], +]; + +const revRoundingRules = roundingRules.slice(0).reverse(); + +function find(rules, check, last) { + function pick(buckets, duration) { + const target = duration / buckets; + let lastResp = null; + + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + const resp = check(rule[0], rule[1], target); + + if (resp == null) { + if (!last) continue; + if (lastResp) return lastResp; + break; + } + + if (!last) return resp; + lastResp = resp; + } + + // fallback to just a number of milliseconds, ensure ms is >= 1 + const ms = Math.max(Math.floor(target), 1); + return moment.duration(ms, 'ms'); + } + + return (buckets, duration) => { + const interval = pick(buckets, duration); + if (interval) return moment.duration(interval._data); + }; +} + +export const calculateAuto = { + near: find( + revRoundingRules, + function near(bound, interval, target) { + if (bound > target) return interval; + }, + true + ), + + lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) { + if (interval < target) return interval; + }), + + atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) { + if (interval <= target) return interval; + }), +}; diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts new file mode 100644 index 0000000000000..39c4aedaa6013 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { getBucketSize } from './index'; +import moment from 'moment'; + +describe('getBuckets', () => { + describe("minInterval 'auto'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 10, + intervalString: '10s', + }); + }); + it('last 1 hour', () => { + const start = moment().subtract(1, 'hour').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 week', () => { + const start = moment().subtract(1, 'week').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 3600, + intervalString: '3600s', + }); + }); + it('last 30 days', () => { + const start = moment().subtract(30, 'days').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 43200, + intervalString: '43200s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); + describe("minInterval '30s'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts new file mode 100644 index 0000000000000..5673b890adf33 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +// @ts-ignore +import { calculateAuto } from './calculate_auto'; +import { unitToSeconds } from './unit_to_seconds'; + +export function getBucketSize({ + start, + end, + minInterval, +}: { + start: number; + end: number; + minInterval: string; +}) { + const duration = moment.duration(end - start, 'ms'); + const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1); + const intervalString = `${bucketSize}s`; + const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/); + const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0; + + if (bucketSize < minBucketSize) { + return { + bucketSize: minBucketSize, + intervalString: minInterval, + }; + } + + return { bucketSize, intervalString }; +} diff --git a/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts new file mode 100644 index 0000000000000..657726d988495 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_bucket_size/unit_to_seconds.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment, { unitOfTime as UnitOfTIme } from 'moment'; + +function getDurationAsSeconds(value: number, unitOfTime: UnitOfTIme.Base) { + return moment.duration(value, unitOfTime).asSeconds(); +} + +const units = { + ms: getDurationAsSeconds(1, 'millisecond'), + s: getDurationAsSeconds(1, 'second'), + m: getDurationAsSeconds(1, 'minute'), + h: getDurationAsSeconds(1, 'hour'), + d: getDurationAsSeconds(1, 'day'), + w: getDurationAsSeconds(1, 'week'), + M: getDurationAsSeconds(1, 'month'), + y: getDurationAsSeconds(1, 'year'), +}; + +export function unitToSeconds(unit: string) { + return units[unit as keyof typeof units]; +} diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx new file mode 100644 index 0000000000000..2a290f2b24d6b --- /dev/null +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render as testLibRender } from '@testing-library/react'; +import { AppMountContext } from 'kibana/public'; +import { PluginContext } from '../context/plugin_context'; +import { EuiThemeProvider } from '../typings'; + +export const core = ({ + http: { + basePath: { + prepend: jest.fn(), + }, + }, +} as unknown) as AppMountContext['core']; + +export const render = (component: React.ReactNode) => { + return testLibRender( + + {component} + + ); +}; diff --git a/x-pack/plugins/observability/public/utils/url.ts b/x-pack/plugins/observability/public/utils/url.ts new file mode 100644 index 0000000000000..962ab8233a8f5 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/url.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { parse, stringify } from 'query-string'; +import { url } from '../../../../../src/plugins/kibana_utils/public'; + +export function toQuery(search?: string) { + return search ? parse(search.slice(1), { sort: false }) : {}; +} + +export function fromQuery(query: Record) { + const encodedQuery = url.encodeQuery(query, (value) => + encodeURIComponent(value).replace(/%3A/g, ':') + ); + + return stringify(encodedQuery, { sort: false, encode: false }); +} diff --git a/x-pack/plugins/observability/scripts/storybook.js b/x-pack/plugins/observability/scripts/storybook.js new file mode 100644 index 0000000000000..e9db98e2adf6b --- /dev/null +++ b/x-pack/plugins/observability/scripts/storybook.js @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'observability', + storyGlobs: [ + join(__dirname, '..', 'public', 'components', '**', '*.stories.tsx'), + join(__dirname, '..', 'public', 'pages', '**', '*.stories.tsx'), + ], +}); diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js index 4ff189d8f1be0..643cc3efb0136 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js @@ -100,6 +100,7 @@ export class RollupIndexPatternCreationConfig extends IndexPatternCreationConfig { key: this.type, name: rollupIndexPatternIndexLabel, + color: 'primary', }, ] : []; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index a34a76361f799..bf9cf7486810d 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -51,7 +51,7 @@ export const APP_HOSTS_PATH = `${APP_PATH}/hosts`; export const APP_NETWORK_PATH = `${APP_PATH}/network`; export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`; export const APP_CASES_PATH = `${APP_PATH}/cases`; -export const APP_MANAGEMENT_PATH = `${APP_PATH}/management`; +export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`; /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const DEFAULT_INDEX_PATTERN = [ diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index efd9ece8aec56..9438c28f05fef 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -99,6 +99,6 @@ describe('Cases', () => { cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title); cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description); - cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query); + cy.get(TIMELINE_QUERY).invoke('text').should('eq', case1.timeline.query); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 0c3424576e4cf..6b3fc9e751ea4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -27,74 +27,67 @@ import { describe('ml conditional links', () => { it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(process.name: "conhost.exe" or process.name: "sc.exe")' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")'); }); it('sets the KQL from a multiple IPs with a null for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should( + 'eq', + '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2"))' + ); }); it('sets the KQL from a multiple IPs with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should( + 'eq', + '((source.ip: "127.0.0.1" or destination.ip: "127.0.0.1") or (source.ip: "127.0.0.2" or destination.ip: "127.0.0.2")) and ((process.name: "conhost.exe" or process.name: "sc.exe"))' + ); }); it('sets the KQL from a $ip$ with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(process.name: "conhost.exe" or process.name: "sc.exe")' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")'); }); it('sets the KQL from a single host name with a value for query', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(process.name: "conhost.exe" or process.name: "sc.exe")' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")'); }); it('sets the KQL from a multiple host names with null for query', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(host.name: "siem-windows" or host.name: "siem-suricata")' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should('eq', '(host.name: "siem-windows" or host.name: "siem-suricata")'); }); it('sets the KQL from a multiple host names with a value for query', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should( + 'eq', + '(host.name: "siem-windows" or host.name: "siem-suricata") and ((process.name: "conhost.exe" or process.name: "sc.exe"))' + ); }); it('sets the KQL from a undefined/null host name but with a value for query', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); - cy.get(KQL_INPUT).should( - 'have.attr', - 'value', - '(process.name: "conhost.exe" or process.name: "sc.exe")' - ); + cy.get(KQL_INPUT) + .invoke('text') + .should('eq', '(process.name: "conhost.exe" or process.name: "sc.exe")'); }); it('redirects from a single IP with a null for the query', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index ea3a78c77152a..7864160d5bca0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -21,7 +21,7 @@ import { CASES_URL, HOSTS_URL, KIBANA_HOME, - MANAGEMENT_URL, + ADMINISTRATION_URL, NETWORK_URL, OVERVIEW_URL, TIMELINES_URL, @@ -31,7 +31,7 @@ import { ALERTS_PAGE, CASES_PAGE, HOSTS_PAGE, - MANAGEMENT_PAGE, + ADMINISTRATION_PAGE, NETWORK_PAGE, OVERVIEW_PAGE, TIMELINES_PAGE, @@ -72,9 +72,9 @@ describe('top-level navigation common to all pages in the Security app', () => { cy.url().should('include', CASES_URL); }); - it('navigates to the Management page', () => { + it('navigates to the Administration page', () => { navigateFromHeaderTo(MANAGEMENT); - cy.url().should('include', MANAGEMENT_URL); + cy.url().should('include', ADMINISTRATION_URL); }); }); @@ -115,8 +115,8 @@ describe('Kibana navigation to all pages in the Security app ', () => { cy.url().should('include', CASES_URL); }); - it('navigates to the Management page', () => { - navigateFromKibanaCollapsibleTo(MANAGEMENT_PAGE); - cy.url().should('include', MANAGEMENT_URL); + it('navigates to the Administration page', () => { + navigateFromKibanaCollapsibleTo(ADMINISTRATION_PAGE); + cy.url().should('include', ADMINISTRATION_URL); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index a3a927cbea7d4..81af9ece9ed45 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -154,12 +154,12 @@ describe('url state', () => { it('sets kql on network page', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlNetworkNetwork); - cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); + cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"'); }); it('sets kql on hosts page', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); - cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); + cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"'); }); it('sets the url state when kql is set', () => { @@ -230,8 +230,7 @@ describe('url state', () => { it('Do not clears kql when navigating to a new page', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); navigateFromHeaderTo(NETWORK); - - cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); + cy.get(KQL_INPUT).invoke('text').should('eq', 'source.ip: "10.142.0.9"'); }); it.skip('sets and reads the url state for timeline by id', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts index 2f7956ce370bc..eeec19fa3dd1e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts +++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts @@ -12,8 +12,8 @@ export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [titl export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]'; -export const MANAGEMENT_PAGE = - '[data-test-subj="collapsibleNavGroup-security"] [title="Management"]'; +export const ADMINISTRATION_PAGE = + '[data-test-subj="collapsibleNavGroup-security"] [title="Administration"]'; export const NETWORK_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Network"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index eca5885e7b3d9..88ae582b58891 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -82,7 +82,7 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); - cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -91,7 +91,7 @@ export const fillDefineCustomRuleAndContinue = (rule: CustomRule) => { export const fillDefineCustomRuleWithImportedQueryAndContinue = (rule: CustomRule) => { cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); cy.get(TIMELINE(rule.timelineId)).click(); - cy.get(CUSTOM_QUERY_INPUT).should('have.attr', 'value', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 9e17433090c2b..761fd2c1e6a0b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -58,7 +58,7 @@ export const createNewTimeline = () => { }; export const executeTimelineKQL = (query: string) => { - cy.get(`${SEARCH_OR_FILTER_CONTAINER} input`).type(`${query} {enter}`); + cy.get(`${SEARCH_OR_FILTER_CONTAINER} textarea`).type(`${query} {enter}`); }; export const expandFirstTimelineEventDetails = () => { diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 9da9abf388e4d..e53dac157eed7 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -16,7 +16,7 @@ export const HOSTS_PAGE_TAB_URLS = { uncommonProcesses: '/app/security/hosts/uncommonProcesses', }; export const KIBANA_HOME = '/app/home#/'; -export const MANAGEMENT_URL = '/app/security/management'; +export const ADMINISTRATION_URL = '/app/security/administration'; export const NETWORK_URL = '/app/security/network'; export const OVERVIEW_URL = '/app/security/overview'; export const TIMELINES_URL = '/app/security/timelines'; diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx index 88e9d4179a971..d7acda4988570 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx @@ -27,7 +27,7 @@ export const navTabs: SiemNavTab = { }, [SecurityPageName.alerts]: { id: SecurityPageName.alerts, - name: i18n.Alerts, + name: i18n.ALERTS, href: APP_ALERTS_PATH, disabled: false, urlKey: 'alerts', @@ -63,7 +63,7 @@ export const navTabs: SiemNavTab = { }, [SecurityPageName.management]: { id: SecurityPageName.management, - name: i18n.MANAGEMENT, + name: i18n.ADMINISTRATION, href: APP_MANAGEMENT_PATH, disabled: false, urlKey: SecurityPageName.management, diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts index f5a08e6395f1f..bee1dfe333851 100644 --- a/x-pack/plugins/security_solution/public/app/home/translations.ts +++ b/x-pack/plugins/security_solution/public/app/home/translations.ts @@ -25,7 +25,7 @@ export const DETECTION_ENGINE = i18n.translate( } ); -export const Alerts = i18n.translate('xpack.securitySolution.navigation.alerts', { +export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', { defaultMessage: 'Alerts', }); @@ -37,6 +37,6 @@ export const CASE = i18n.translate('xpack.securitySolution.navigation.case', { defaultMessage: 'Cases', }); -export const MANAGEMENT = i18n.translate('xpack.securitySolution.navigation.management', { - defaultMessage: 'Management', +export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.administration', { + defaultMessage: 'Administration', }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index 888c881f45ce4..483ca5d6d332e 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -7,8 +7,8 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { IFieldType } from '../../../../../../../src/plugins/data/common'; -import { Ipv4Address } from '../../../../../../../src/plugins/kibana_utils/public'; +import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common'; + import { EXCEPTION_OPERATORS, isOperator, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 10f8b11b4d9c5..2ad83d37576b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -108,9 +108,9 @@ describe('SIEM Navigation', () => { }, management: { disabled: false, - href: '/app/security/management', + href: '/app/security/administration', id: 'management', - name: 'Management', + name: 'Administration', urlKey: 'management', }, hosts: { @@ -220,9 +220,9 @@ describe('SIEM Navigation', () => { }, management: { disabled: false, - href: '/app/security/management', + href: '/app/security/administration', id: 'management', - name: 'Management', + name: 'Administration', urlKey: 'management', }, network: { diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx index a3cab1cfabd71..aac83ce650d86 100644 --- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx @@ -214,15 +214,18 @@ describe('QueryBar ', () => { /> ); - const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + let queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); queryInput.simulate('change', { target: { value: 'host.name:*' } }); - expect(queryInput.html()).toContain('value="host.name:*"'); + wrapper.update(); + queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); + expect(queryInput.props().children).toBe('host.name:*'); wrapper.setProps({ filterQueryDraft: null }); wrapper.update(); + queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); - expect(queryInput.html()).toContain('value=""'); + expect(queryInput.props().children).toBe(''); }); }); @@ -258,7 +261,7 @@ describe('QueryBar ', () => { const onSubmitQueryRef = searchBarProps.onQuerySubmit; const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + const queryInput = wrapper.find(QueryBar).find('textarea[data-test-subj="queryInput"]'); queryInput.simulate('change', { target: { value: 'hello: world' } }); wrapper.update(); diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 0fad1273c7279..4bc586bdee8a9 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -10,7 +10,7 @@ import { SecurityPageName } from '../../app/types'; // --[ ROUTING ]--------------------------------------------------------------------------- export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`; export const MANAGEMENT_ROUTING_ROOT_PATH = ''; -export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; +export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.hosts})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`; @@ -21,5 +21,5 @@ export const MANAGEMENT_STORE_GLOBAL_NAMESPACE: ManagementStoreGlobalNamespace = export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList'; /** Namespace within the Management state where policy details state is maintained */ export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; -/** Namespace within the Management state where endpoints state is maintained */ -export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints'; +/** Namespace within the Management state where hosts state is maintained */ +export const MANAGEMENT_STORE_HOSTS_NAMESPACE = 'hosts'; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 92eb7717318d3..5add6b753a7a9 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -10,7 +10,7 @@ import { generatePath } from 'react-router-dom'; import querystring from 'querystring'; import { - MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_HOSTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, } from './constants'; @@ -32,11 +32,11 @@ const querystringStringify: ( ) => string = querystring.stringify; /** Make `selected_host` required */ -type EndpointDetailsUrlProps = Omit & +type HostDetailsUrlProps = Omit & Required>; -export const getEndpointListPath = ( - props: { name: 'default' | 'endpointList' } & HostIndexUIQueryParams, +export const getHostListPath = ( + props: { name: 'default' | 'hostList' } & HostIndexUIQueryParams, search?: string ) => { const { name, ...queryParams } = props; @@ -45,29 +45,27 @@ export const getEndpointListPath = ( ); const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; - if (name === 'endpointList') { - return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - tabName: ManagementSubTab.endpoints, + if (name === 'hostList') { + return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, { + tabName: ManagementSubTab.hosts, })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; } return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; }; -export const getEndpointDetailsPath = ( - props: { name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps, +export const getHostDetailsPath = ( + props: { name: 'hostDetails' | 'hostPolicyResponse' } & HostDetailsUrlProps, search?: string ) => { const { name, ...queryParams } = props; - queryParams.show = (props.name === 'endpointPolicyResponse' + queryParams.show = (props.name === 'hostPolicyResponse' ? 'policy_response' : '') as HostIndexUIQueryParams['show']; - const urlQueryParams = querystringStringify( - queryParams - ); + const urlQueryParams = querystringStringify(queryParams); const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; - return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - tabName: ManagementSubTab.endpoints, + return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, { + tabName: ManagementSubTab.hosts, })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; }; diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index 5dd47d4e88028..c3d6cb48e4dae 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -103,7 +103,7 @@ const PolicyEmptyState = React.memo<{ ); }); -const EndpointsEmptyState = React.memo<{ +const HostsEmptyState = React.memo<{ loading: boolean; onActionClick: (event: MouseEvent) => void; actionDisabled: boolean; @@ -113,14 +113,14 @@ const EndpointsEmptyState = React.memo<{ const policySteps = useMemo( () => [ { - title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepOneTitle', { + title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', { defaultMessage: 'Select a policy you created from the list below.', }), children: ( <> @@ -138,7 +138,7 @@ const EndpointsEmptyState = React.memo<{ return loading ? ( @@ -146,7 +146,7 @@ const EndpointsEmptyState = React.memo<{ list ) : ( ); @@ -156,14 +156,14 @@ const EndpointsEmptyState = React.memo<{ ), }, { - title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepTwoTitle', { + title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepTwoTitle', { defaultMessage: 'Head over to Ingest to deploy your Agent with Endpoint Security enabled.', }), children: ( @@ -178,18 +178,18 @@ const EndpointsEmptyState = React.memo<{ loading={loading} onActionClick={onActionClick} actionDisabled={actionDisabled} - dataTestSubj="emptyEndpointsTable" + dataTestSubj="emptyHostsTable" steps={policySteps} headerComponent={ } bodyComponent={ } /> @@ -271,7 +271,7 @@ const ManagementEmptyState = React.memo<{ ); PolicyEmptyState.displayName = 'PolicyEmptyState'; -EndpointsEmptyState.displayName = 'EndpointsEmptyState'; +HostsEmptyState.displayName = 'HostsEmptyState'; ManagementEmptyState.displayName = 'ManagementEmptyState'; -export { PolicyEmptyState, EndpointsEmptyState, ManagementEmptyState }; +export { PolicyEmptyState, HostsEmptyState, ManagementEmptyState }; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index c3dbb93b369a9..8495628709d2a 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -11,7 +11,7 @@ import { PageView, PageViewProps } from '../../common/components/endpoint/page_v import { ManagementSubTab } from '../types'; import { SecurityPageName } from '../../app/types'; import { useFormatUrl } from '../../common/components/link_to'; -import { getEndpointListPath, getPoliciesPath } from '../common/routing'; +import { getHostListPath, getPoliciesPath } from '../common/routing'; import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; export const ManagementPageView = memo>((options) => { @@ -19,7 +19,7 @@ export const ManagementPageView = memo>((options) => const { tabName } = useParams<{ tabName: ManagementSubTab }>(); const goToEndpoint = useNavigateByRouterEventHandler( - getEndpointListPath({ name: 'endpointList' }, search) + getHostListPath({ name: 'hostList' }, search) ); const goToPolicies = useNavigateByRouterEventHandler(getPoliciesPath(search)); @@ -31,11 +31,11 @@ export const ManagementPageView = memo>((options) => return [ { name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', { - defaultMessage: 'Endpoints', + defaultMessage: 'Hosts', }), - id: ManagementSubTab.endpoints, - isSelected: tabName === ManagementSubTab.endpoints, - href: formatUrl(getEndpointListPath({ name: 'endpointList' })), + id: ManagementSubTab.hosts, + isSelected: tabName === ManagementSubTab.hosts, + href: formatUrl(getHostListPath({ name: 'hostList' })), onClick: goToEndpoint, }, { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx index ff7f522b9bc52..a970edd4d30f4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx @@ -7,19 +7,19 @@ import { Switch, Route } from 'react-router-dom'; import React, { memo } from 'react'; import { HostList } from './view'; -import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../common/constants'; +import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../common/constants'; import { NotFoundPage } from '../../../app/404'; /** - * Provides the routing container for the endpoints related views + * Provides the routing container for the hosts related views */ -export const EndpointsContainer = memo(() => { +export const HostsContainer = memo(() => { return ( - + ); }); -EndpointsContainer.displayName = 'EndpointsContainer'; +HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts index ae2ce9facc837..533b14e50f3dd 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts @@ -24,7 +24,7 @@ import { MiddlewareActionSpyHelper, createSpyMiddleware, } from '../../../../common/store/test_utils'; -import { getEndpointListPath } from '../../../common/routing'; +import { getHostListPath } from '../../../common/routing'; describe('host list pagination: ', () => { let fakeCoreStart: jest.Mocked; @@ -56,7 +56,7 @@ describe('host list pagination: ', () => { queryParams = () => uiQueryParams(store.getState()); historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { - return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams })); + return history.push(getHostListPath({ name: 'hostList', ...nextQueryParams })); }; }); @@ -70,7 +70,7 @@ describe('host list pagination: ', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getEndpointListPath({ name: 'endpointList' }), + pathname: getHostListPath({ name: 'hostList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index e62c53e061a33..1c5c4fbac51ba 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -21,7 +21,7 @@ import { listData } from './selectors'; import { HostState } from '../types'; import { hostListReducer } from './reducer'; import { hostMiddlewareFactory } from './middleware'; -import { getEndpointListPath } from '../../../common/routing'; +import { getHostListPath } from '../../../common/routing'; describe('host list middleware', () => { let fakeCoreStart: jest.Mocked; @@ -60,7 +60,7 @@ describe('host list middleware', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getEndpointListPath({ name: 'endpointList' }), + pathname: getHostListPath({ name: 'hostList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index e75d2129f61a5..4f47eaf565d8c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -15,7 +15,7 @@ import { HostPolicyResponseActionStatus, } from '../../../../../common/endpoint/types'; import { HostState, HostIndexUIQueryParams } from '../types'; -import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants'; +import { MANAGEMENT_ROUTING_HOSTS_PATH } from '../../../common/constants'; const PAGE_SIZES = Object.freeze([10, 20, 50]); @@ -114,7 +114,7 @@ export const policyResponseError = (state: Immutable) => state.policy export const isOnHostPage = (state: Immutable) => { return ( matchPath(state.location?.pathname ?? '', { - path: MANAGEMENT_ROUTING_ENDPOINTS_PATH, + path: MANAGEMENT_ROUTING_HOSTS_PATH, exact: true, }) !== null ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 66abf993770a7..10ea271139e49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -26,7 +26,7 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; -import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; +import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public'; @@ -84,14 +84,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { selected_host, show, ...currentUrlParams } = queryParams; return [ formatUrl( - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + getHostDetailsPath({ + name: 'hostPolicyResponse', ...currentUrlParams, selected_host: details.host.id, }) ), - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + getHostDetailsPath({ + name: 'hostPolicyResponse', ...currentUrlParams, selected_host: details.host.id, }), @@ -108,7 +108,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { onDoneNavigateTo: [ 'securitySolution:management', { - path: getEndpointDetailsPath({ name: 'endpointDetails', selected_host: details.host.id }), + path: getHostDetailsPath({ name: 'hostDetails', selected_host: details.host.id }), }, ], }, @@ -200,8 +200,8 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { description: details.host.hostname, }, { - title: i18n.translate('xpack.securitySolution.endpoint.host.details.sensorVersion', { - defaultMessage: 'Sensor Version', + title: i18n.translate('xpack.securitySolution.endpoint.host.details.endpointVersion', { + defaultMessage: 'Endpoint Version', }), description: details.agent.version, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 3d44b73858e90..e29d796325bd6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -38,7 +38,7 @@ import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/endpoint/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { getEndpointListPath } from '../../../../common/routing'; +import { getHostListPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -122,14 +122,14 @@ const PolicyResponseFlyoutPanel = memo<{ const [detailsUri, detailsRoutePath] = useMemo( () => [ formatUrl( - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, selected_host: hostMeta.host.id, }) ), - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, selected_host: hostMeta.host.id, }), diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index b048a8f69b5d2..d11335df875e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -9,14 +9,14 @@ import { useMemo } from 'react'; import { useKibana } from '../../../../common/lib/kibana'; import { HostState } from '../types'; import { - MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_HOSTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, } from '../../../common/constants'; import { State } from '../../../../common/store'; export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function (state: State) { return selector( - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE] as HostState + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE] as HostState ); }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9766cd6abd2b1..996b987ea2be3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -44,7 +44,7 @@ describe('when on the hosts page', () => { it('should show the empty state when there are no hosts or polices', async () => { const renderResult = render(); - // Initially, there are no endpoints or policies, so we prompt to add policies first. + // Initially, there are no hosts or policies, so we prompt to add policies first. const table = await renderResult.findByTestId('emptyPolicyTable'); expect(table).not.toBeNull(); }); @@ -79,8 +79,8 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); - const emptyEndpointsTable = await renderResult.findByTestId('emptyEndpointsTable'); - expect(emptyEndpointsTable).not.toBeNull(); + const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); + expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { @@ -335,7 +335,7 @@ describe('when on the hosts page', () => { const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( - '/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response' + '/hosts?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); @@ -549,7 +549,7 @@ describe('when on the hosts page', () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); expect(subHeaderBackLink.getAttribute('href')).toBe( - '/endpoints?page_index=0&page_size=10&selected_host=1' + '/hosts?page_index=0&page_size=10&selected_host=1' ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index d49335ca8de2c..492c75607a255 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -10,6 +10,8 @@ import { EuiBasicTable, EuiBasicTableColumn, EuiText, + EuiTitle, + EuiSpacer, EuiLink, EuiHealth, EuiToolTip, @@ -33,7 +35,7 @@ import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { ManagementPageView } from '../../../components/management_page_view'; -import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/management_empty_state'; +import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { @@ -41,11 +43,7 @@ import { AgentConfigDetailsDeployAgentAction, } from '../../../../../../ingest_manager/public'; import { SecurityPageName } from '../../../../app/types'; -import { - getEndpointListPath, - getEndpointDetailsPath, - getPolicyDetailPath, -} from '../../../common/routing'; +import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing'; import { useFormatUrl } from '../../../../common/components/link_to'; import { HostAction } from '../store/action'; @@ -107,8 +105,8 @@ export const HostList = () => { const { index, size } = page; // FIXME: PT: if host details is open, table is not displaying correct number of rows history.push( - getEndpointListPath({ - name: 'endpointList', + getHostListPath({ + name: 'hostList', ...queryParams, page_index: JSON.stringify(index), page_size: JSON.stringify(size), @@ -127,12 +125,12 @@ export const HostList = () => { state: { onCancelNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], - onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })), + onCancelUrl: formatUrl(getHostListPath({ name: 'hostList' })), onSaveNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], }, } @@ -145,7 +143,7 @@ export const HostList = () => { state: { onDoneNavigateTo: [ 'securitySolution:management', - { path: getEndpointListPath({ name: 'endpointList' }) }, + { path: getHostListPath({ name: 'hostList' }) }, ], }, }); @@ -191,10 +189,10 @@ export const HostList = () => { defaultMessage: 'Hostname', }), render: ({ hostname, id }: HostInfo['metadata']['host']) => { - const toRoutePath = getEndpointDetailsPath( + const toRoutePath = getHostDetailsPath( { ...queryParams, - name: 'endpointDetails', + name: 'hostDetails', selected_host: id, }, search @@ -259,8 +257,8 @@ export const HostList = () => { }), // eslint-disable-next-line react/display-name render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { - const toRoutePath = getEndpointDetailsPath({ - name: 'endpointPolicyResponse', + const toRoutePath = getHostDetailsPath({ + name: 'hostPolicyResponse', selected_host: item.metadata.host.id, }); const toRouteUrl = formatUrl(toRoutePath); @@ -341,7 +339,7 @@ export const HostList = () => { ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { return ( - { + +

+ +

+
+ + +

+ +

+
+ + } > {hasSelectedHost && } {listData && listData.length > 0 && ( diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 0e81b75d651ba..2cf07b9b4382e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -9,25 +9,25 @@ import { useHistory, Route, Switch } from 'react-router-dom'; import { PolicyContainer } from './policy'; import { - MANAGEMENT_ROUTING_ENDPOINTS_PATH, + MANAGEMENT_ROUTING_HOSTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_ROOT_PATH, } from '../common/constants'; import { NotFoundPage } from '../../app/404'; -import { EndpointsContainer } from './endpoint_hosts'; -import { getEndpointListPath } from '../common/routing'; +import { HostsContainer } from './endpoint_hosts'; +import { getHostListPath } from '../common/routing'; export const ManagementContainer = memo(() => { const history = useHistory(); return ( - + { - history.replace(getEndpointListPath({ name: 'endpointList' })); + history.replace(getHostListPath({ name: 'hostList' })); return null; }} /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 447a70ef998a9..aa7e867e89d6a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -8,6 +8,8 @@ import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from import { EuiBasicTable, EuiText, + EuiTitle, + EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiTableFieldDataColumnType, @@ -20,7 +22,6 @@ import { EuiOverlayMask, EuiConfirmModal, EuiCallOut, - EuiSpacer, EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -391,9 +392,27 @@ export const PolicyList = React.memo(() => { + +

+ +

+
+ + +

+ +

+
+ + } headerRight={ const policyDetailsSelector = (state: State) => state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]; const endpointsSelector = (state: State) => - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]; + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_HOSTS_NAMESPACE]; export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( coreStart, diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index 2ed3dfe86d2f8..f3c470fb1e8a3 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -14,7 +14,7 @@ import { initialPolicyListState, } from '../pages/policy/store/policy_list/reducer'; import { - MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, + MANAGEMENT_STORE_HOSTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, } from '../common/constants'; @@ -31,7 +31,7 @@ const immutableCombineReducers: ImmutableCombineReducers = combineReducers; export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), - [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialHostListState, + [MANAGEMENT_STORE_HOSTS_NAMESPACE]: initialHostListState, }; /** @@ -40,5 +40,5 @@ export const mockManagementState: Immutable = { export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, - [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: hostListReducer, + [MANAGEMENT_STORE_HOSTS_NAMESPACE]: hostListReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 854e9faa0204d..cb21a236ddd7e 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -18,14 +18,14 @@ export type ManagementStoreGlobalNamespace = 'management'; export type ManagementState = CombinedState<{ policyList: PolicyListState; policyDetails: PolicyDetailsState; - endpoints: HostState; + hosts: HostState; }>; /** * The management list of sub-tabs. Changes to these will impact the Router routes. */ export enum ManagementSubTab { - endpoints = 'endpoints', + hosts = 'hosts', policies = 'policy', } diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx index ee048f0d61212..3758bd10bfc8f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx @@ -7,13 +7,13 @@ import React, { memo } from 'react'; import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getEndpointListPath } from '../../../management/common/routing'; +import { getHostListPath } from '../../../management/common/routing'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url'; import { MANAGEMENT_APP_ID } from '../../../management/common/constants'; export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => { - const endpointsPath = getEndpointListPath({ name: 'endpointList' }); + const endpointsPath = getHostListPath({ name: 'hostList' }); const endpointsLink = useManagementFormatUrl(endpointsPath); const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, { path: endpointsPath, diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 18072c25e6dde..2b7fc160110f5 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -48,6 +48,15 @@ import { ConfigureEndpointPackageConfig } from './management/pages/policy/view/i import { State, createStore, createInitialState } from './common/store'; import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; +import { + OVERVIEW, + HOSTS, + NETWORK, + TIMELINES, + ALERTS, + CASE, + ADMINISTRATION, +} from './app/home/translations'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -95,10 +104,12 @@ export class Plugin implements IPlugin { + mount: async () => { const [{ application }] = await core.getStartServices(); application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); return () => true; @@ -107,9 +118,7 @@ export class Plugin implements IPlugin = ( activeDescendantId: null, selectedDescendantId: null, processEntityIdOfSelectedDescendant: null, - panelToDisplay: null, }, action ) => { @@ -39,11 +38,6 @@ const uiReducer: Reducer = ( selectedDescendantId: action.payload.nodeId, processEntityIdOfSelectedDescendant: action.payload.selectedProcessId, }; - } else if (action.type === 'appDisplayedDifferentPanel') { - return { - ...uiState, - panelToDisplay: action.payload, - }; } else if ( action.type === 'userBroughtProcessIntoView' || action.type === 'appDetectedNewIdFromQueryParams' diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index e54193ab394a5..2bc254d118d33 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -127,11 +127,6 @@ export const uiSelectedDescendantProcessId = composeSelectors( uiSelectors.selectedDescendantProcessId ); -/** - * The current panel to display - */ -export const currentPanelView = composeSelectors(uiStateSelector, uiSelectors.currentPanelView); - /** * Returns the camera state from within ResolverState */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index bddc7d34abf1c..494d8884329c6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -39,8 +39,3 @@ export const selectedDescendantProcessId = createSelector( return processEntityIdOfSelectedDescendant; } ); - -// Select the current panel to be displayed -export const currentPanelView = (uiState: ResolverUIState) => { - return uiState.panelToDisplay; -}; diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 5dd9a944b88ea..2025762a0605c 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -45,10 +45,6 @@ export interface ResolverUIState { * The entity_id of the process for the resolver's currently selected descendant. */ readonly processEntityIdOfSelectedDescendant: string | null; - /** - * Which panel the ui should display - */ - readonly panelToDisplay: string | null; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx index 2a2e7e87394a9..f4fe4fe520c92 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.tsx @@ -4,17 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { - memo, - useCallback, - useMemo, - useContext, - useLayoutEffect, - useState, - useEffect, -} from 'react'; +import React, { memo, useCallback, useMemo, useContext, useLayoutEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; // eslint-disable-next-line import/no-nodejs-modules import querystring from 'querystring'; import { EuiPanel } from '@elastic/eui'; @@ -48,7 +40,7 @@ import { CrumbInfo } from './panels/panel_content_utilities'; */ const PanelContent = memo(function PanelContent() { const history = useHistory(); - const urlSearch = history.location.search; + const urlSearch = useLocation().search; const dispatch = useResolverDispatch(); const { timestamp } = useContext(SideEffectContext); @@ -205,21 +197,12 @@ const PanelContent = memo(function PanelContent() { return 'processListWithCounts'; }, [uiSelectedEvent, crumbEvent, crumbId, graphableProcessEntityIds]); - useEffect(() => { - // dispatch `appDisplayedDifferentPanel` to sync state with which panel gets displayed - dispatch({ - type: 'appDisplayedDifferentPanel', - payload: panelToShow, - }); - }, [panelToShow, dispatch]); - - const currentPanelView = useSelector(selectors.currentPanelView); const terminatedProcesses = useSelector(selectors.terminatedProcesses); const processEntityId = uiSelectedEvent ? event.entityId(uiSelectedEvent) : undefined; const isProcessTerminated = processEntityId ? terminatedProcesses.has(processEntityId) : false; const panelInstance = useMemo(() => { - if (currentPanelView === 'processDetails') { + if (panelToShow === 'processDetails') { return ( sum + val, 0); @@ -278,7 +261,7 @@ const PanelContent = memo(function PanelContent() { crumbId, pushToQueryParams, relatedStatsForIdFromParams, - currentPanelView, + panelToShow, isProcessTerminated, ]); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 56f88ccb13115..517b847855647 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -82,10 +82,13 @@ const invalidDateText = i18n.translate( } ); /** - * @param {ConstructorParameters[0]} timestamp To be passed through Date->Intl.DateTimeFormat * @returns {string} A nicely formatted string for a date */ -export function formatDate(timestamp: ConstructorParameters[0]) { +export function formatDate( + /** To be passed through Date->Intl.DateTimeFormat */ timestamp: ConstructorParameters< + typeof Date + >[0] +): string { const date = new Date(timestamp); if (isFinite(date.getTime())) { return formatter.format(date); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 7b07548af67ae..8d55d00b50e13 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -298,6 +298,7 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message: export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate( 'xpack.securitySolution.timelines.components.templateCallOutMessageTitle', { - defaultMessage: 'Now you can add timeline templates and link it to rules.', + defaultMessage: + 'Prebuit detection rules are now packaged with Timeline templates. You can also create your own Timeline templates and associate them with any rule.', } ); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts index f5345c3dce222..84a18cb1573dd 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts @@ -28,7 +28,7 @@ import { import { CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, CREATE_TIMELINE_ERROR_MESSAGE, -} from './utils/create_timelines'; +} from './utils/failure_cases'; describe('create timelines', () => { let server: ReturnType; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index fb4991d7d1e7d..0f4e8f3204e2b 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -46,7 +46,7 @@ import { createTimelines } from './utils/create_timelines'; import { TimelineStatus } from '../../../../common/types/timeline'; const CHUNK_PARSED_OBJECT_SIZE = 10; -const DEFAULT_IMPORT_ERROR = `Something went wrong, there's something we didn't handle properly, please help us improve by providing the file you try to import on https://discuss.elastic.co/c/security/siem`; +const DEFAULT_IMPORT_ERROR = `Something has gone wrong. We didn't handle something properly. To help us fix this, please upload your file to https://discuss.elastic.co/c/security/siem.`; export const importTimelinesRoute = ( router: IRouter, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts index abe298566341c..67965469e1a9f 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts @@ -13,11 +13,6 @@ import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/ import { SavedNote } from '../../../../../common/types/timeline/note'; import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; -export const CREATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE timeline with POST is not allowed, please use PATCH instead'; -export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; - export const saveTimelines = ( frameworkRequest: FrameworkRequest, timeline: SavedTimeline, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts index 60ba5389280c4..5e7a73ca18d0e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts @@ -11,27 +11,31 @@ import { } from '../../../../../common/types/timeline'; export const UPDATE_TIMELINE_ERROR_MESSAGE = - 'CREATE timeline with PATCH is not allowed, please use POST instead'; + 'You cannot create new timelines with PATCH. Use POST instead.'; export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - "CREATE template timeline with PATCH is not allowed, please use POST instead (Given template timeline doesn't exist)"; + 'You cannot create new Timeline templates with PATCH. Use POST instead (templateTimelineId does not exist).'; export const NO_MATCH_VERSION_ERROR_MESSAGE = - 'TimelineVersion conflict: The given version doesn not match with existing timeline'; + 'Timeline template version conflict. The provided templateTimelineVersion does not match the current template.'; export const NO_MATCH_ID_ERROR_MESSAGE = - "Timeline id doesn't match with existing template timeline"; -export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict'; + 'There are no Timeline templates that match the provided templateTimelineId.'; +export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = + 'To update existing Timeline templates, you must increment the templateTimelineVersion value.'; export const CREATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE timeline with POST is not allowed, please use PATCH instead'; + 'You cannot update timelines with POST. Use PATCH instead.'; export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = - 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; -export const EMPTY_TITLE_ERROR_MESSAGE = 'Title cannot be empty'; -export const UPDATE_STATUS_ERROR_MESSAGE = 'Update an immutable timeline is is not allowed'; + 'You cannot update Timeline templates with POST. Use PATCH instead.'; +export const EMPTY_TITLE_ERROR_MESSAGE = 'The title field cannot be empty.'; +export const UPDATE_STATUS_ERROR_MESSAGE = + 'You are not allowed to set the status field value to immutable.'; export const CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE = - 'Create template timeline without a valid templateTimelineVersion is not allowed. Please start from 1 to create a new template timeline'; -export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = 'Cannot create a draft timeline'; -export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'Update status is not allowed'; -export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'Update timelineType is not allowed'; + 'You must provide a valid templateTimelineVersion value. Use 1 for new Timeline templates.'; +export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = + 'You are not allowed to set the status field value to draft.'; +export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'You are not allowed to set the status field.'; +export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = + 'You cannot convert a Timeline template to a timeline, or a timeline to a Timeline template.'; export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE = - 'Update timeline via import is not allowed'; + 'You cannot update a timeline via imports. Use the UI to modify existing timelines.'; const isUpdatingStatus = ( isHandlingTemplateTimeline: boolean, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx similarity index 80% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx index aa78dfb4315f9..4686ede7bc2c2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_clone.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/clone_button.tsx @@ -7,7 +7,7 @@ import React, { FC, useContext } from 'react'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { createCapabilityFailureMessage, @@ -20,7 +20,7 @@ interface CloneActionProps { itemId: string; } -export const CloneAction: FC = ({ itemId }) => { +export const CloneButton: FC = ({ itemId }) => { const history = useHistory(); const { canCreateTransform } = useContext(AuthorizationContext).capabilities; @@ -34,17 +34,15 @@ export const CloneAction: FC = ({ itemId }) => { } const cloneButton = ( - - {buttonCloneText} - + {buttonCloneText} + ); if (!canCreateTransform) { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts new file mode 100644 index 0000000000000..727cc87c70f2c --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CloneButton } from './clone_button'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap new file mode 100644 index 0000000000000..3980cc5d5a1ae --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/__snapshots__/delete_button.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transform: Transform List Actions Minimal initialization 1`] = ` + + + + + Delete + + +`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx similarity index 74% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx index fdd0b821f54fd..63f8243b403d3 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { DeleteAction } from './action_delete'; +import { DeleteButton } from './delete_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,13 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - deleteTransform(d: TransformListRow) {}, + onClick: () => {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx new file mode 100644 index 0000000000000..b81c3ebc34ca0 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { TRANSFORM_STATE } from '../../../../../../common'; +import { + AuthorizationContext, + createCapabilityFailureMessage, +} from '../../../../lib/authorization'; +import { TransformListRow } from '../../../../common'; + +interface DeleteButtonProps { + items: TransformListRow[]; + forceDisable?: boolean; + onClick: (items: TransformListRow[]) => void; +} + +const transformCanNotBeDeleted = (i: TransformListRow) => + ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(i.stats.state); + +export const DeleteButton: FC = ({ items, forceDisable, onClick }) => { + const isBulkAction = items.length > 1; + + const disabled = items.some(transformCanNotBeDeleted); + const { canDeleteTransform } = useContext(AuthorizationContext).capabilities; + + const buttonDeleteText = i18n.translate('xpack.transform.transformList.deleteActionName', { + defaultMessage: 'Delete', + }); + const bulkDeleteButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.deleteBulkActionDisabledToolTipContent', + { + defaultMessage: 'One or more selected transforms must be stopped in order to be deleted.', + } + ); + const deleteButtonDisabledText = i18n.translate( + 'xpack.transform.transformList.deleteActionDisabledToolTipContent', + { + defaultMessage: 'Stop the transform in order to delete it.', + } + ); + + const buttonDisabled = forceDisable === true || disabled || !canDeleteTransform; + let deleteButton = ( + onClick(items)} + aria-label={buttonDeleteText} + > + {buttonDeleteText} + + ); + + if (disabled || !canDeleteTransform) { + let content; + if (disabled) { + content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText; + } else { + content = createCapabilityFailureMessage('canDeleteTransform'); + } + + deleteButton = ( + + {deleteButton} + + ); + } + + return deleteButton; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx similarity index 54% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx index 79a9e45e317e5..668e535198649 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_delete.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_button_modal.tsx @@ -4,88 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useContext, useMemo, useState } from 'react'; +import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CONFIRM_BUTTON, - EuiButtonEmpty, EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiOverlayMask, EuiSpacer, EuiSwitch, - EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { TRANSFORM_STATE } from '../../../../../../common'; -import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; -import { - AuthorizationContext, - createCapabilityFailureMessage, -} from '../../../../lib/authorization'; -import { TransformListRow } from '../../../../common'; - -interface DeleteActionProps { - items: TransformListRow[]; - forceDisable?: boolean; -} -const transformCanNotBeDeleted = (i: TransformListRow) => - ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(i.stats.state); +import { DeleteAction } from './use_delete_action'; -export const DeleteAction: FC = ({ items, forceDisable }) => { +export const DeleteButtonModal: FC = ({ + closeModal, + deleteAndCloseModal, + deleteDestIndex, + deleteIndexPattern, + indexPatternExists, + items, + shouldForceDelete, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, +}) => { const isBulkAction = items.length > 1; - const disabled = items.some(transformCanNotBeDeleted); - const shouldForceDelete = useMemo( - () => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED), - [items] - ); - const { canDeleteTransform } = useContext(AuthorizationContext).capabilities; - const deleteTransforms = useDeleteTransforms(); - const { - userCanDeleteIndex, - deleteDestIndex, - indexPatternExists, - deleteIndexPattern, - toggleDeleteIndex, - toggleDeleteIndexPattern, - } = useDeleteIndexAndTargetIndex(items); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const deleteAndCloseModal = () => { - setModalVisible(false); - - const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex; - const shouldDeleteDestIndexPattern = - userCanDeleteIndex && indexPatternExists && deleteIndexPattern; - // if we are deleting multiple transforms, then force delete all if at least one item has failed - // else, force delete only when the item user picks has failed - const forceDelete = isBulkAction - ? shouldForceDelete - : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; - deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); - }; - const openModal = () => setModalVisible(true); - - const buttonDeleteText = i18n.translate('xpack.transform.transformList.deleteActionName', { - defaultMessage: 'Delete', - }); - const bulkDeleteButtonDisabledText = i18n.translate( - 'xpack.transform.transformList.deleteBulkActionDisabledToolTipContent', - { - defaultMessage: 'One or more selected transforms must be stopped in order to be deleted.', - } - ); - const deleteButtonDisabledText = i18n.translate( - 'xpack.transform.transformList.deleteActionDisabledToolTipContent', - { - defaultMessage: 'Stop the transform in order to delete it.', - } - ); const bulkDeleteModalTitle = i18n.translate( 'xpack.transform.transformList.bulkDeleteModalTitle', { @@ -203,63 +151,23 @@ export const DeleteAction: FC = ({ items, forceDisable }) => ); - let deleteButton = ( - - {buttonDeleteText} - - ); - - if (disabled || !canDeleteTransform) { - let content; - if (disabled) { - content = isBulkAction ? bulkDeleteButtonDisabledText : deleteButtonDisabledText; - } else { - content = createCapabilityFailureMessage('canDeleteTransform'); - } - - deleteButton = ( - - {deleteButton} - - ); - } - return ( - - {deleteButton} - {isModalVisible && ( - - - {isBulkAction ? bulkDeleteModalContent : deleteModalContent} - - - )} - + + + {isBulkAction ? bulkDeleteModalContent : deleteModalContent} + + ); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts new file mode 100644 index 0000000000000..ef891d7c4a128 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DeleteButton } from './delete_button'; +export { DeleteButtonModal } from './delete_button_modal'; +export { useDeleteAction } from './use_delete_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts new file mode 100644 index 0000000000000..d76eebe954d7b --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState } from 'react'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; +import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; + +export type DeleteAction = ReturnType; +export const useDeleteAction = () => { + const deleteTransforms = useDeleteTransforms(); + + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const isBulkAction = items.length > 1; + const shouldForceDelete = useMemo( + () => items.some((i: TransformListRow) => i.stats.state === TRANSFORM_STATE.FAILED), + [items] + ); + + const closeModal = () => setModalVisible(false); + + const { + userCanDeleteIndex, + deleteDestIndex, + indexPatternExists, + deleteIndexPattern, + toggleDeleteIndex, + toggleDeleteIndexPattern, + } = useDeleteIndexAndTargetIndex(items); + + const deleteAndCloseModal = () => { + setModalVisible(false); + + const shouldDeleteDestIndex = userCanDeleteIndex && deleteDestIndex; + const shouldDeleteDestIndexPattern = + userCanDeleteIndex && indexPatternExists && deleteIndexPattern; + // if we are deleting multiple transforms, then force delete all if at least one item has failed + // else, force delete only when the item user picks has failed + const forceDelete = isBulkAction + ? shouldForceDelete + : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; + deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); + }; + + const openModal = (newItems: TransformListRow[]) => { + // EUI issue: Might trigger twice, one time as an array, + // one time as a single object. See https://github.com/elastic/eui/issues/3679 + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + return { + closeModal, + deleteAndCloseModal, + deleteDestIndex, + deleteIndexPattern, + indexPatternExists, + isModalVisible, + items, + openModal, + shouldForceDelete, + toggleDeleteIndex, + toggleDeleteIndexPattern, + userCanDeleteIndex, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx similarity index 53% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx index dfb4cd443e904..6ba8e7aeba20f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_edit.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/edit_button.tsx @@ -4,47 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useState, FC } from 'react'; +import React, { useContext, FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; -import { TransformPivotConfig } from '../../../../common'; import { createCapabilityFailureMessage, AuthorizationContext, } from '../../../../lib/authorization'; -import { EditTransformFlyout } from '../edit_transform_flyout'; - -interface EditActionProps { - config: TransformPivotConfig; +interface EditButtonProps { + onClick: () => void; } - -export const EditAction: FC = ({ config }) => { +export const EditButton: FC = ({ onClick }) => { const { canCreateTransform } = useContext(AuthorizationContext).capabilities; - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = () => setIsFlyoutVisible(true); - const buttonEditText = i18n.translate('xpack.transform.transformList.editActionName', { defaultMessage: 'Edit', }); const editButton = ( - - {buttonEditText} - + {buttonEditText} + ); if (!canCreateTransform) { @@ -57,10 +47,5 @@ export const EditAction: FC = ({ config }) => { ); } - return ( - <> - {editButton} - {isFlyoutVisible && } - - ); + return editButton; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts new file mode 100644 index 0000000000000..17a2ad9444f8d --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EditButton } from './edit_button'; +export { useEditAction } from './use_edit_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts new file mode 100644 index 0000000000000..ace3ec8f636e6 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { TransformPivotConfig } from '../../../../common'; + +export const useEditAction = () => { + const [config, setConfig] = useState(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const showFlyout = (newConfig: TransformPivotConfig) => { + setConfig(newConfig); + setIsFlyoutVisible(true); + }; + + return { + config, + closeFlyout, + isFlyoutVisible, + showFlyout, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap new file mode 100644 index 0000000000000..231a1f30f2c31 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/__snapshots__/start_button.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transform: Transform List Actions Minimal initialization 1`] = ` + + + + + Start + + +`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts new file mode 100644 index 0000000000000..df6bbb7c61908 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StartButton } from './start_button'; +export { StartButtonModal } from './start_button_modal'; +export { useStartAction } from './use_start_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx similarity index 74% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx index 2de115236c4dc..b88e1257f56ad 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { StartAction } from './action_start'; +import { StartButton } from './start_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,13 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - startTransform(d: TransformListRow) {}, + onClick: () => {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx new file mode 100644 index 0000000000000..a0fe1bfbb9544 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { + createCapabilityFailureMessage, + AuthorizationContext, +} from '../../../../lib/authorization'; +import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; + +interface StartButtonProps { + items: TransformListRow[]; + forceDisable?: boolean; + onClick: (items: TransformListRow[]) => void; +} +export const StartButton: FC = ({ items, forceDisable, onClick }) => { + const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; + const isBulkAction = items.length > 1; + + const buttonStartText = i18n.translate('xpack.transform.transformList.startActionName', { + defaultMessage: 'Start', + }); + + // Disable start for batch transforms which have completed. + const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i)); + // Disable start action if one of the transforms is already started or trying to restart will throw error + const startedTransform = items.some( + (i: TransformListRow) => i.stats.state === TRANSFORM_STATE.STARTED + ); + + let startedTransformMessage; + let completedBatchTransformMessage; + + if (isBulkAction === true) { + startedTransformMessage = i18n.translate( + 'xpack.transform.transformList.startedTransformBulkToolTip', + { + defaultMessage: 'One or more transforms are already started.', + } + ); + completedBatchTransformMessage = i18n.translate( + 'xpack.transform.transformList.completeBatchTransformBulkActionToolTip', + { + defaultMessage: + 'One or more transforms are completed batch transforms and cannot be restarted.', + } + ); + } else { + startedTransformMessage = i18n.translate( + 'xpack.transform.transformList.startedTransformToolTip', + { + defaultMessage: '{transformId} is already started.', + values: { transformId: items[0] && items[0].config.id }, + } + ); + completedBatchTransformMessage = i18n.translate( + 'xpack.transform.transformList.completeBatchTransformToolTip', + { + defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.', + values: { transformId: items[0] && items[0].config.id }, + } + ); + } + + const actionIsDisabled = + !canStartStopTransform || completedBatchTransform || startedTransform || items.length === 0; + + let content: string | undefined; + if (actionIsDisabled && items.length > 0) { + if (!canStartStopTransform) { + content = createCapabilityFailureMessage('canStartStopTransform'); + } else if (completedBatchTransform) { + content = completedBatchTransformMessage; + } else if (startedTransform) { + content = startedTransformMessage; + } + } + + const disabled = forceDisable === true || actionIsDisabled; + + const startButton = ( + onClick(items)} + > + {buttonStartText} + + ); + if (disabled && content !== undefined) { + return ( + + {startButton} + + ); + } + return startButton; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx new file mode 100644 index 0000000000000..2ef0d20c45116 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_button_modal.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; + +import { StartAction } from './use_start_action'; + +export const StartButtonModal: FC = ({ + closeModal, + isModalVisible, + items, + startAndCloseModal, +}) => { + const isBulkAction = items.length > 1; + + const bulkStartModalTitle = i18n.translate('xpack.transform.transformList.bulkStartModalTitle', { + defaultMessage: 'Start {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items && items.length }, + }); + const startModalTitle = i18n.translate('xpack.transform.transformList.startModalTitle', { + defaultMessage: 'Start {transformId}', + values: { transformId: items[0] && items[0].config.id }, + }); + + return ( + + +

+ {i18n.translate('xpack.transform.transformList.startModalBody', { + defaultMessage: + 'A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?', + values: { count: items.length }, + })} +

+
+
+ ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts new file mode 100644 index 0000000000000..32d2dc6dabf86 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; + +import { TransformListRow } from '../../../../common'; +import { useStartTransforms } from '../../../../hooks'; + +export type StartAction = ReturnType; +export const useStartAction = () => { + const startTransforms = useStartTransforms(); + + const [isModalVisible, setModalVisible] = useState(false); + const [items, setItems] = useState([]); + + const closeModal = () => setModalVisible(false); + + const startAndCloseModal = () => { + setModalVisible(false); + startTransforms(items); + }; + + const openModal = (newItems: TransformListRow[]) => { + // EUI issue: Might trigger twice, one time as an array, + // one time as a single object. See https://github.com/elastic/eui/issues/3679 + if (Array.isArray(newItems)) { + setItems(newItems); + setModalVisible(true); + } + }; + + return { + closeModal, + isModalVisible, + items, + openModal, + startAndCloseModal, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap similarity index 78% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap index 97d393bc8128b..dd81bf34bf582 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_stop.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/__snapshots__/stop_button.test.tsx.snap @@ -6,16 +6,17 @@ exports[`Transform: Transform List Actions Minimal initialization delay="regular" position="top" > - + + Stop - + `; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts new file mode 100644 index 0000000000000..858b6c70501b3 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StopButton } from './stop_button'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx similarity index 76% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx index a97097d909848..d9c07a9dccc8f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.test.tsx @@ -5,10 +5,10 @@ */ import { shallow } from 'enzyme'; -import React from 'react'; +import React, { ComponentProps } from 'react'; import { TransformListRow } from '../../../../common'; -import { StopAction } from './action_stop'; +import { StopButton } from './stop_button'; import transformListRow from '../../../../common/__mocks__/transform_list_row.json'; @@ -18,13 +18,12 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List Actions ', () => { test('Minimal initialization', () => { const item: TransformListRow = transformListRow; - const props = { - disabled: false, + const props: ComponentProps = { + forceDisable: false, items: [item], - stopTransform(d: TransformListRow) {}, }; - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx similarity index 85% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx index 3f35bef458951..2c67ea3e83ecc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_stop.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_button.tsx @@ -6,7 +6,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { TRANSFORM_STATE } from '../../../../../../common'; @@ -17,12 +17,11 @@ import { } from '../../../../lib/authorization'; import { useStopTransforms } from '../../../../hooks'; -interface StopActionProps { +interface StopButtonProps { items: TransformListRow[]; forceDisable?: boolean; } - -export const StopAction: FC = ({ items, forceDisable }) => { +export const StopButton: FC = ({ items, forceDisable }) => { const isBulkAction = items.length > 1; const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; const stopTransforms = useStopTransforms(); @@ -57,18 +56,17 @@ export const StopAction: FC = ({ items, forceDisable }) => { stopTransforms(items); }; + const disabled = forceDisable === true || !canStartStopTransform || stoppedTransform === true; + const stopButton = ( - - {buttonStopText} - + {buttonStopText} + ); if (!canStartStopTransform || stoppedTransform) { return ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap deleted file mode 100644 index da5ad27c9d6b1..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_delete.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - - - Delete - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap deleted file mode 100644 index d534f05d3be96..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/action_start.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Transform List Actions Minimal initialization 1`] = ` - - - - Start - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx deleted file mode 100644 index 9edfe7fab70a0..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/action_start.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, FC, useContext, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiToolTip, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; - -import { TRANSFORM_STATE } from '../../../../../../common'; - -import { useStartTransforms } from '../../../../hooks'; -import { - createCapabilityFailureMessage, - AuthorizationContext, -} from '../../../../lib/authorization'; -import { TransformListRow, isCompletedBatchTransform } from '../../../../common'; - -interface StartActionProps { - items: TransformListRow[]; - forceDisable?: boolean; -} - -export const StartAction: FC = ({ items, forceDisable }) => { - const isBulkAction = items.length > 1; - const { canStartStopTransform } = useContext(AuthorizationContext).capabilities; - const startTransforms = useStartTransforms(); - - const [isModalVisible, setModalVisible] = useState(false); - - const closeModal = () => setModalVisible(false); - const startAndCloseModal = () => { - setModalVisible(false); - startTransforms(items); - }; - const openModal = () => setModalVisible(true); - - const buttonStartText = i18n.translate('xpack.transform.transformList.startActionName', { - defaultMessage: 'Start', - }); - - // Disable start for batch transforms which have completed. - const completedBatchTransform = items.some((i: TransformListRow) => isCompletedBatchTransform(i)); - // Disable start action if one of the transforms is already started or trying to restart will throw error - const startedTransform = items.some( - (i: TransformListRow) => i.stats.state === TRANSFORM_STATE.STARTED - ); - - let startedTransformMessage; - let completedBatchTransformMessage; - - if (isBulkAction === true) { - startedTransformMessage = i18n.translate( - 'xpack.transform.transformList.startedTransformBulkToolTip', - { - defaultMessage: 'One or more transforms are already started.', - } - ); - completedBatchTransformMessage = i18n.translate( - 'xpack.transform.transformList.completeBatchTransformBulkActionToolTip', - { - defaultMessage: - 'One or more transforms are completed batch transforms and cannot be restarted.', - } - ); - } else { - startedTransformMessage = i18n.translate( - 'xpack.transform.transformList.startedTransformToolTip', - { - defaultMessage: '{transformId} is already started.', - values: { transformId: items[0] && items[0].config.id }, - } - ); - completedBatchTransformMessage = i18n.translate( - 'xpack.transform.transformList.completeBatchTransformToolTip', - { - defaultMessage: '{transformId} is a completed batch transform and cannot be restarted.', - values: { transformId: items[0] && items[0].config.id }, - } - ); - } - - const actionIsDisabled = !canStartStopTransform || completedBatchTransform || startedTransform; - - let startButton = ( - - {buttonStartText} - - ); - - if (actionIsDisabled) { - let content; - if (!canStartStopTransform) { - content = createCapabilityFailureMessage('canStartStopTransform'); - } else if (completedBatchTransform) { - content = completedBatchTransformMessage; - } else if (startedTransform) { - content = startedTransformMessage; - } - - startButton = ( - - {startButton} - - ); - } - - const bulkStartModalTitle = i18n.translate('xpack.transform.transformList.bulkStartModalTitle', { - defaultMessage: 'Start {count} {count, plural, one {transform} other {transforms}}?', - values: { count: items && items.length }, - }); - const startModalTitle = i18n.translate('xpack.transform.transformList.startModalTitle', { - defaultMessage: 'Start {transformId}', - values: { transformId: items[0] && items[0].config.id }, - }); - - return ( - - {startButton} - {isModalVisible && ( - - -

- {i18n.translate('xpack.transform.transformList.startModalBody', { - defaultMessage: - 'A transform will increase search and indexing load in your cluster. Please stop the transform if excessive load is experienced. Are you sure you want to start {count, plural, one {this} other {these}} {count} {count, plural, one {transform} other {transforms}}?', - values: { count: items.length }, - })} -

-
-
- )} -
- ); -}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx deleted file mode 100644 index 343b5e4db67e3..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/actions.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { TRANSFORM_STATE } from '../../../../../../common'; - -import { TransformListRow } from '../../../../common'; - -import { CloneAction } from './action_clone'; -import { DeleteAction } from './action_delete'; -import { EditAction } from './action_edit'; -import { StartAction } from './action_start'; -import { StopAction } from './action_stop'; - -export const getActions = ({ forceDisable }: { forceDisable: boolean }) => { - return [ - { - render: (item: TransformListRow) => { - if (item.stats.state === TRANSFORM_STATE.STOPPED) { - return ; - } - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - { - render: (item: TransformListRow) => { - return ; - }, - }, - ]; -}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx index 5e0363d0a7a15..70b3dc7c2bffe 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { TransformList } from './transform_list'; jest.mock('../../../../../shared_imports'); +jest.mock('../../../../../app/app_dependencies'); describe('Transform: Transform List ', () => { test('Minimal initialization', () => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index b1eea4a09fca3..9df4113fa9a8b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -35,13 +35,12 @@ import { AuthorizationContext } from '../../../../lib/authorization'; import { CreateTransformButton } from '../create_transform_button'; import { RefreshTransformListButton } from '../refresh_transform_list_button'; -import { getTaskStateBadge } from './columns'; -import { DeleteAction } from './action_delete'; -import { StartAction } from './action_start'; -import { StopAction } from './action_stop'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; import { ItemIdToExpandedRowMap, Clause, TermClause, FieldClause, Value } from './common'; -import { getColumns } from './columns'; +import { getTaskStateBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; function getItemIdToExpandedRowMap( @@ -90,6 +89,8 @@ export const TransformList: FC = ({ const [transformSelection, setTransformSelection] = useState([]); const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const bulkStartAction = useStartAction(); + const bulkDeleteAction = useDeleteAction(); const [searchError, setSearchError] = useState(undefined); @@ -185,6 +186,12 @@ export const TransformList: FC = ({ setIsLoading(false); }; + const { columns, modals: singleActionModals } = useColumns( + expandedRowItemIds, + setExpandedRowItemIds, + transformSelection + ); + // Before the transforms have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No transforms found' during the initial loading. if (!isInitialized) { @@ -231,8 +238,6 @@ export const TransformList: FC = ({ ); } - const columns = getColumns(expandedRowItemIds, setExpandedRowItemIds, transformSelection); - const sorting = { sort: { field: sortField, @@ -252,13 +257,13 @@ export const TransformList: FC = ({ const bulkActionMenuItems = [
- +
,
- +
,
- +
, ]; @@ -375,6 +380,13 @@ export const TransformList: FC = ({ return (
+ {/* Bulk Action Modals */} + {bulkStartAction.isModalVisible && } + {bulkDeleteAction.isModalVisible && } + + {/* Single Action Modals */} + {singleActionModals} + { - test('getActions()', () => { - const actions = getActions({ forceDisable: false }); + test('useActions()', () => { + const { result } = renderHook(() => useActions({ forceDisable: false })); + const actions: ReturnType['actions'] = result.current.actions; expect(actions).toHaveLength(4); expect(typeof actions[0].render).toBe('function'); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx new file mode 100644 index 0000000000000..a6b1aa1a1b5c5 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiTableComputedColumnType } from '@elastic/eui'; + +import { TRANSFORM_STATE } from '../../../../../../common'; + +import { TransformListRow } from '../../../../common'; + +import { CloneButton } from '../action_clone'; +import { useDeleteAction, DeleteButton, DeleteButtonModal } from '../action_delete'; +import { EditTransformFlyout } from '../edit_transform_flyout'; +import { useEditAction, EditButton } from '../action_edit'; +import { useStartAction, StartButton, StartButtonModal } from '../action_start'; +import { StopButton } from '../action_stop'; + +export const useActions = ({ + forceDisable, +}: { + forceDisable: boolean; +}): { actions: Array>; modals: JSX.Element } => { + const deleteAction = useDeleteAction(); + const editAction = useEditAction(); + const startAction = useStartAction(); + + return { + modals: ( + <> + {startAction.isModalVisible && } + {editAction.config && editAction.isFlyoutVisible && ( + + )} + {deleteAction.isModalVisible && } + + ), + actions: [ + { + render: (item: TransformListRow) => { + if (item.stats.state === TRANSFORM_STATE.STOPPED) { + return ( + + ); + } + return ; + }, + }, + { + render: (item: TransformListRow) => { + return editAction.showFlyout(item.config)} />; + }, + }, + { + render: (item: TransformListRow) => { + return ; + }, + }, + { + render: (item: TransformListRow) => { + return ( + + ); + }, + }, + ], + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx similarity index 67% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index 3c75c33caf840..94d3e5322a2e8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getColumns } from './columns'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useColumns } from './use_columns'; jest.mock('../../../../../shared_imports'); +jest.mock('../../../../../app/app_dependencies'); describe('Transform: Job List Columns', () => { - test('getColumns()', () => { - const columns = getColumns([], () => {}, []); + test('useColumns()', () => { + const { result } = renderHook(() => useColumns([], () => {}, [])); + const columns: ReturnType['columns'] = result.current.columns; expect(columns).toHaveLength(7); expect(columns[0].isExpander).toBeTruthy(); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx similarity index 96% rename from x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx rename to x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index 5ed2566e8a194..d2d8c7084941d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -30,7 +30,7 @@ import { TransformStats, TRANSFORM_LIST_COLUMN, } from '../../../../common'; -import { getActions } from './actions'; +import { useActions } from './use_actions'; enum STATE_COLOR { aborting = 'warning', @@ -64,12 +64,12 @@ export const getTaskStateBadge = ( ); }; -export const getColumns = ( +export const useColumns = ( expandedRowItemIds: TransformId[], setExpandedRowItemIds: React.Dispatch>, transformSelection: TransformListRow[] ) => { - const actions = getActions({ forceDisable: transformSelection.length > 0 }); + const { actions, modals } = useActions({ forceDisable: transformSelection.length > 0 }); function toggleDetails(item: TransformListRow) { const index = expandedRowItemIds.indexOf(item.config.id); @@ -223,10 +223,10 @@ export const getColumns = ( }, { name: i18n.translate('xpack.transform.tableActionLabel', { defaultMessage: 'Actions' }), - actions, + actions: actions as EuiTableActionsColumnType['actions'], width: '80px', }, ]; - return columns; + return { columns, modals }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3200240e9089a..5fc5bf604351e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6455,9 +6455,7 @@ "xpack.idxMgmt.freezeIndicesAction.successfullyFrozeIndicesMessage": "[{indexNames}] が凍結されました", "xpack.idxMgmt.frozenBadgeLabel": "凍結", "xpack.idxMgmt.home.appTitle": "インデックス管理", - "xpack.idxMgmt.home.idxMgmtDescription": "Elasticsearch インデックスを個々に、または一斉に更新します", "xpack.idxMgmt.home.idxMgmtDocsLinkText": "インデックス管理ドキュメント", - "xpack.idxMgmt.home.indexTemplatesDescription": "インデックステンプレートを使用して設定、マッピング、エイリアスをインデックスに自動的に適用します。", "xpack.idxMgmt.home.indexTemplatesTabTitle": "インデックステンプレート", "xpack.idxMgmt.home.indicesTabTitle": "インデックス", "xpack.idxMgmt.indexActionsMenu.clearIndexCacheLabel": "{selectedIndexCount, plural, one {インデックス} other {インデックス} }のキャッシュを消去", @@ -8739,30 +8737,25 @@ "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "ご使用のライセンスには有効期限がありません。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "トライアルを延長", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendYourTrialTitle": "トライアルの延長", - "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.howToContinueUsingPluginsDescription": "機械学習、高度なセキュリティ、その他の素晴らしい {platinumLicenseFeaturesLinkText} の使用を続けるには、今すぐ延長をお申し込みください。", - "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.platinumLicenseFeaturesLinkText": "プラチナ機能", + "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.howToContinueUsingPluginsDescription": "機械学習、高度なセキュリティ、その他の素晴らしい {subscriptionFeaturesLinkText} の使用を続けるには、今すぐ延長をお申し込みください。", "xpack.licenseMgmt.licenseDashboard.revertToBasic.acknowledgeModal.revertToBasicButtonLabel": "ベーシックに戻す", "xpack.licenseMgmt.licenseDashboard.revertToBasic.acknowledgeModalTitle": "ベーシックライセンスに戻す", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModal.cancelButtonLabel": "キャンセル", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModal.confirmButtonLabel": "確認", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModalTitle": "ベーシックライセンスに戻す確認", - "xpack.licenseMgmt.licenseDashboard.revertToBasic.platinumLicenseFeaturesLinkText": "プラチナ機能", - "xpack.licenseMgmt.licenseDashboard.revertToBasic.revertToFreeFeaturesDescription": "無料の機能に戻すと、セキュリティ、機械学習、その他 {platinumLicenseFeaturesLinkText} が利用できなくなります。", + "xpack.licenseMgmt.licenseDashboard.revertToBasic.revertToFreeFeaturesDescription": "無料の機能に戻すと、セキュリティ、機械学習、その他 {subscriptionFeaturesLinkText} が利用できなくなります。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModal.cancelButtonLabel": "キャンセル", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModal.startTrialButtonLabel": "トライアルを開始", - "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription": "このトライアルは、Elastic Stack の {platinumLicenseFeaturesLinkText} のフルセットが使えます。次の機能に直ちにアクセスできるようになります:", + "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription": "このトライアルは、Elastic Stack の {subscriptionFeaturesLinkText} のフルセットが使えます。次の機能に直ちにアクセスできるようになります:", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.alertingFeatureTitle": "アラート", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.dataBaseConnectivityFeatureTitle": "{sqlDataBase} の {jdbcStandard} および {odbcStandard} 接続", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.graphCapabilitiesFeatureTitle": "グラフ機能", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.mashingLearningFeatureTitle": "機械学習", - "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.platinumLicenseFeaturesLinkText": "プラチナ機能", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.securityDocumentationLinkText": "ドキュメンテーション", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.securityFeaturesConfigurationDescription": "認証 ({authenticationTypeList})、フィールドとドキュメントレベルのセキュリティ、監査などの高度なセキュリティ機能には構成が必要です。手順は {securityDocumentationLinkText} をご覧ください。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.termsAndConditionsDescription": "このトライアルを開始することで、これらの {termsAndConditionsLinkText} が適用されることに同意したものとみなされます。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.termsAndConditionsLinkText": "諸条件", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalTitle": "30 日間の無料トライアルの開始", - "xpack.licenseMgmt.licenseDashboard.startTrial.platinumFeaturesExperienceDescription": "機械学習、高度なセキュリティ、その他 {platinumLicenseFeaturesLinkText} をご体験ください。", - "xpack.licenseMgmt.licenseDashboard.startTrial.platinumLicenseFeaturesLinkText": "プラチナ機能", "xpack.licenseMgmt.licenseDashboard.startTrial.startTrialButtonLabel": "トライアルを開始", "xpack.licenseMgmt.licenseDashboard.startTrialTitle": "30 日間のトライアルの開始", "xpack.licenseMgmt.managementSectionDisplayName": "ライセンス管理", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9758893732540..856a3ec852ffe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6458,9 +6458,7 @@ "xpack.idxMgmt.freezeIndicesAction.successfullyFrozeIndicesMessage": "成功冻结:[{indexNames}]", "xpack.idxMgmt.frozenBadgeLabel": "已冻结", "xpack.idxMgmt.home.appTitle": "索引管理", - "xpack.idxMgmt.home.idxMgmtDescription": "单个或批量更新您的 Elasticsearch 索引。", "xpack.idxMgmt.home.idxMgmtDocsLinkText": "索引管理文档", - "xpack.idxMgmt.home.indexTemplatesDescription": "使用索引模板可将设置、映射和别名自动应用到索引。", "xpack.idxMgmt.home.indexTemplatesTabTitle": "索引模板", "xpack.idxMgmt.home.indicesTabTitle": "索引", "xpack.idxMgmt.indexActionsMenu.clearIndexCacheLabel": "清除 {selectedIndexCount, plural, one { 个索引} other { 个索引} } 缓存", @@ -8743,30 +8741,25 @@ "xpack.licenseMgmt.licenseDashboard.licenseStatus.permanentActiveLicenseStatusDescription": "您的许可永不会过期。", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendTrialButtonLabel": "延期试用", "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.extendYourTrialTitle": "延期您的试用", - "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.howToContinueUsingPluginsDescription": "如果您想继续使用 Machine Learning、高级安全性以及我们其他超卓的{platinumLicenseFeaturesLinkText},请立即申请延期。", - "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.platinumLicenseFeaturesLinkText": "白金级功能", + "xpack.licenseMgmt.licenseDashboard.requestTrialExtension.howToContinueUsingPluginsDescription": "如果您想继续使用 Machine Learning、高级安全性以及我们其他超卓的{subscriptionFeaturesLinkText},请立即申请延期。", "xpack.licenseMgmt.licenseDashboard.revertToBasic.acknowledgeModal.revertToBasicButtonLabel": "恢复为基础级", "xpack.licenseMgmt.licenseDashboard.revertToBasic.acknowledgeModalTitle": "恢复为基础级许可", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModal.cancelButtonLabel": "取消", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModal.confirmButtonLabel": "确认", "xpack.licenseMgmt.licenseDashboard.revertToBasic.confirmModalTitle": "确认恢复为基础级许可", - "xpack.licenseMgmt.licenseDashboard.revertToBasic.platinumLicenseFeaturesLinkText": "白金级功能", - "xpack.licenseMgmt.licenseDashboard.revertToBasic.revertToFreeFeaturesDescription": "您将恢复到我们的免费功能,并失去对 Machine Learning、高级安全性和其他{platinumLicenseFeaturesLinkText}的访问权限。", + "xpack.licenseMgmt.licenseDashboard.revertToBasic.revertToFreeFeaturesDescription": "您将恢复到我们的免费功能,并失去对 Machine Learning、高级安全性和其他{subscriptionFeaturesLinkText}的访问权限。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModal.cancelButtonLabel": "取消", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModal.startTrialButtonLabel": "开始我的试用", - "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription": "此试用具有 Elastic Stack 的全套{platinumLicenseFeaturesLinkText}您立即可以访问:", + "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription": "此试用具有 Elastic Stack 的全套{subscriptionFeaturesLinkText}您立即可以访问:", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.alertingFeatureTitle": "告警", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.dataBaseConnectivityFeatureTitle": "{sqlDataBase} 的 {jdbcStandard} 和 {odbcStandard} 连接性", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.graphCapabilitiesFeatureTitle": "图表功能", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.mashingLearningFeatureTitle": "Machine Learning", - "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.platinumLicenseFeaturesLinkText": "白金级功能", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.securityDocumentationLinkText": "文档", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.securityFeaturesConfigurationDescription": "诸如身份验证 ({authenticationTypeList})、字段级和文档级安全以及审计等高级安全功能需要配置。有关说明,请参阅 {securityDocumentationLinkText}。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.termsAndConditionsDescription": "通过开始此试用,您同意其受这些{termsAndConditionsLinkText}约束。", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.termsAndConditionsLinkText": "条款和条件", "xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalTitle": "立即开始为期 30 天的免费试用", - "xpack.licenseMgmt.licenseDashboard.startTrial.platinumFeaturesExperienceDescription": "体验 Machine Learning、高级安全性以及我们所有其他{platinumLicenseFeaturesLinkText}能帮您做什么。", - "xpack.licenseMgmt.licenseDashboard.startTrial.platinumLicenseFeaturesLinkText": "白金级功能", "xpack.licenseMgmt.licenseDashboard.startTrial.startTrialButtonLabel": "开始试用", "xpack.licenseMgmt.licenseDashboard.startTrialTitle": "开始为期 30 天的试用", "xpack.licenseMgmt.managementSectionDisplayName": "许可管理", diff --git a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts index bede391537ec5..89720b275c63d 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts +++ b/x-pack/plugins/uptime/public/apps/uptime_overview_fetcher.ts @@ -29,29 +29,24 @@ export async function fetchUptimeOverviewData({ stats: { monitors: { type: 'number', - label: 'Monitors', value: snapshot.total, }, up: { type: 'number', - label: 'Up', value: snapshot.up, }, down: { type: 'number', - label: 'Down', value: snapshot.down, }, }, series: { up: { - label: 'Up', coordinates: pings.histogram.map((p) => { return { x: p.x!, y: p.upCount || 0 }; }), }, down: { - label: 'Down', coordinates: pings.histogram.map((p) => { return { x: p.x!, y: p.downCount || 0 }; }), diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index 45833012cb475..e8381aa9d59ea 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -22,7 +22,7 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); - const { body: apiKeyBody } = await esClient.security.createApiKey({ + const { body: apiKeyBody } = await esClient.security.createApiKey({ body: { name: `test access api key: ${uuid.v4()}`, }, diff --git a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts index d24f7f495a06c..8942deafdd83c 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts @@ -22,7 +22,7 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); - const { body: apiKeyBody } = await esClient.security.createApiKey({ + const { body: apiKeyBody } = await esClient.security.createApiKey({ body: { name: `test access api key: ${uuid.v4()}`, }, diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index b4d23a2392320..e9f7471f6437e 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -25,7 +25,7 @@ export default function (providerContext: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); - const { body: apiKeyBody } = await esClient.security.createApiKey({ + const { body: apiKeyBody } = await esClient.security.createApiKey({ body: { name: `test access api key: ${uuid.v4()}`, }, diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts index 74ab59f2ffdc6..afd1faadcde5f 100644 --- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts +++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts @@ -53,7 +53,8 @@ export default function ({ getService }: FtrProviderContext) { await deleteComposableIndexTemplate(name); }; - describe('Data streams', function () { + // Failing ES Promotion: https://github.com/elastic/kibana/issues/71018 + describe.skip('Data streams', function () { describe('Get', () => { const testDataStreamName = 'test-data-stream'; diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz new file mode 100644 index 0000000000000..ad3f2351ed30a Binary files /dev/null and b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz differ diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json new file mode 100644 index 0000000000000..48ac74d97dfa7 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json @@ -0,0 +1,600 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "apm-8.0.0-transaction-005", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "client": { + "properties": { + "geo": { + "properties": { + "continent_name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "country_iso_code": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "location": { + "properties": { + "lat": { + "type": "float" + }, + "lon": { + "type": "float" + } + } + } + } + }, + "ip": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ecs": { + "properties": { + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "event": { + "properties": { + "ingested": { + "type": "date" + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "referrer": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "response": { + "properties": { + "decoded_body_size": { + "type": "long" + }, + "encoded_body_size": { + "type": "long" + }, + "transfer_size": { + "type": "long" + } + } + } + } + }, + "observer": { + "properties": { + "ephemeral_id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "hostname": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version_major": { + "type": "long" + } + } + }, + "processor": { + "properties": { + "event": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "service": { + "properties": { + "language": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "source": { + "properties": { + "ip": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "timestamp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "trace": { + "properties": { + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "transaction": { + "properties": { + "custom": { + "properties": { + "userConfig": { + "properties": { + "featureFlags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "showDashboard": { + "type": "boolean" + } + } + } + } + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "marks": { + "properties": { + "agent": { + "properties": { + "domComplete": { + "type": "long" + }, + "domInteractive": { + "type": "long" + }, + "firstContentfulPaint": { + "type": "float" + }, + "largestContentfulPaint": { + "type": "float" + }, + "timeToFirstByte": { + "type": "long" + } + } + }, + "navigationTiming": { + "properties": { + "connectEnd": { + "type": "long" + }, + "connectStart": { + "type": "long" + }, + "domComplete": { + "type": "long" + }, + "domContentLoadedEventEnd": { + "type": "long" + }, + "domContentLoadedEventStart": { + "type": "long" + }, + "domInteractive": { + "type": "long" + }, + "domLoading": { + "type": "long" + }, + "domainLookupEnd": { + "type": "long" + }, + "domainLookupStart": { + "type": "long" + }, + "fetchStart": { + "type": "long" + }, + "loadEventEnd": { + "type": "long" + }, + "loadEventStart": { + "type": "long" + }, + "requestStart": { + "type": "long" + }, + "responseEnd": { + "type": "long" + }, + "responseStart": { + "type": "long" + } + } + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "page": { + "properties": { + "referer": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "sampled": { + "type": "boolean" + }, + "span_count": { + "properties": { + "started": { + "type": "long" + } + } + }, + "type": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "url": { + "properties": { + "domain": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "full": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "original": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "path": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "port": { + "type": "long" + }, + "scheme": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "user": { + "properties": { + "email": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "id": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "original": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "os": { + "properties": { + "full": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "version": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 1a00f7e2df9e8..37328badcb794 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -11,5 +11,6 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr this.tags('ciGroup1'); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./service_maps')); + loadTestFile(require.resolve('./rum_services')); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/rum_services.ts b/x-pack/test/apm_api_integration/trial/tests/rum_services.ts new file mode 100644 index 0000000000000..5505387de54a7 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/rum_services.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('RUM Services', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql([]); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns rum services list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/services?start=2020-06-28T10%3A24%3A46.055Z&end=2020-07-29T10%3A24%3A46.055Z&uiFilters=%7B%22agentName%22%3A%5B%22js-base%22%2C%22rum-js%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expect(response.body).to.eql(['client', 'opbean-client-rum']); + }); + }); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts index cc6fa53939f60..c88b094879ac8 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts @@ -22,7 +22,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - describe('find_statuses', () => { + // FLAKY: https://github.com/elastic/kibana/issues/69632 + describe.skip('find_statuses', () => { beforeEach(async () => { await createSignalsIndex(supertest); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 4a79610cadbde..2c6edeba2129f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('classification creation', function () { + + describe('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); @@ -96,9 +96,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); - it('inputs the model memory limit', async () => { + it('accepts the suggested model memory limit', async () => { await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); - await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); + await ml.dataFrameAnalyticsCreation.assertModelMemoryInputPopulated(); }); it('continues to the details step', async () => { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 65e6dc9b4ea74..6cdb9caa1e2db 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -11,8 +11,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // Flaky: https://github.com/elastic/kibana/issues/70906 - describe.skip('outlier detection creation', function () { + describe('outlier detection creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); @@ -93,9 +92,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); - it('inputs the model memory limit', async () => { + it('accepts the suggested model memory limit', async () => { await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); - await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); + await ml.dataFrameAnalyticsCreation.assertModelMemoryInputPopulated(); }); it('continues to the details step', async () => { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 33f0ee9cd99ac..03117d4cc419d 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('regression creation', function () { + + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); @@ -96,9 +96,9 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); - it('inputs the model memory limit', async () => { + it('accepts the suggested model memory limit', async () => { await ml.dataFrameAnalyticsCreation.assertModelMemoryInputExists(); - await ml.dataFrameAnalyticsCreation.setModelMemory(testData.modelMemory); + await ml.dataFrameAnalyticsCreation.assertModelMemoryInputPopulated(); }); it('continues to the details step', async () => { diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index ece0c0a6c7854..c3b9d20b3ac4a 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -8,6 +8,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { const PageObjects = getPageObjects(['common', 'header', 'security', 'login', 'spaceSelector']); const testSubjects = getService('testSubjects'); const security = getService('security'); + const find = getService('find'); return new (class MonitoringPage { async navigateTo(useSuperUser = false) { @@ -25,6 +26,11 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { await PageObjects.common.navigateToApp('monitoring'); } + async getWelcome() { + const el = await find.byCssSelector('.euiCallOut--primary', 10000 * 10); + return await el.getVisibleText(); + } + async getAccessDeniedMessage() { return testSubjects.getVisibleText('accessDeniedTitle'); } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 918c982de02ed..1b756bbaca5d8 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -306,6 +306,15 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( ); }, + async assertModelMemoryInputPopulated() { + const actualModelMemory = await testSubjects.getAttribute( + 'mlAnalyticsCreateJobWizardModelMemoryInput', + 'value' + ); + + expect(actualModelMemory).not.to.be(''); + }, + async assertPredictionFieldNameValue(expectedValue: string) { const actualPredictedFieldName = await testSubjects.getAttribute( 'mlAnalyticsCreateJobWizardPredictionFieldNameInput', diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 7531c406a4118..6971d9f523e7e 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -13,14 +13,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); - describe('endpoint list', function () { + describe('host list', function () { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); describe('when there is data,', () => { before(async () => { await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); - await pageObjects.endpoint.navigateToEndpointList(); + await pageObjects.endpoint.navigateToHostList(); }); after(async () => { await deleteMetadataStream(getService); @@ -28,7 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('finds page title', async () => { const title = await testSubjects.getVisibleText('pageViewHeaderLeftTitle'); - expect(title).to.equal('Endpoints'); + expect(title).to.equal('Hosts'); }); it('displays table data', async () => { @@ -129,7 +129,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // This set of tests fails the flyout does not open in the before() and will be fixed in soon describe.skip('has a url with a host id', () => { before(async () => { - await pageObjects.endpoint.navigateToEndpointList( + await pageObjects.endpoint.navigateToHostList( 'selected_host=fc0ff548-feba-41b6-8367-65e8790d0eaf' ); }); @@ -178,7 +178,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { before(async () => { // clear out the data and reload the page await deleteMetadataStream(getService); - await pageObjects.endpoint.navigateToEndpointList(); + await pageObjects.endpoint.navigateToHostList(); }); it('displays empty Policy Table page.', async () => { await testSubjects.existOrFail('emptyPolicyTable'); diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index f02a6bdcd51ed..2d94163fa1018 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -23,7 +23,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { apps: { ...xpackFunctionalConfig.get('apps'), ['securitySolutionManagement']: { - pathname: '/app/security/management', + pathname: '/app/security/administration', }, }, kbnTestServer: { diff --git a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts index 7339903d74a0b..ae4320fc5395f 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts @@ -14,12 +14,12 @@ export function EndpointPageProvider({ getService, getPageObjects }: FtrProvider return { /** - * Navigate to the Endpoints list page + * Navigate to the Hosts list page */ - async navigateToEndpointList(searchParams?: string) { + async navigateToHostList(searchParams?: string) { await pageObjects.common.navigateToUrlWithBrowserHistory( 'securitySolutionManagement', - `/endpoints${searchParams ? `?${searchParams}` : ''}` + `/hosts${searchParams ? `?${searchParams}` : ''}` ); await pageObjects.header.waitUntilLoadingHasFinished(); }, diff --git a/x-pack/test/stack_functional_integration/configs/build_state.js b/x-pack/test/stack_functional_integration/configs/build_state.js new file mode 100644 index 0000000000000..abf1bff56331a --- /dev/null +++ b/x-pack/test/stack_functional_integration/configs/build_state.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import dotEnv from 'dotenv'; +import testsList from './tests_list'; + +// envObj :: path -> {} +const envObj = (path) => dotEnv.config({ path }); + +// default fn :: path -> {} +export default (path) => { + const obj = envObj(path).parsed; + return { tests: testsList(obj), ...obj }; +}; diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js new file mode 100644 index 0000000000000..a34d158496ba0 --- /dev/null +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import buildState from './build_state'; +import { ToolingLog } from '@kbn/dev-utils'; +import chalk from 'chalk'; +import { esTestConfig, kbnTestConfig } from '@kbn/test'; + +const reportName = 'Stack Functional Integration Tests'; +const testsFolder = '../test/functional/apps'; +const stateFilePath = '../../../../../integration-test/qa/envvars.sh'; +const prepend = (testFile) => require.resolve(`${testsFolder}/${testFile}`); +const log = new ToolingLog({ + level: 'info', + writeTo: process.stdout, +}); + +export default async ({ readConfigFile }) => { + const defaultConfigs = await readConfigFile(require.resolve('../../functional/config')); + const { tests, ...provisionedConfigs } = buildState(resolve(__dirname, stateFilePath)); + + const servers = { + kibana: kbnTestConfig.getUrlParts(), + elasticsearch: esTestConfig.getUrlParts(), + }; + log.info(`servers data: ${JSON.stringify(servers)}`); + const settings = { + ...defaultConfigs.getAll(), + junit: { + reportName: `${reportName} - ${provisionedConfigs.VM}`, + }, + servers, + testFiles: tests.map(prepend).map(logTest), + // testFiles: ['monitoring'].map(prepend).map(logTest), + // If we need to do things like disable animations, we can do it in configure_start_kibana.sh, in the provisioner...which lives in the integration-test private repo + uiSettings: {}, + security: { disableTestUser: true }, + }; + return settings; +}; + +// Returns index 1 from the resulting array-like. +const splitRight = (re) => (testPath) => re.exec(testPath)[1]; + +function truncate(testPath) { + const dropKibanaPath = splitRight(/^.+kibana[\\/](.*$)/gm); + return dropKibanaPath(testPath); +} +function highLight(testPath) { + const dropTestsPath = splitRight(/^.+test[\\/]functional[\\/]apps[\\/](.*)[\\/]/gm); + const cleaned = dropTestsPath(testPath); + const colored = chalk.greenBright.bold(cleaned); + return testPath.replace(cleaned, colored); +} +function logTest(testPath) { + log.info(`Testing: '${highLight(truncate(testPath))}'`); + return testPath; +} diff --git a/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js new file mode 100644 index 0000000000000..933a59e4e25b9 --- /dev/null +++ b/x-pack/test/stack_functional_integration/configs/config.stack_functional_integration_base_ie.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export default async ({ readConfigFile }) => { + const baseConfigs = await readConfigFile( + require.resolve('./config.stack_functional_integration_base.js') + ); + return { + ...baseConfigs.getAll(), + browser: { + type: 'ie', + }, + security: { disableTestUser: true }, + }; +}; diff --git a/x-pack/test/stack_functional_integration/configs/tests_list.js b/x-pack/test/stack_functional_integration/configs/tests_list.js new file mode 100644 index 0000000000000..ff68cb6285965 --- /dev/null +++ b/x-pack/test/stack_functional_integration/configs/tests_list.js @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// testsList :: {} -> list +export default (envObj) => { + const xs = []; + // one of these 2 needs to create the default index pattern + if (envObj.PRODUCTS.includes('logstash')) { + xs.push('management'); + } else { + xs.push('sample_data'); + } + + // get the opt in/out banner out of the way early + if (envObj.XPACK === 'YES') { + xs.push('telemetry'); + } + + if (envObj.BEATS.includes('metricbeat')) { + xs.push('metricbeat'); + } + if (envObj.BEATS.includes('filebeat')) { + xs.push('filebeat'); + } + if (envObj.BEATS.includes('packetbeat')) { + xs.push('packetbeat'); + } + if (envObj.BEATS.includes('winlogbeat')) { + xs.push('winlogbeat'); + } + if (envObj.BEATS.includes('heartbeat')) { + xs.push('heartbeat'); + } + if (envObj.VM === 'ubuntu16_tar_ccs') { + xs.push('ccs'); + } + + // with latest elasticsearch Js client, we can only run these watcher tests + // which use the watcher API on a config with x-pack but without TLS (no security) + if (envObj.VM === 'ubuntu16_tar') { + xs.push('reporting'); + } + + if (envObj.XPACK === 'YES' && ['TRIAL', 'GOLD', 'PLATINUM'].includes(envObj.LICENSE)) { + // we can't test enabling monitoring on this config because we already enable it through cluster settings for both clusters. + if (envObj.VM !== 'ubuntu16_tar_ccs') { + // monitoring is last because we switch to the elastic superuser here + xs.push('monitoring'); + } + } + + return xs; +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/ccs/ccs.js b/x-pack/test/stack_functional_integration/test/functional/apps/ccs/ccs.js new file mode 100644 index 0000000000000..a952824d8db61 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/ccs/ccs.js @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default ({ getService, getPageObjects }) => { + describe('Cross cluster search test', async () => { + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'discover', + 'security', + 'header', + 'timePicker', + ]); + const retry = getService('retry'); + const log = getService('log'); + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const kibanaServer = getService('kibanaServer'); + + before(async () => { + await browser.setWindowSize(1200, 800); + // pincking relative time in timepicker isn't working. This is also faster. + // It's the default set, plus new "makelogs" +/- 3 days from now + await kibanaServer.uiSettings.replace({ + 'timepicker:quickRanges': `[ + { + "from": "now-3d", + "to": "now+3d", + "display": "makelogs" + }, + { + "from": "now/d", + "to": "now/d", + "display": "Today" + }, + { + "from": "now/w", + "to": "now/w", + "display": "This week" + }, + { + "from": "now-15m", + "to": "now", + "display": "Last 15 minutes" + }, + { + "from": "now-30m", + "to": "now", + "display": "Last 30 minutes" + }, + { + "from": "now-1h", + "to": "now", + "display": "Last 1 hour" + }, + { + "from": "now-24h", + "to": "now", + "display": "Last 24 hours" + }, + { + "from": "now-7d", + "to": "now", + "display": "Last 7 days" + }, + { + "from": "now-30d", + "to": "now", + "display": "Last 30 days" + }, + { + "from": "now-90d", + "to": "now", + "display": "Last 90 days" + }, + { + "from": "now-1y", + "to": "now", + "display": "Last 1 year" + } + ]`, + }); + }); + + before(async () => { + if (process.env.SECURITY === 'YES') { + log.debug( + '### provisionedEnv.SECURITY === YES so log in as elastic superuser to create cross cluster indices' + ); + await PageObjects.security.logout(); + } + const url = await browser.getCurrentUrl(); + log.debug(url); + if (!url.includes('kibana')) { + await PageObjects.common.navigateToApp('management', { insertTimestamp: false }); + } else if (!url.includes('management')) { + await appsMenu.clickLink('Management'); + } + }); + + it('create local admin makelogs index pattern', async () => { + log.debug('create local admin makelogs工程 index pattern'); + await PageObjects.settings.createIndexPattern('local:makelogs工程*'); + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('local:makelogs工程*'); + }); + + it('create remote data makelogs index pattern', async () => { + log.debug('create remote data makelogs工程 index pattern'); + await PageObjects.settings.createIndexPattern('data:makelogs工程*'); + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('data:makelogs工程*'); + }); + + it('create comma separated index patterns for data and local makelogs index pattern', async () => { + log.debug( + 'create comma separated index patterns for data and local makelogs工程 index pattern' + ); + await PageObjects.settings.createIndexPattern('data:makelogs工程-*,local:makelogs工程-*'); + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('data:makelogs工程-*,local:makelogs工程-*'); + }); + + it('create index pattern for data from both clusters', async () => { + await PageObjects.settings.createIndexPattern('*:makelogs工程-*', '@timestamp', true, false); + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('*:makelogs工程-*'); + }); + + it('local:makelogs(star) should discover data from the local cluster', async () => { + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + + await PageObjects.discover.selectIndexPattern('local:makelogs工程*'); + await PageObjects.timePicker.setCommonlyUsedTime('makelogs'); + await retry.tryForTime(40000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be('14,005'); + }); + }); + + it('data:makelogs(star) should discover data from remote', async function () { + await PageObjects.discover.selectIndexPattern('data:makelogs工程*'); + await retry.tryForTime(40000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be('14,005'); + }); + }); + + it('star:makelogs-star should discover data from both clusters', async function () { + await PageObjects.discover.selectIndexPattern('*:makelogs工程-*'); + await PageObjects.timePicker.setCommonlyUsedTime('makelogs'); + await retry.tryForTime(40000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be('28,010'); + }); + }); + + it('data:makelogs-star,local:makelogs-star should discover data from both clusters', async function () { + await PageObjects.discover.selectIndexPattern('data:makelogs工程-*,local:makelogs工程-*'); + await retry.tryForTime(40000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + log.debug('### hit count = ' + hitCount); + expect(hitCount).to.be('28,010'); + }); + }); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/ccs/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/ccs/index.js new file mode 100644 index 0000000000000..e31a903cf0be2 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/ccs/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('ccs test', function () { + loadTestFile(require.resolve('./ccs')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/filebeat.js b/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/filebeat.js new file mode 100644 index 0000000000000..14d06ac296ba3 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/filebeat.js @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + describe('check filebeat', function () { + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + + it('filebeat- should have hit count GT 0', async function () { + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + await PageObjects.discover.selectIndexPattern('filebeat-*'); + await PageObjects.timePicker.setCommonlyUsedTime('Last_30 days'); + await retry.try(async () => { + const hitCount = parseInt(await PageObjects.discover.getHitCount()); + expect(hitCount).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/index.js new file mode 100644 index 0000000000000..c3a81ca43a68f --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/filebeat/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('filebeat app', function () { + loadTestFile(require.resolve('./filebeat')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/_heartbeat.js b/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/_heartbeat.js new file mode 100644 index 0000000000000..4e1c02b627de0 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/_heartbeat.js @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'uptime']); + + describe('check heartbeat', function () { + it('Uptime app should show snapshot count greater than zero', async function () { + await PageObjects.common.navigateToApp('uptime', { insertTimestamp: false }); + + await retry.try(async function () { + const upCount = parseInt((await PageObjects.uptime.getSnapshotCount()).up); + expect(upCount).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/index.js new file mode 100644 index 0000000000000..28ae1bbaa488d --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/heartbeat/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('heartbeat app', function () { + require('./_heartbeat'); + loadTestFile(require.resolve('./_heartbeat')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/management/_index_pattern_create.js b/x-pack/test/stack_functional_integration/test/functional/apps/management/_index_pattern_create.js new file mode 100644 index 0000000000000..a43a2fce61ea1 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/management/_index_pattern_create.js @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default ({ getService, getPageObjects }) => { + describe('creating default index', function describeIndexTests() { + const PageObjects = getPageObjects(['common', 'settings']); + const retry = getService('retry'); + const log = getService('log'); + const browser = getService('browser'); + + before(async () => { + await PageObjects.common.navigateToApp('management', { insertTimestamp: false }); + await browser.setWindowSize(1200, 800); + }); + + it('create makelogs工程 index pattern', async function pageHeader() { + log.debug('create makelogs工程 index pattern'); + await PageObjects.settings.createIndexPattern('makelogs工程-*'); + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('makelogs工程-*'); + }); + + describe('create logstash index pattern', function indexPatternCreation() { + before(async () => { + await retry.tryForTime(120000, async () => { + log.debug('create Index Pattern'); + await PageObjects.settings.createIndexPattern(); + }); + }); + + it('should have index pattern in page header', async function pageHeader() { + const patternName = await PageObjects.settings.getIndexPageHeading(); + expect(patternName).to.be('logstash-*'); + }); + + it('should have expected table headers', async function checkingHeader() { + const headers = await PageObjects.settings.getTableHeader(); + log.debug('header.length = ' + headers.length); + const expectedHeaders = [ + 'Name', + 'Type', + 'Format', + 'Searchable', + 'Aggregatable', + 'Excluded', + ]; + + expect(headers.length).to.be(expectedHeaders.length); + + await Promise.all( + headers.map(async function compareHead(header, i) { + const text = await header.getVisibleText(); + expect(text).to.be(expectedHeaders[i]); + }) + ); + }); + }); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/management/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/management/index.js new file mode 100644 index 0000000000000..6e032c198bc6a --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/management/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('settings / management app', function () { + loadTestFile(require.resolve('./_index_pattern_create')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/_metricbeat.js b/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/_metricbeat.js new file mode 100644 index 0000000000000..8f6ddff180695 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/_metricbeat.js @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + const log = getService('log'); + const retry = getService('retry'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const appsMenu = getService('appsMenu'); + + describe('check metricbeat', function () { + it('metricbeat- should have hit count GT 0', async function () { + const url = await browser.getCurrentUrl(); + log.debug(url); + if (!url.includes('kibana')) { + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + } else if (!url.includes('discover')) { + await appsMenu.clickLink('Discover'); + } + + await PageObjects.discover.selectIndexPattern('metricbeat-*'); + await PageObjects.timePicker.setCommonlyUsedTime('Today'); + await retry.try(async function () { + const hitCount = parseInt(await PageObjects.discover.getHitCount()); + expect(hitCount).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/index.js new file mode 100644 index 0000000000000..d45d6c835a315 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/metricbeat/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('metricbeat app', function () { + loadTestFile(require.resolve('./_metricbeat')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/_monitoring.js b/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/_monitoring.js new file mode 100644 index 0000000000000..623937b178833 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/_monitoring.js @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default ({ getService, getPageObjects }) => { + describe('monitoring app - stack functional integration - suite', () => { + const browser = getService('browser'); + const PageObjects = getPageObjects(['security', 'monitoring', 'common']); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const isSaml = !!process.env.VM.includes('saml') || !!process.env.VM.includes('oidc'); + + before(async () => { + await browser.setWindowSize(1200, 800); + if (process.env.SECURITY === 'YES' && !isSaml) { + await PageObjects.security.logout(); + log.debug('### log in as elastic superuser to enable monitoring'); + // Tests may be running as a non-superuser like `power` but that user + // doesn't have the cluster privs to enable monitoring. + // On the SAML config, this will fail, but the test recovers on the next + // navigate and logs in as the saml user. + } + // navigateToApp without a username and password will default to the superuser + await PageObjects.common.navigateToApp('monitoring', { insertTimestamp: false }); + }); + + it('should enable Monitoring', async () => { + await testSubjects.click('useInternalCollection'); + await testSubjects.click('enableCollectionEnabled'); + }); + + after(async () => { + if (process.env.SECURITY === 'YES' && !isSaml) { + await PageObjects.security.forceLogout(isSaml); + } + }); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/index.js new file mode 100644 index 0000000000000..f6ea0ae4aa2b5 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/monitoring/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('monitoring app - stack functional integration - index', function () { + loadTestFile(require.resolve('./_monitoring')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/_packetbeat.js b/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/_packetbeat.js new file mode 100644 index 0000000000000..e09ac478fccbd --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/_packetbeat.js @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + const log = getService('log'); + const retry = getService('retry'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const appsMenu = getService('appsMenu'); + + describe('check packetbeat', function () { + before(function () { + log.debug('navigateToApp Discover'); + }); + + it('packetbeat- should have hit count GT 0', async function () { + const url = await browser.getCurrentUrl(); + log.debug(url); + if (!url.includes('kibana')) { + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + } + if (!url.includes('discover')) { + await appsMenu.clickLink('Discover'); + } + await PageObjects.discover.selectIndexPattern('packetbeat-*'); + await PageObjects.timePicker.setCommonlyUsedTime('Today'); + await retry.try(async function () { + const hitCount = parseInt(await PageObjects.discover.getHitCount()); + expect(hitCount).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/index.js new file mode 100644 index 0000000000000..5bb4582eb16de --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/packetbeat/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('packetbeat app', function () { + loadTestFile(require.resolve('./_packetbeat')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/reporting/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/index.js new file mode 100644 index 0000000000000..98771a57693a2 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/index.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService, loadTestFile }) { + describe('reporting app', function () { + const browser = getService('browser'); + + before(async () => { + await browser.setWindowSize(1200, 800); + }); + + loadTestFile(require.resolve('./reporting_watcher_png')); + loadTestFile(require.resolve('./reporting_watcher')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher.js b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher.js new file mode 100644 index 0000000000000..c373c797bef50 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher.js @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getWatcher, deleteWatcher, putWatcher } from './util'; + +export default function ({ getService, getPageObjects }) { + describe('watcher app', function describeIndexTests() { + const config = getService('config'); + const servers = config.get('servers'); + const retry = getService('retry'); + const log = getService('log'); + const client = getService('es'); + + const KIBANAIP = process.env.KIBANAIP; + const VERSION_NUMBER = process.env.VERSION_NUMBER; + const VM = process.env.VM; + const VERSION_BUILD_HASH = process.env.VERSION_BUILD_HASH; + const STARTEDBY = process.env.STARTEDBY; + const REPORTING_TEST_EMAILS = process.env.REPORTING_TEST_EMAILS; + + const PageObjects = getPageObjects(['common']); + describe('PDF Reporting watch', function () { + let id = 'watcher_report-'; + id = id + new Date().getTime(); // For debugging. + const watch = { id }; + const interval = 10; + const emails = REPORTING_TEST_EMAILS.split(','); + + // https://localhost:5601/api/reporting/generate/printablePdf?jobParams=(objectType:dashboard,queryString:%27_g%3D(refreshInterval%3A(display%3AOff%2Cpause%3A!!f%2Cvalue%3A0)%2Ctime%3A(from%3Anow-7d%2Cmode%3Aquick%2Cto%3Anow))%26_a%3D(description%3A%2527%2527%2Cfilters%3A!!()%2CfullScreenMode%3A!!f%2Coptions%3A(darkTheme%3A!!f)%2Cpanels%3A!!((col%3A1%2Cid%3ASystem-Navigation%2CpanelIndex%3A9%2Crow%3A1%2Csize_x%3A8%2Csize_y%3A1%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3Ac6f2ffd0-4d17-11e7-a196-69b9a7a020a9%2CpanelIndex%3A11%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3Afe064790-1b1f-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A12%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%2527855899e0-1b1c-11e7-b09e-037021c4f8df%2527%2CpanelIndex%3A13%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%25277cdb1330-4d1a-11e7-a196-69b9a7a020a9%2527%2CpanelIndex%3A14%2Crow%3A9%2Csize_x%3A12%2Csize_y%3A6%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3A%2527522ee670-1b92-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A16%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A11%2Cid%3A%25271aae9140-1b93-11e7-8ada-3df93aab833e%2527%2CpanelIndex%3A17%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3A%2527825fdb80-4d1d-11e7-b5f2-2b7c1895bf32%2527%2CpanelIndex%3A18%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A5%2Cid%3Ad3166e80-1b91-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A19%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A3%2Cid%3A%252783e12df0-1b91-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A20%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3Ae9d22060-4d64-11e7-aa29-87a97a796de6%2CpanelIndex%3A21%2Crow%3A1%2Csize_x%3A4%2Csize_y%3A1%2Ctype%3Avisualization))%2Cquery%3A(language%3Alucene%2Cquery%3A(query_string%3A(analyze_wildcard%3A!!t%2Cquery%3A%2527*%2527)))%2CtimeRestore%3A!!f%2Ctitle%3A%2527Metricbeat%2Bsystem%2Boverview%2527%2CuiState%3A(P-11%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-12%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-14%3A(vis%3A(defaultColors%3A(%25270%2525%2B-%2B8.75%2525%2527%3A%2527rgb(247%2C252%2C245)%2527%2C%252717.5%2525%2B-%2B26.25%2525%2527%3A%2527rgb(116%2C196%2C118)%2527%2C%252726.25%2525%2B-%2B35%2525%2527%3A%2527rgb(35%2C139%2C69)%2527%2C%25278.75%2525%2B-%2B17.5%2525%2527%3A%2527rgb(199%2C233%2C192)%2527)))%2CP-16%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-2%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-3%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527))))%2CviewMode%3Aview)%27,savedObjectId:Metricbeat-system-overview) + // https://localhost:5601/api/reporting/generate/printablePdf?jobParams=(objectType:dashboard,queryString:%27_g%3D()%26_a%3D(description%3A%2527%2527%2Cfilters%3A!!()%2CfullScreenMode%3A!!f%2Coptions%3A(darkTheme%3A!!f)%2Cpanels%3A!!((col%3A1%2Cid%3ASystem-Navigation%2CpanelIndex%3A9%2Crow%3A1%2Csize_x%3A12%2Csize_y%3A1%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3Ac6f2ffd0-4d17-11e7-a196-69b9a7a020a9%2CpanelIndex%3A11%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3Afe064790-1b1f-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A12%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%2527855899e0-1b1c-11e7-b09e-037021c4f8df%2527%2CpanelIndex%3A13%2Crow%3A4%2Csize_x%3A6%2Csize_y%3A5%2Ctype%3Avisualization)%2C(col%3A1%2Cid%3A%25277cdb1330-4d1a-11e7-a196-69b9a7a020a9%2527%2CpanelIndex%3A14%2Crow%3A9%2Csize_x%3A12%2Csize_y%3A6%2Ctype%3Avisualization)%2C(col%3A9%2Cid%3A%2527522ee670-1b92-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A16%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A11%2Cid%3A%25271aae9140-1b93-11e7-8ada-3df93aab833e%2527%2CpanelIndex%3A17%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A7%2Cid%3A%2527825fdb80-4d1d-11e7-b5f2-2b7c1895bf32%2527%2CpanelIndex%3A18%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A5%2Cid%3Ad3166e80-1b91-11e7-bec4-a5e9ec5cab8b%2CpanelIndex%3A19%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization)%2C(col%3A3%2Cid%3A%252783e12df0-1b91-11e7-bec4-a5e9ec5cab8b%2527%2CpanelIndex%3A20%2Crow%3A2%2Csize_x%3A2%2Csize_y%3A2%2Ctype%3Avisualization))%2Cquery%3A(language%3Alucene%2Cquery%3A(query_string%3A(analyze_wildcard%3A!!t%2Cdefault_field%3A%2527*%2527%2Cquery%3A%2527*%2527)))%2CtimeRestore%3A!!f%2Ctitle%3A%2527%255BMetricbeat%2BSystem%255D%2BOverview%2527%2CuiState%3A(P-11%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-12%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-14%3A(vis%3A(defaultColors%3A(%25270%2525%2B-%2B8.75%2525%2527%3A%2527rgb(247%2C252%2C245)%2527%2C%252717.5%2525%2B-%2B26.25%2525%2527%3A%2527rgb(116%2C196%2C118)%2527%2C%252726.25%2525%2B-%2B35%2525%2527%3A%2527rgb(35%2C139%2C69)%2527%2C%25278.75%2525%2B-%2B17.5%2525%2527%3A%2527rgb(199%2C233%2C192)%2527)))%2CP-16%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-2%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527)))%2CP-3%3A(vis%3A(defaultColors%3A(%25270%2B-%2B100%2527%3A%2527rgb(0%2C104%2C55)%2527))))%2CviewMode%3Aview)%27,savedObjectId:Metricbeat-system-overview) + // https://localhost:5601 + // "/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FChicago,layout:(dimensions:(height:540.5,width:633),id:preserve_layout),objectType:visualization,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fvisualize%2Fedit%2FLatency-histogram%3F_g%3D(refreshInterval:(pause:!!t,value:0),time:(from:now-24h,mode:quick,to:now))%26_a%3D(filters:!!(),linked:!!t,query:(language:lucene,query:!%27!%27),uiState:(),vis:(aggs:!!((enabled:!!t,id:!%271!%27,params:(),schema:metric,type:count),(enabled:!!t,id:!%272!%27,params:(extended_bounds:(),field:responsetime,interval:10),schema:segment,type:histogram)),params:(addLegend:!!t,addTimeMarker:!!f,addTooltip:!!t,categoryAxes:!!((id:CategoryAxis-1,labels:(show:!!t,truncate:100),position:bottom,scale:(type:linear),show:!!t,style:(),title:(),type:category)),defaultYExtents:!!f,grid:(categoryLines:!!f,style:(color:%2523eee)),interpolate:linear,legendPosition:right,mode:stacked,scale:linear,seriesParams:!!((data:(id:!%271!%27,label:Count),interpolate:cardinal,mode:stacked,show:true,type:area,valueAxis:ValueAxis-1)),setYExtents:!!f,shareYAxis:!!t,smoothLines:!!t,times:!!(),type:area,valueAxes:!!((id:ValueAxis-1,labels:(filter:!!f,rotate:0,show:!!t,truncate:100),name:LeftAxis-1,position:left,scale:(defaultYExtents:!!f,mode:normal,setYExtents:!!f,type:linear),show:!!t,style:(),title:(text:Count),type:value)),yAxis:()),title:!%27Latency%2Bhistogram!%27,type:area))%27),title:%27Latency%20histogram%27) + const url = + servers.kibana.protocol + + '://' + + KIBANAIP + + ':' + + servers.kibana.port + + '/api/reporting/generate/printablePdf?jobParams=(browserTimezone:America%2FChicago,layout:(dimensions:(height:2024,width:3006.400146484375),id:preserve_layout),objectType:dashboard,relativeUrls:!(%27%2Fapp%2Fkibana%23%2Fdashboard%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D(refreshInterval:(pause:!!f,value:900000),time:(from:now-7d,to:now))%26_a%3D(description:!%27Analyze%2Bmock%2BeCommerce%2Borders%2Band%2Brevenue!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(vis:(colors:(!%27Men!!!%27s%2BAccessories!%27:%252382B5D8,!%27Men!!!%27s%2BClothing!%27:%2523F9BA8F,!%27Men!!!%27s%2BShoes!%27:%2523F29191,!%27Women!!!%27s%2BAccessories!%27:%2523F4D598,!%27Women!!!%27s%2BClothing!%27:%252370DBED,!%27Women!!!%27s%2BShoes!%27:%2523B7DBAB))),gridData:(h:10,i:!%271!%27,w:36,x:12,y:18),id:!%2737cc8650-b882-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%271!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(FEMALE:%25236ED0E0,MALE:%2523447EBC),legendOpen:!!f)),gridData:(h:11,i:!%272!%27,w:12,x:12,y:7),id:ed8436b0-b88b-11e8-a6d9-e546fe2bba5f,panelIndex:!%272!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%273!%27,w:18,x:0,y:0),id:!%2709ffee60-b88c-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%273!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%274!%27,w:30,x:18,y:0),id:!%271c389590-b88d-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%274!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%275!%27,w:48,x:0,y:28),id:!%2745e07720-b890-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%275!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:10,i:!%276!%27,w:12,x:0,y:18),id:!%2710f1a240-b891-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%276!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%277!%27,w:12,x:0,y:7),id:b80e6540-b891-11e8-a6d9-e546fe2bba5f,panelIndex:!%277!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B50!%27:%2523E24D42,!%2750%2B-%2B75!%27:%2523EAB839,!%2775%2B-%2B100!%27:%25237EB26D),defaultColors:(!%270%2B-%2B50!%27:!%27rgb(165,0,38)!%27,!%2750%2B-%2B75!%27:!%27rgb(255,255,190)!%27,!%2775%2B-%2B100!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%278!%27,w:12,x:24,y:7),id:!%274b3ec120-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%278!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B2!%27:%2523E24D42,!%272%2B-%2B3!%27:%2523F2C96D,!%273%2B-%2B4!%27:%25239AC48A),defaultColors:(!%270%2B-%2B2!%27:!%27rgb(165,0,38)!%27,!%272%2B-%2B3!%27:!%27rgb(255,255,190)!%27,!%273%2B-%2B4!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%279!%27,w:12,x:36,y:7),id:!%279ca7aa90-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%279!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:18,i:!%2710!%27,w:48,x:0,y:54),id:!%273ba638e0-b894-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%2710!%27,type:search,version:!%277.0.0-alpha1!%27),(embeddableConfig:(isLayerTOCOpen:!!f,mapCenter:(lat:45.88578,lon:-15.07605,zoom:2.11),openTOCDetails:!!()),gridData:(h:15,i:!%2711!%27,w:24,x:0,y:39),id:!%272c9c1f60-1909-11e9-919b-ffe5949a18d2!%27,panelIndex:!%2711!%27,type:map,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:15,i:!%2712!%27,w:24,x:24,y:39),id:b72dd430-bb4d-11e8-9c84-77068524bcab,panelIndex:!%2712!%27,type:visualization,version:!%277.0.0-alpha1!%27)),query:(language:kuery,query:!%27!%27),timeRestore:!!t,title:!%27%255BeCommerce%255D%2BRevenue%2BDashboard!%27,viewMode:view)%27),title:%27%5BeCommerce%5D%20Revenue%20Dashboard%27)'; + + const body = { + trigger: { + schedule: { + interval: `${interval}s`, + }, + }, + actions: { + email_admin: { + email: { + to: emails, + subject: + 'PDF ' + + VERSION_NUMBER + + ' ' + + id + + ', VM=' + + VM + + ' ' + + VERSION_BUILD_HASH + + ' by:' + + STARTEDBY, + attachments: { + 'test_report.pdf': { + reporting: { + url: url, + auth: { + basic: { + username: servers.elasticsearch.username, + password: servers.elasticsearch.password, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + it('should successfully add a new watch for PDF Reporting', async () => { + await putWatcher(watch, id, body, client, log); + }); + it('should be successful and increment revision', async () => { + await getWatcher(watch, id, client, log, PageObjects.common, retry.tryForTime); + }); + it('should delete watch and update revision', async () => { + await deleteWatcher(watch, id, client, log); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher_png.js b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher_png.js new file mode 100644 index 0000000000000..ac247cc23900d --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/reporting_watcher_png.js @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getWatcher, deleteWatcher, putWatcher } from './util'; + +export default ({ getService, getPageObjects }) => { + describe('watcher app', () => { + const config = getService('config'); + const servers = config.get('servers'); + const retry = getService('retry'); + const log = getService('log'); + const client = getService('es'); + + const KIBANAIP = process.env.KIBANAIP; + const VERSION_NUMBER = process.env.VERSION_NUMBER; + const VM = process.env.VM; + const VERSION_BUILD_HASH = process.env.VERSION_BUILD_HASH; + const STARTEDBY = process.env.STARTEDBY; + const REPORTING_TEST_EMAILS = process.env.REPORTING_TEST_EMAILS; + + const PageObjects = getPageObjects(['common']); + describe('PNG Reporting watch', () => { + let id = 'watcher_png_report-'; + id = id + new Date().getTime(); // For debugging. + const watch = { id }; + const reportingUrl = + servers.kibana.protocol + + '://' + + KIBANAIP + + ':' + + servers.kibana.port + + '/api/reporting/generate/png?jobParams=(browserTimezone:America%2FChicago,layout:(dimensions:(height:2024,width:3006.400146484375),id:png),objectType:dashboard,relativeUrl:%27%2Fapp%2Fkibana%23%2Fdashboard%2F722b74f0-b882-11e8-a6d9-e546fe2bba5f%3F_g%3D(refreshInterval:(pause:!!f,value:900000),time:(from:now-7d,to:now))%26_a%3D(description:!%27Analyze%2Bmock%2BeCommerce%2Borders%2Band%2Brevenue!%27,filters:!!(),fullScreenMode:!!f,options:(hidePanelTitles:!!f,useMargins:!!t),panels:!!((embeddableConfig:(vis:(colors:(!%27Men!!!%27s%2BAccessories!%27:%252382B5D8,!%27Men!!!%27s%2BClothing!%27:%2523F9BA8F,!%27Men!!!%27s%2BShoes!%27:%2523F29191,!%27Women!!!%27s%2BAccessories!%27:%2523F4D598,!%27Women!!!%27s%2BClothing!%27:%252370DBED,!%27Women!!!%27s%2BShoes!%27:%2523B7DBAB))),gridData:(h:10,i:!%271!%27,w:36,x:12,y:18),id:!%2737cc8650-b882-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%271!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(FEMALE:%25236ED0E0,MALE:%2523447EBC),legendOpen:!!f)),gridData:(h:11,i:!%272!%27,w:12,x:12,y:7),id:ed8436b0-b88b-11e8-a6d9-e546fe2bba5f,panelIndex:!%272!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%273!%27,w:18,x:0,y:0),id:!%2709ffee60-b88c-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%273!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:7,i:!%274!%27,w:30,x:18,y:0),id:!%271c389590-b88d-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%274!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%275!%27,w:48,x:0,y:28),id:!%2745e07720-b890-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%275!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:10,i:!%276!%27,w:12,x:0,y:18),id:!%2710f1a240-b891-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%276!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:11,i:!%277!%27,w:12,x:0,y:7),id:b80e6540-b891-11e8-a6d9-e546fe2bba5f,panelIndex:!%277!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B50!%27:%2523E24D42,!%2750%2B-%2B75!%27:%2523EAB839,!%2775%2B-%2B100!%27:%25237EB26D),defaultColors:(!%270%2B-%2B50!%27:!%27rgb(165,0,38)!%27,!%2750%2B-%2B75!%27:!%27rgb(255,255,190)!%27,!%2775%2B-%2B100!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%278!%27,w:12,x:24,y:7),id:!%274b3ec120-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%278!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(vis:(colors:(!%270%2B-%2B2!%27:%2523E24D42,!%272%2B-%2B3!%27:%2523F2C96D,!%273%2B-%2B4!%27:%25239AC48A),defaultColors:(!%270%2B-%2B2!%27:!%27rgb(165,0,38)!%27,!%272%2B-%2B3!%27:!%27rgb(255,255,190)!%27,!%273%2B-%2B4!%27:!%27rgb(0,104,55)!%27),legendOpen:!!f)),gridData:(h:11,i:!%279!%27,w:12,x:36,y:7),id:!%279ca7aa90-b892-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%279!%27,type:visualization,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:18,i:!%2710!%27,w:48,x:0,y:54),id:!%273ba638e0-b894-11e8-a6d9-e546fe2bba5f!%27,panelIndex:!%2710!%27,type:search,version:!%277.0.0-alpha1!%27),(embeddableConfig:(isLayerTOCOpen:!!f,mapCenter:(lat:45.88578,lon:-15.07605,zoom:2.11),openTOCDetails:!!()),gridData:(h:15,i:!%2711!%27,w:24,x:0,y:39),id:!%272c9c1f60-1909-11e9-919b-ffe5949a18d2!%27,panelIndex:!%2711!%27,type:map,version:!%277.0.0-alpha1!%27),(embeddableConfig:(),gridData:(h:15,i:!%2712!%27,w:24,x:24,y:39),id:b72dd430-bb4d-11e8-9c84-77068524bcab,panelIndex:!%2712!%27,type:visualization,version:!%277.0.0-alpha1!%27)),query:(language:kuery,query:!%27!%27),timeRestore:!!t,title:!%27%255BeCommerce%255D%2BRevenue%2BDashboard!%27,viewMode:view)%27,title:%27%5BeCommerce%5D%20Revenue%20Dashboard%27)'; + const emails = REPORTING_TEST_EMAILS.split(','); + const interval = 10; + const body = { + trigger: { + schedule: { + interval: `${interval}s`, + }, + }, + actions: { + email_admin: { + email: { + to: emails, + subject: + 'PNG ' + + VERSION_NUMBER + + ' ' + + id + + ', VM=' + + VM + + ' ' + + VERSION_BUILD_HASH + + ' by:' + + STARTEDBY, + attachments: { + 'test_report.png': { + reporting: { + url: reportingUrl, + auth: { + basic: { + username: servers.elasticsearch.username, + password: servers.elasticsearch.password, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + it('should successfully add a new watch for PNG Reporting', async () => { + await putWatcher(watch, id, body, client, log); + }); + it('should be successful and increment revision', async () => { + await getWatcher(watch, id, client, log, PageObjects.common, retry.tryForTime); + }); + it('should delete watch and update revision', async () => { + await deleteWatcher(watch, id, client, log); + }); + }); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/reporting/util.js b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/util.js new file mode 100644 index 0000000000000..3c959656a3c57 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/reporting/util.js @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export const pretty = (x) => JSON.stringify(x, null, 2); +export const buildUrl = ({ protocol, auth, hostname, port }) => + new URL(`${protocol}://${auth}@${hostname}:${port}`); +export const putWatcher = async (watch, id, body, client, log) => { + const putWatchResponse = await client.watcher.putWatch({ ...watch, body }); + log.debug(`# putWatchResponse \n${pretty(putWatchResponse)}`); + expect(putWatchResponse.body._id).to.eql(id); + expect(putWatchResponse.statusCode).to.eql('201'); + expect(putWatchResponse.body._version).to.eql('1'); +}; +export const getWatcher = async (watch, id, client, log, common, tryForTime) => { + await common.sleep(50000); + await tryForTime( + 250000, + async () => { + await common.sleep(25000); + + await watcherHistory(id, client, log); + + const getWatchResponse = await client.watcher.getWatch(watch); + log.debug(`\n getWatchResponse: ${JSON.stringify(getWatchResponse)}`); + expect(getWatchResponse.body._id).to.eql(id); + expect(getWatchResponse.body._version).to.be.above(1); + log.debug(`\n getWatchResponse.body._version: ${getWatchResponse.body._version}`); + expect(getWatchResponse.body.status.execution_state).to.eql('executed'); + expect(getWatchResponse.body.status.actions.email_admin.last_execution.successful).to.eql( + true + ); + + return getWatchResponse; + }, + async function onFailure(obj) { + log.debug(`\n### tryForTime-Failure--raw body: \n\t${pretty(obj)}`); + } + ); +}; +export const deleteWatcher = async (watch, id, client, log) => { + const deleteResponse = await client.watcher.deleteWatch(watch); + log.debug('\nDelete Response=' + pretty(deleteResponse) + '\n'); + expect(deleteResponse.body._id).to.eql(id); + expect(deleteResponse.body.found).to.eql(true); + expect(deleteResponse.statusCode).to.eql('200'); +}; +async function watcherHistory(watchId, client, log) { + const { body } = await client.search({ + index: '.watcher-history*', + body: { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + watchId, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, + }); + log.debug(`\nwatcherHistoryResponse \n${pretty(body)}\n`); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js new file mode 100644 index 0000000000000..306f30133f6ee --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/e_commerce.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService, getPageObjects }) { + describe('eCommerce Sample Data', function sampleData() { + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'home']); + const testSubjects = getService('testSubjects'); + + before(async () => { + await browser.setWindowSize(1200, 800); + await PageObjects.common.navigateToUrl('home', '/home/tutorial_directory/sampleData', { + useActualUrl: true, + insertTimestamp: false, + }); + await PageObjects.common.sleep(3000); + }); + + it('install eCommerce sample data', async function installECommerceData() { + await PageObjects.home.addSampleDataSet('ecommerce'); + await PageObjects.common.sleep(5000); + // verify it's installed by finding the remove link + await testSubjects.find('removeSampleDataSetecommerce'); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/index.js new file mode 100644 index 0000000000000..4b9178c753b9a --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/sample_data/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default ({ loadTestFile }) => { + describe('sample data', function () { + loadTestFile(require.resolve('./e_commerce')); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/_telemetry.js b/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/_telemetry.js new file mode 100644 index 0000000000000..09698675f0678 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/_telemetry.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default ({ getService, getPageObjects }) => { + const log = getService('log'); + const browser = getService('browser'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'monitoring', 'header']); + + describe('telemetry', function () { + before(async () => { + log.debug('monitoring'); + await browser.setWindowSize(1200, 800); + await appsMenu.clickLink('Stack Monitoring'); + }); + + it('should show banner Help us improve Kibana and Elasticsearch', async () => { + const expectedMessage = `Help us improve the Elastic Stack +To learn about how usage data helps us manage and improve our products and services, see our Privacy Statement. To stop collection, disable usage data here. +Dismiss`; + const actualMessage = await PageObjects.monitoring.getWelcome(); + log.debug(`X-Pack message = ${actualMessage}`); + expect(actualMessage).to.be(expectedMessage); + }); + }); +}; diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/index.js new file mode 100644 index 0000000000000..0803f48ed90fe --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/telemetry/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('telemetry feature', function () { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./_telemetry')); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/_winlogbeat.js b/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/_winlogbeat.js new file mode 100644 index 0000000000000..657fdf4daaeb4 --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/_winlogbeat.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService, getPageObjects }) { + const log = getService('log'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const retry = getService('retry'); + const appsMenu = getService('appsMenu'); + + describe('check winlogbeat', function () { + it('winlogbeat- should have hit count GT 0', async function () { + const url = await browser.getCurrentUrl(); + log.debug(url); + if (!url.includes('kibana')) { + await PageObjects.common.navigateToApp('discover', { insertTimestamp: false }); + } else if (!url.includes('discover')) { + await appsMenu.clickLink('Discover'); + } + await PageObjects.discover.selectIndexPattern('winlogbeat-*'); + await PageObjects.timePicker.setCommonlyUsedTime('Today'); + await retry.try(async function () { + const hitCount = parseInt(await PageObjects.discover.getHitCount()); + expect(hitCount).to.be.greaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/index.js b/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/index.js new file mode 100644 index 0000000000000..a940be781ccfe --- /dev/null +++ b/x-pack/test/stack_functional_integration/test/functional/apps/winlogbeat/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ loadTestFile }) { + describe('winlogbeat app', function () { + loadTestFile(require.resolve('./_winlogbeat')); + }); +} diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts index 76cea64bffc1c..d13b9836f25a1 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/status.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { ClusterStateAPIResponse } from '../../../plugins/upgrade_assistant/common/types'; import { getIndexStateFromClusterState } from '../../../plugins/upgrade_assistant/common/get_index_state_from_cluster_state'; // eslint-disable-next-line import/no-default-export @@ -28,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { it('the _cluster/state endpoint is still what we expect', async () => { await esArchiver.load('upgrade_assistant/reindex'); await es.indices.close({ index: '7.0-data' }); - const result = await es.cluster.state({ + const result = await es.cluster.state({ index: '7.0-data', metric: 'metadata', }); diff --git a/yarn.lock b/yarn.lock index ac5f653fdf3d5..2d575634686a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2144,10 +2144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.3.0" -"@elastic/charts@19.8.0": - version "19.8.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.0.tgz#d8439288e2574053ca9e6eee6f3b00bf04917803" - integrity sha512-px0mX0UBtFhbt5O4JAqOZPYC+K9avVmjgKPoIqQBMnnwkKtuKGH1mQ7XZro3E7COJ4WQ5nGxWtC+ewlFQP3zww== +"@elastic/charts@19.8.1": + version "19.8.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.8.1.tgz#27653823911c26e4703c73588367473215beaf0f" + integrity sha512-vONCrcZ8bZ+C16+bKLoLyNrMC/b2UvYNoPbYcnB5XYAg5a68finvXEcWD6Y+qa7GLaO2CMe5J9eSjLWXHHDmLg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -2172,6 +2172,17 @@ redux-immutable-state-invariant "^2.1.0" redux-logger "^3.0.6" +"@elastic/elasticsearch@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.8.0.tgz#3f9ee54fe8ef79874ebd231db03825fa500a7111" + integrity sha512-rUOTNN1At0KoN0Fcjd6+J7efghuURnoMTB/od9EMK6Mcdebi6N3z5ulShTsKRn6OanS9Eq3l/OmheQY1Y+WLcg== + dependencies: + debug "^4.1.1" + decompress-response "^4.2.0" + ms "^2.1.1" + pump "^3.0.0" + secure-json-parse "^2.1.0" + "@elastic/elasticsearch@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.4.0.tgz#57f4066acf25e9d4e9b4f6376088433aae6f25d4" @@ -27784,6 +27795,11 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +secure-json-parse@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.1.0.tgz#ae76f5624256b5c497af887090a5d9e156c9fb20" + integrity sha512-GckO+MS/wT4UogDyoI/H/S1L0MCcKS1XX/vp48wfmU7Nw4woBmb8mIpu4zPBQjKlRT88/bt9xdoV4111jPpNJA== + seedrandom@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"