From 5f4368f778d47a59fcffb74b13cc47d77731151b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 14 Jul 2021 19:59:39 -0400 Subject: [PATCH 01/10] [Fleet] Fix add agent in the package policy table (#104749) (#105691) Co-authored-by: Nicolas Chaulet --- .../components/package_policy_actions_menu.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 9743135d5f1c10..7b0a300ac9dc8b 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -28,6 +28,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; const refreshAgentPolicy = useAgentPolicyRefresh(); + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const onEnrollmentFlyoutClose = useMemo(() => { return () => setIsEnrollmentFlyoutOpen(false); @@ -48,7 +49,10 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ // , setIsEnrollmentFlyoutOpen(true)} + onClick={() => { + setIsActionsMenuOpen(false); + setIsEnrollmentFlyoutOpen(true); + }} key="addAgent" > )} - + setIsActionsMenuOpen(isOpen)} + /> ); }; From 5f5f09741860b584df7ec66f1e585bcd38354a42 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 14 Jul 2021 17:01:39 -0700 Subject: [PATCH 02/10] skip flaky suite (#105087) (cherry picked from commit 961681fc5dc4429c5f1863601e308352fe9ea542) --- .../apps/ml/data_visualizer/index_data_visualizer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 031074876f39c7..2e88198adbc089 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -496,7 +496,8 @@ export default function ({ getService }: FtrProviderContext) { }); } - describe('index based', function () { + // FLAKY: https://github.com/elastic/kibana/issues/105087 + describe.skip('index based', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); From b99f75d669000520239d27a7f8b0618f2536fcc4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 14 Jul 2021 21:10:35 -0400 Subject: [PATCH 03/10] [Fleet] Add containerized fleet server instructions to Fleet README (#105669) (#105697) * Add containerized fleet server instructions to Fleet README * Address PR feedback Co-authored-by: Kyle Pollich --- x-pack/plugins/fleet/README.md | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 1b18b726475378..ce8e95c83d6bdc 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -48,6 +48,48 @@ This plugin follows the `common`, `server`, `public` structure from the [Archite Note: The plugin was previously named Ingest Manager it's possible that some variables are still named with that old plugin name. +### Running Fleet Server Locally in a Container + +It can be useful to run Fleet Server in a container on your local machine in order to free up your actual "bare metal" machine to run Elastic Agent for testing purposes. Otherwise, you'll only be able to a single instance of Elastic Agent dedicated to Fleet Server on your local machine, and this can make testing integrations and policies difficult. + +_The following is adapted from the Fleet Server [README](https://github.com/elastic/fleet-server#running-elastic-agent-with-fleet-server-in-container)_ + +1. Add the following configuration to your `kibana.dev.yml` + +```yml +server.host: 0.0.0.0 +``` + +2. Append the following option to the command you use to start Elasticsearch + +``` +-E http.host=0.0.0.0 +``` + +This command should look something like this: + +``` +yarn es snapshot --license trial -E xpack.security.authc.api_key.enabled=true -E path.data=/tmp/es-data -E http.host=0.0.0.0 +``` + +3. Run the Fleet Server Docker container. Make sure you include a `BASE-PATH` value if your local Kibana instance is using one. `YOUR-IP` should correspond to the IP address used by your Docker network to represent the host. For Windows and Mac machines, this should be `192.168.65.2`. If you're not sure what this IP should be, run the following to look it up: + +``` +docker run -it --rm alpine nslookup host.docker.internal +``` + +To run the Fleet Server Docker container: + +``` +docker run -e KIBANA_HOST=http://{YOUR-IP}:5601/{BASE-PATH} -e KIBANA_USERNAME=elastic -e KIBANA_PASSWORD=changeme -e ELASTICSEARCH_HOST=http://{YOUR-IP}:9200 -e ELASTICSEARCH_USERNAME=elastic -e ELASTICSEARCH_PASSWORD=changeme -e KIBANA_FLEET_SETUP=1 -e FLEET_SERVER_ENABLE=1 -e FLEET_SERVER_INSECURE_HTTP=1 -p 8220:8220 docker.elastic.co/beats/elastic-agent:{VERSION} +``` + +Ensure you provide the `-p 8220:8220` port mapping to map the Fleet Server container's port `8220` to your local machine's port `8220` in order for Fleet to communicate with Fleet Server. + +For the latest version, use `8.0.0-SNAPSHOT`. Otherwise, you can explore the available versions at https://www.docker.elastic.co/r/beats/elastic-agent. + +Once the Fleet Server container is running, you should be able to treat it as if it were a local process running on `http://localhost:8220` when configuring Fleet via the UI. You can then run `elastic-agent` on your local machine directly for testing purposes. + ### Tests #### API integration tests @@ -77,3 +119,4 @@ You need to have `docker` to run ingest manager api integration tests ``` FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE='docker.elastic.co/package-registry/distribution:production' FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner ``` + From 8939c16b9d4e2c47aea3bdc3e8d91da8e3c0e01a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 14 Jul 2021 21:55:32 -0400 Subject: [PATCH 04/10] [Workplace Search] Fix Chrome issues with GitHub sources (#105680) (#105699) * Fix route validation This param is not always required. Was already fixed for org version but the personal dashboard came later and was not fixed. Original fix for org: https://github.com/elastic/kibana/pull/84164/commits/30d8b1dfa861fc5c6ce027b7831362e023767bee#diff-07f094b2a4719e8511f003d8e278a77cd6b808d11b14d1c528705f9b259c328fR373 * Fix route to account for private github route Previously had the org route hard-coded * Move the logic for parsing the query params to template Because the useEffect call comes after the initial render, the chrome flashes. We originally got around this by hiding the chrome always because in non-github scenarios, this worked fine. However, because the oauth plugin sends the state in the quert params and uses the same URL, we need to parse that to determine whether this is an org or accoutn route. We now do that logic in the template and set the chrome before calling the useEffect. We still need to pass both the parsed params and the original quert string because the redirect passes that string to the next view. Co-authored-by: Scotty Bollinger --- .../add_source/add_source_logic.test.ts | 24 +++++++++---------- .../components/add_source/add_source_logic.ts | 20 +++++++++------- .../components/source_added.test.tsx | 3 ++- .../components/source_added.tsx | 10 +++++--- .../server/routes/workplace_search/sources.ts | 2 +- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 09ba41f81d76ac..950412e84f8705 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -289,6 +289,7 @@ describe('AddSourceLogic', () => { describe('saveSourceParams', () => { const params = { code: 'code123', + session_state: 'session_state123', state: '{"action":"create","context":"organization","service_type":"gmail","csrf_token":"token==","index_permissions":false}', }; @@ -306,7 +307,7 @@ describe('AddSourceLogic', () => { const setAddedSourceSpy = jest.spyOn(SourcesLogic.actions, 'setAddedSource'); const { serviceName, indexPermissions, serviceType } = response; http.get.mockReturnValue(Promise.resolve(response)); - AddSourceLogic.actions.saveSourceParams(queryString); + AddSourceLogic.actions.saveSourceParams(queryString, params, true); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/sources/create', { query: { ...params, @@ -324,7 +325,7 @@ describe('AddSourceLogic', () => { const accountQueryString = '?state=%7B%22action%22:%22create%22,%22context%22:%22account%22,%22service_type%22:%22gmail%22,%22csrf_token%22:%22token%3D%3D%22,%22index_permissions%22:false%7D&code=code'; - AddSourceLogic.actions.saveSourceParams(accountQueryString); + AddSourceLogic.actions.saveSourceParams(accountQueryString, params, false); await nextTick(); @@ -345,7 +346,7 @@ describe('AddSourceLogic', () => { preContentSourceId, }) ); - AddSourceLogic.actions.saveSourceParams(queryString); + AddSourceLogic.actions.saveSourceParams(queryString, params, true); expect(http.get).toHaveBeenCalledWith('/api/workplace_search/sources/create', { query: { ...params, @@ -360,28 +361,27 @@ describe('AddSourceLogic', () => { }); describe('Github error edge case', () => { + const GITHUB_ERROR = + 'The redirect_uri MUST match the registered callback URL for this application.'; + const errorParams = { ...params, error_description: GITHUB_ERROR }; const getGithubQueryString = (context: 'organization' | 'account') => `?error=redirect_uri_mismatch&error_description=The+redirect_uri+MUST+match+the+registered+callback+URL+for+this+application.&error_uri=https%3A%2F%2Fdocs.github.com%2Fapps%2Fmanaging-oauth-apps%2Ftroubleshooting-authorization-request-errors%2F%23redirect-uri-mismatch&state=%7B%22action%22%3A%22create%22%2C%22context%22%3A%22${context}%22%2C%22service_type%22%3A%22github%22%2C%22csrf_token%22%3A%22TOKEN%3D%3D%22%2C%22index_permissions%22%3Afalse%7D`; it('handles "organization" redirect and displays error', () => { const githubQueryString = getGithubQueryString('organization'); - AddSourceLogic.actions.saveSourceParams(githubQueryString); + AddSourceLogic.actions.saveSourceParams(githubQueryString, errorParams, true); expect(navigateToUrl).toHaveBeenCalledWith('/'); - expect(setErrorMessage).toHaveBeenCalledWith( - 'The redirect_uri MUST match the registered callback URL for this application.' - ); + expect(setErrorMessage).toHaveBeenCalledWith(GITHUB_ERROR); }); it('handles "account" redirect and displays error', () => { const githubQueryString = getGithubQueryString('account'); - AddSourceLogic.actions.saveSourceParams(githubQueryString); + AddSourceLogic.actions.saveSourceParams(githubQueryString, errorParams, false); expect(navigateToUrl).toHaveBeenCalledWith(PERSONAL_SOURCES_PATH); expect(setErrorMessage).toHaveBeenCalledWith( - PERSONAL_DASHBOARD_SOURCE_ERROR( - 'The redirect_uri MUST match the registered callback URL for this application.' - ) + PERSONAL_DASHBOARD_SOURCE_ERROR(GITHUB_ERROR) ); }); }); @@ -389,7 +389,7 @@ describe('AddSourceLogic', () => { it('handles error', async () => { http.get.mockReturnValue(Promise.reject('this is an error')); - AddSourceLogic.actions.saveSourceParams(queryString); + AddSourceLogic.actions.saveSourceParams(queryString, params, true); await nextTick(); expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 81e27f07293dc2..a75e494aa2b1c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -20,7 +20,6 @@ import { } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { KibanaLogic } from '../../../../../shared/kibana'; -import { parseQueryParams } from '../../../../../shared/query_params'; import { AppLogic } from '../../../../app_logic'; import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; import { @@ -95,7 +94,11 @@ export interface AddSourceActions { isUpdating: boolean, successCallback?: () => void ): { isUpdating: boolean; successCallback?(): void }; - saveSourceParams(search: Search): { search: Search }; + saveSourceParams( + search: Search, + params: OauthParams, + isOrganization: boolean + ): { search: Search; params: OauthParams; isOrganization: boolean }; getSourceConfigData(serviceType: string): { serviceType: string }; getSourceConnectData( serviceType: string, @@ -206,7 +209,11 @@ export const AddSourceLogic = kea ({ search }), + saveSourceParams: (search: Search, params: OauthParams, isOrganization: boolean) => ({ + search, + params, + isOrganization, + }), createContentSource: ( serviceType: string, successCallback: () => void, @@ -500,15 +507,12 @@ export const AddSourceLogic = kea { + saveSourceParams: async ({ search, params, isOrganization }) => { const { http } = HttpLogic.values; const { navigateToUrl } = KibanaLogic.values; const { setAddedSource } = SourcesLogic.actions; - const params = (parseQueryParams(search) as unknown) as OauthParams; const query = { ...params, kibana_host: kibanaHost }; const route = '/api/workplace_search/sources/create'; - const state = JSON.parse(params.state); - const isOrganization = state.context !== 'account'; /** There is an extreme edge case where the user is trying to connect Github as source from ent-search, @@ -539,7 +543,7 @@ export const AddSourceLogic = kea { }); it('renders', () => { - const search = '?name=foo&serviceType=custom&indexPermissions=false'; + const search = + '?code=1234&state=%7B%22action%22%3A%22create%22%2C%22context%22%3A%22account%22%2C%22service_type%22%3A%22github%22%2C%22csrf_token%22%3A%22TOKEN123%3D%3D%22%2C%22index_permissions%22%3Afalse%7D'; (useLocation as jest.Mock).mockImplementationOnce(() => ({ search })); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index 5b93b7a426936e..77b39310504834 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -15,8 +15,9 @@ import { EuiPage, EuiPageBody } from '@elastic/eui'; import { KibanaLogic } from '../../../../shared/kibana'; import { Loading } from '../../../../shared/loading'; +import { parseQueryParams } from '../../../../shared/query_params'; -import { AddSourceLogic } from './add_source/add_source_logic'; +import { AddSourceLogic, OauthParams } from './add_source/add_source_logic'; /** * This component merely triggers catchs the redirect from the oauth application and initializes the saving @@ -25,14 +26,17 @@ import { AddSourceLogic } from './add_source/add_source_logic'; */ export const SourceAdded: React.FC = () => { const { search } = useLocation() as Location; + const params = (parseQueryParams(search) as unknown) as OauthParams; + const state = JSON.parse(params.state); + const isOrganization = state.context !== 'account'; const { setChromeIsVisible } = useValues(KibanaLogic); const { saveSourceParams } = useActions(AddSourceLogic); // We don't want the personal dashboard to flash the Kibana chrome, so we hide it. - setChromeIsVisible(false); + setChromeIsVisible(isOrganization); useEffect(() => { - saveSourceParams(search); + saveSourceParams(search, params, isOrganization); }, []); return ( diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 5de4387f2c0d9c..835ad84ef6853c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -135,7 +135,7 @@ export function registerAccountCreateSourceRoute({ login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), organizations: schema.maybe(schema.arrayOf(schema.string())), - indexPermissions: schema.boolean(), + indexPermissions: schema.maybe(schema.boolean()), }), }, }, From 1ce67c96cefda0b3cd22bc88d477b6fbbbd3eab6 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 14 Jul 2021 19:59:11 -0700 Subject: [PATCH 05/10] [Reporting] Clean up types for internal APIs needed for UI (#105508) (#105702) * [Reporting] Refactor types for API response data * fix api test * Update x-pack/plugins/reporting/common/types.ts Co-authored-by: Jean-Louis Leysens * more verbose comments * set comments in interface type definition * Update x-pack/plugins/reporting/common/types.ts Co-authored-by: Michael Dokolin * more strict * Update x-pack/plugins/reporting/public/management/report_info_button.tsx Co-authored-by: Michael Dokolin * Update x-pack/plugins/reporting/public/management/report_info_button.tsx Co-authored-by: Michael Dokolin * fix suggestion * fix accidental declaration of interface methods as function properties * fix info panel Co-authored-by: Jean-Louis Leysens Co-authored-by: Michael Dokolin Co-authored-by: Jean-Louis Leysens Co-authored-by: Michael Dokolin --- x-pack/plugins/reporting/common/types.ts | 120 +- x-pack/plugins/reporting/public/lib/job.ts | 75 + .../reporting_api_client.ts | 143 +- .../public/lib/stream_handler.test.ts | 53 +- .../reporting/public/lib/stream_handler.ts | 26 +- .../report_info_button.test.tsx.snap | 2 + .../report_listing.test.tsx.snap | 3874 +++++++++++++---- .../management/report_delete_button.tsx | 5 +- .../management/report_download_button.tsx | 3 +- .../public/management/report_error_button.tsx | 4 +- .../public/management/report_info_button.tsx | 203 +- .../public/management/report_listing.test.tsx | 45 +- .../public/management/report_listing.tsx | 67 +- .../reporting/server/lib/enqueue_job.ts | 2 - .../reporting/server/lib/store/report.test.ts | 6 +- .../reporting/server/lib/store/report.ts | 53 +- .../reporting/server/lib/store/store.ts | 1 + .../server/lib/tasks/execute_report.ts | 13 +- .../reporting/server/lib/tasks/index.ts | 1 - .../reporting/server/routes/jobs.test.ts | 16 +- .../plugins/reporting/server/routes/jobs.ts | 21 +- .../server/routes/lib/get_document_payload.ts | 17 +- .../server/routes/lib/job_response_handler.ts | 14 +- .../reporting/server/routes/lib/jobs_query.ts | 107 +- .../job_apis_csv.ts | 97 +- .../job_apis_csv_deprecated.ts | 87 +- 26 files changed, 3641 insertions(+), 1414 deletions(-) create mode 100644 x-pack/plugins/reporting/public/lib/job.ts diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 71bcf143a7f85f..43ae839ad1c2a0 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -55,31 +55,39 @@ export interface TaskRunResult { } export interface ReportSource { - jobtype: string; - kibana_name: string; - kibana_id: string; - created_by: string | false; + /* + * Required fields: populated in enqueue_job when the request comes in to + * generate the report + */ + jobtype: string; // refers to `ExportTypeDefinition.jobType` + created_by: string | false; // username or `false` if security is disabled. Used for ensuring users can only access the reports they've created. payload: { headers: string; // encrypted headers - browserTimezone?: string; // may use timezone from advanced settings - objectType: string; - title: string; - layout?: LayoutParams; - isDeprecated?: boolean; - }; - meta: { objectType: string; layout?: string }; - browser_type: string; - migration_version: string; - max_attempts: number; - timeout: number; - + isDeprecated?: boolean; // set to true when the export type is being phased out + } & BaseParams; + meta: { objectType: string; layout?: string }; // for telemetry + migration_version: string; // for reminding the user to update their POST URL + attempts: number; // initially populated as 0 + created_at: string; // timestamp in UTC status: JobStatus; - attempts: number; + + /* + * `output` is only populated if the report job is completed or failed. + */ output: TaskRunResult | null; - started_at?: string; - completed_at?: string; - created_at: string; - process_expiration?: string | null; // must be set to null to clear the expiration + + /* + * Optional fields: populated when the job is claimed to execute, and after + * execution has finished + */ + kibana_name?: string; // for troubleshooting + kibana_id?: string; // for troubleshooting + browser_type?: string; // no longer used since chromium is the only option (used to allow phantomjs) + timeout?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.queue.timeout + max_attempts?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.capture.maxAttempts + started_at?: string; // timestamp in UTC + completed_at?: string; // timestamp in UTC + process_expiration?: string | null; // timestamp in UTC - is overwritten with `null` when the job needs a retry } /* @@ -99,48 +107,41 @@ export interface BaseParams { } export type JobId = string; + +/* + * JobStatus: + * - Begins as 'pending' + * - Changes to 'processing` when the job is claimed + * - Then 'completed' | 'failed' when execution is done + * If the job needs a retry, it reverts back to 'pending'. + */ export type JobStatus = - | 'completed' - | 'completed_with_warnings' - | 'pending' - | 'processing' - | 'failed'; + | 'completed' // Report was successful + | 'completed_with_warnings' // The download available for troubleshooting - it **should** show a meaningful error + | 'pending' // Report job is waiting to be claimed + | 'processing' // Report job has been claimed and is executing + | 'failed'; // Report was not successful, and all retries are done. Nothing to download. export interface JobContent { content: string; } -export interface ReportApiJSON { +/* + * Info API response: to avoid unnecessary large payloads on a network, the + * report query results do not include `payload.headers` or `output.content`, + * which can be long strings of meaningless text + */ +interface ReportSimple extends Omit { + payload: Omit; + output?: Omit; // is undefined for report jobs that are not completed +} + +/* + * The response format for all of the report job APIs + */ +export interface ReportApiJSON extends ReportSimple { id: string; index: string; - kibana_name: string; - kibana_id: string; - browser_type: string | undefined; - created_at: string; - jobtype: string; - created_by: string | false; - timeout?: number; - output?: { - content_type: string; - size: number; - warnings?: string[]; - }; - process_expiration?: string; - completed_at: string | undefined; - payload: { - layout?: LayoutParams; - title: string; - browserTimezone?: string; - isDeprecated?: boolean; - }; - meta: { - layout?: string; - objectType: string; - }; - max_attempts: number; - started_at: string | undefined; - attempts: number; - status: string; } export interface LicenseCheckResults { @@ -149,13 +150,14 @@ export interface LicenseCheckResults { message: string; } +/* Notifier Toasts */ export interface JobSummary { id: JobId; status: JobStatus; - title: string; - jobtype: string; - maxSizeReached?: boolean; - csvContainsFormulas?: boolean; + jobtype: ReportSource['jobtype']; + title: ReportSource['payload']['title']; + maxSizeReached: TaskRunResult['max_size_reached']; + csvContainsFormulas: TaskRunResult['csv_contains_formulas']; } export interface JobSummarySet { diff --git a/x-pack/plugins/reporting/public/lib/job.ts b/x-pack/plugins/reporting/public/lib/job.ts new file mode 100644 index 00000000000000..c882e8b92986bc --- /dev/null +++ b/x-pack/plugins/reporting/public/lib/job.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JobId, ReportApiJSON, ReportSource, TaskRunResult } from '../../common/types'; + +type ReportPayload = ReportSource['payload']; + +/* + * This class represents a report job for the UI + * It can be instantiated with ReportApiJSON: the response data format for the report job APIs + */ +export class Job { + public id: JobId; + public index: string; + + public objectType: ReportPayload['objectType']; + public title: ReportPayload['title']; + public isDeprecated: ReportPayload['isDeprecated']; + public browserTimezone?: ReportPayload['browserTimezone']; + public layout: ReportPayload['layout']; + + public jobtype: ReportSource['jobtype']; + public created_by: ReportSource['created_by']; + public created_at: ReportSource['created_at']; + public started_at: ReportSource['started_at']; + public completed_at: ReportSource['completed_at']; + public status: ReportSource['status']; + public attempts: ReportSource['attempts']; + public max_attempts: ReportSource['max_attempts']; + + public timeout: ReportSource['timeout']; + public kibana_name: ReportSource['kibana_name']; + public kibana_id: ReportSource['kibana_id']; + public browser_type: ReportSource['browser_type']; + + public size?: TaskRunResult['size']; + public content_type?: TaskRunResult['content_type']; + public csv_contains_formulas?: TaskRunResult['csv_contains_formulas']; + public max_size_reached?: TaskRunResult['max_size_reached']; + public warnings?: TaskRunResult['warnings']; + + constructor(report: ReportApiJSON) { + this.id = report.id; + this.index = report.index; + + this.jobtype = report.jobtype; + this.objectType = report.payload.objectType; + this.title = report.payload.title; + this.layout = report.payload.layout; + this.created_by = report.created_by; + this.created_at = report.created_at; + this.started_at = report.started_at; + this.completed_at = report.completed_at; + this.status = report.status; + this.attempts = report.attempts; + this.max_attempts = report.max_attempts; + + this.timeout = report.timeout; + this.kibana_name = report.kibana_name; + this.kibana_id = report.kibana_id; + this.browser_type = report.browser_type; + this.browserTimezone = report.payload.browserTimezone; + this.size = report.output?.size; + this.content_type = report.output?.content_type; + + this.isDeprecated = report.payload.isDeprecated || false; + this.csv_contains_formulas = report.output?.csv_contains_formulas; + this.max_size_reached = report.output?.max_size_reached; + this.warnings = report.output?.warnings; + } +} diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 64caac0e27bddc..90411884332c83 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -15,37 +15,52 @@ import { API_MIGRATE_ILM_POLICY_URL, REPORTING_MANAGEMENT_HOME, } from '../../../common/constants'; -import { - DownloadReportFn, - JobId, - ManagementLinkFn, - ReportApiJSON, - ReportDocument, - ReportSource, -} from '../../../common/types'; +import { DownloadReportFn, JobId, ManagementLinkFn, ReportApiJSON } from '../../../common/types'; import { add } from '../../notifier/job_completion_notifications'; - -export interface JobQueueEntry { - _id: string; - _source: ReportSource; -} +import { Job } from '../job'; export interface JobContent { content: string; content_type: boolean; } -interface JobParams { - [paramName: string]: any; -} - export interface DiagnoseResponse { help: string[]; success: boolean; logs: string; } -export class ReportingAPIClient { +interface JobParams { + [paramName: string]: any; +} + +interface IReportingAPI { + // Helpers + getReportURL(jobId: string): string; + getReportingJobPath(exportType: string, jobParams: JobParams): string; // Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL + createReportingJob(exportType: string, jobParams: any): Promise; // Sends a request to queue a job, with the job params in the POST body + getServerBasePath(): string; // Provides the raw server basePath to allow it to be stripped out from relativeUrls in job params + + // CRUD + downloadReport(jobId: string): void; + deleteReport(jobId: string): Promise; + list(page: number, jobIds: string[]): Promise; // gets the first 10 report of the page + total(): Promise; + getError(jobId: string): Promise; + getInfo(jobId: string): Promise; + findForJobIds(jobIds: string[]): Promise; + + // Function props + getManagementLink: ManagementLinkFn; + getDownloadLink: DownloadReportFn; + + // Diagnostic-related API calls + verifyConfig(): Promise; + verifyBrowser(): Promise; + verifyScreenCapture(): Promise; +} + +export class ReportingAPIClient implements IReportingAPI { private http: HttpSetup; constructor(http: HttpSetup) { @@ -71,68 +86,69 @@ export class ReportingAPIClient { }); } - public list = (page = 0, jobIds: string[] = []): Promise => { + public async list(page = 0, jobIds: string[] = []) { const query = { page } as any; if (jobIds.length > 0) { // Only getting the first 10, to prevent URL overflows query.ids = jobIds.slice(0, 10).join(','); } - return this.http.get(`${API_LIST_URL}/list`, { + const jobQueueEntries: ReportApiJSON[] = await this.http.get(`${API_LIST_URL}/list`, { query, asSystemRequest: true, }); - }; - public total(): Promise { - return this.http.get(`${API_LIST_URL}/count`, { + return jobQueueEntries.map((report) => new Job(report)); + } + + public async total() { + return await this.http.get(`${API_LIST_URL}/count`, { asSystemRequest: true, }); } - public getContent(jobId: string): Promise { - return this.http.get(`${API_LIST_URL}/output/${jobId}`, { + public async getError(jobId: string) { + return await this.http.get(`${API_LIST_URL}/output/${jobId}`, { asSystemRequest: true, }); } - public getInfo(jobId: string): Promise { - return this.http.get(`${API_LIST_URL}/info/${jobId}`, { + public async getInfo(jobId: string) { + const report: ReportApiJSON = await this.http.get(`${API_LIST_URL}/info/${jobId}`, { asSystemRequest: true, }); + return new Job(report); } - public findForJobIds = (jobIds: JobId[]): Promise => { - return this.http.fetch(`${API_LIST_URL}/list`, { + public async findForJobIds(jobIds: JobId[]) { + const reports: ReportApiJSON[] = await this.http.fetch(`${API_LIST_URL}/list`, { query: { page: 0, ids: jobIds.join(',') }, method: 'GET', }); - }; + return reports.map((report) => new Job(report)); + } - /* - * Return a URL to queue a job, with the job params encoded in the query string of the URL. Used for copying POST URL - */ - public getReportingJobPath = (exportType: string, jobParams: JobParams) => { + public getReportingJobPath(exportType: string, jobParams: JobParams) { const params = stringify({ jobParams: rison.encode(jobParams) }); return `${this.http.basePath.prepend(API_BASE_GENERATE)}/${exportType}?${params}`; - }; + } - /* - * Sends a request to queue a job, with the job params in the POST body - */ - public createReportingJob = async (exportType: string, jobParams: any) => { + public async createReportingJob(exportType: string, jobParams: any) { const jobParamsRison = rison.encode(jobParams); - const resp = await this.http.post(`${API_BASE_GENERATE}/${exportType}`, { - method: 'POST', - body: JSON.stringify({ - jobParams: jobParamsRison, - }), - }); + const resp: { job: ReportApiJSON } = await this.http.post( + `${API_BASE_GENERATE}/${exportType}`, + { + method: 'POST', + body: JSON.stringify({ + jobParams: jobParamsRison, + }), + } + ); add(resp.job.id); - return resp; - }; + return new Job(resp.job); + } public getManagementLink: ManagementLinkFn = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); @@ -140,36 +156,27 @@ export class ReportingAPIClient { public getDownloadLink: DownloadReportFn = (jobId: JobId) => this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`); - /* - * provides the raw server basePath to allow it to be stripped out from relativeUrls in job params - */ public getServerBasePath = () => this.http.basePath.serverBasePath; - /* - * Diagnostic-related API calls - */ - public verifyConfig = (): Promise => - this.http.post(`${API_BASE_URL}/diagnose/config`, { + public async verifyConfig() { + return await this.http.post(`${API_BASE_URL}/diagnose/config`, { asSystemRequest: true, }); + } - /* - * Diagnostic-related API calls - */ - public verifyBrowser = (): Promise => - this.http.post(`${API_BASE_URL}/diagnose/browser`, { + public async verifyBrowser() { + return await this.http.post(`${API_BASE_URL}/diagnose/browser`, { asSystemRequest: true, }); + } - /* - * Diagnostic-related API calls - */ - public verifyScreenCapture = (): Promise => - this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { + public async verifyScreenCapture() { + return await this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { asSystemRequest: true, }); + } - public migrateReportingIndicesIlmPolicy = (): Promise => { - return this.http.put(`${API_MIGRATE_ILM_POLICY_URL}`); - }; + public async migrateReportingIndicesIlmPolicy() { + return await this.http.put(`${API_MIGRATE_ILM_POLICY_URL}`); + } } diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 7c3837486ad1d8..58fde5cbd83adc 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -7,7 +7,9 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary, ReportDocument } from '../../common/types'; +import { coreMock } from '../../../../../src/core/public/mocks'; +import { JobSummary, ReportApiJSON } from '../../common/types'; +import { Job } from './job'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; @@ -18,43 +20,20 @@ Object.defineProperty(window, 'sessionStorage', { writable: true, }); -const mockJobsFound = [ - { - _id: 'job-source-mock1', - _source: { - status: 'completed', - output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { title: 'specimen' }, - }, - }, - { - _id: 'job-source-mock2', - _source: { - status: 'failed', - output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { title: 'specimen' }, - }, - }, - { - _id: 'job-source-mock3', - _source: { - status: 'pending', - output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { title: 'specimen' }, - }, - }, -]; +const mockJobsFound: Job[] = [ + { id: 'job-source-mock1', status: 'completed', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } }, + { id: 'job-source-mock2', status: 'failed', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } }, + { id: 'job-source-mock3', status: 'pending', output: { csv_contains_formulas: false, max_size_reached: false }, payload: { title: 'specimen' } }, +].map((j) => new Job(j as ReportApiJSON)); // prettier-ignore -const jobQueueClientMock: ReportingAPIClient = { - findForJobIds: async (jobIds: string[]) => { - return mockJobsFound as ReportDocument[]; - }, - getContent: (): Promise => { - return Promise.resolve({ content: 'this is the completed report data' }); - }, - getManagementLink: () => '/#management', - getDownloadLink: () => '/reporting/download/job-123', -} as any; +const jobQueueClientMock = new ReportingAPIClient(coreMock.createSetup().http); +jobQueueClientMock.findForJobIds = async (jobIds: string[]) => mockJobsFound; +jobQueueClientMock.getInfo = () => + Promise.resolve(({ content: 'this is the completed report data' } as unknown) as Job); +jobQueueClientMock.getError = () => + Promise.resolve({ content: 'this is the completed report data' }); +jobQueueClientMock.getManagementLink = () => '/#management'; +jobQueueClientMock.getDownloadLink = () => '/reporting/download/job-123'; const mockShowDanger = stub(); const mockShowSuccess = stub(); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 53191cacb5ba1a..8e41d34d054ecb 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -10,7 +10,7 @@ import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants'; -import { JobId, JobSummary, JobSummarySet, ReportDocument } from '../../common/types'; +import { JobId, JobSummary, JobSummarySet } from '../../common/types'; import { getFailureToast, getGeneralErrorToast, @@ -18,20 +18,21 @@ import { getWarningFormulasToast, getWarningMaxSizeToast, } from '../notifier'; +import { Job } from './job'; import { ReportingAPIClient } from './reporting_api_client'; function updateStored(jobIds: JobId[]): void { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds)); } -function getReportStatus(src: ReportDocument): JobSummary { +function getReportStatus(src: Job): JobSummary { return { - id: src._id, - status: src._source.status, - title: src._source.payload.title, - jobtype: src._source.jobtype, - maxSizeReached: src._source.output?.max_size_reached, - csvContainsFormulas: src._source.output?.csv_contains_formulas, + id: src.id, + status: src.status, + title: src.title, + jobtype: src.jobtype, + maxSizeReached: src.max_size_reached, + csvContainsFormulas: src.csv_contains_formulas, }; } @@ -73,7 +74,7 @@ export class ReportingNotifierStreamHandler { // no download link available for (const job of failedJobs) { - const { content } = await this.apiClient.getContent(job.id); + const { content } = await this.apiClient.getError(job.id); this.notifications.toasts.addDanger( getFailureToast(content, job, this.apiClient.getManagementLink) ); @@ -90,17 +91,14 @@ export class ReportingNotifierStreamHandler { */ public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable { return Rx.from(this.apiClient.findForJobIds(storedJobs)).pipe( - map((jobs: ReportDocument[]) => { + map((jobs) => { const completedJobs: JobSummary[] = []; const failedJobs: JobSummary[] = []; const pending: JobId[] = []; // add side effects to storage for (const job of jobs) { - const { - _id: jobId, - _source: { status: jobStatus }, - } = job; + const { id: jobId, status: jobStatus } = job; if (storedJobs.includes(jobId)) { if (jobStatus === JOB_STATUSES.COMPLETED || jobStatus === JOB_STATUSES.WARNINGS) { completedJobs.push(getReportStatus(job)); diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap index cb365849608677..3417aa59f9d725 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap @@ -6,7 +6,9 @@ exports[`ReportInfoButton handles button click flyout on click 1`] = ` className="euiButtonIcon euiButtonIcon--primary euiButtonIcon--empty euiButtonIcon--xSmall" data-test-subj="reportInfoButton" disabled={false} + onBlur={[Function]} onClick={[Function]} + onFocus={[Function]} type="button" > - - - - - + + + + + + + @@ -1404,7 +1654,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - - - - - - - - - - - - - - -
- - -
- + + + + + + + + + +
+
+ + + + +
+ + + + + + +
+ + +
+ - + - - - - + + + + + + +
@@ -3493,7 +4243,7 @@ exports[`ReportListing Report job listing with some items 1`] = `
- + - - - - + + + + + + + @@ -4594,7 +5594,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - + - - - - + + + + + + + @@ -5662,7 +6912,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - + - - - - + + + + + + + @@ -6730,7 +8230,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - + - - - - + + + + + + + @@ -7798,7 +9548,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - + - - - - + + + + + + + @@ -8866,7 +10866,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` - - - - - + + + + + + + diff --git a/x-pack/plugins/reporting/public/management/report_delete_button.tsx b/x-pack/plugins/reporting/public/management/report_delete_button.tsx index dfb411fc195e8a..5200191184972f 100644 --- a/x-pack/plugins/reporting/public/management/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_delete_button.tsx @@ -7,7 +7,8 @@ import { EuiButton, EuiConfirmModal } from '@elastic/eui'; import React, { Fragment, PureComponent } from 'react'; -import { Job, Props as ListingProps } from './report_listing'; +import { Job } from '../lib/job'; +import { Props as ListingProps } from './report_listing'; type DeleteFn = () => Promise; type Props = { jobsToDelete: Job[]; performDelete: DeleteFn } & ListingProps; @@ -46,7 +47,7 @@ export class ReportDeleteButton extends PureComponent { id: 'xpack.reporting.listing.table.deleteConfirmTitle', defaultMessage: `Delete the "{name}" report?`, }, - { name: jobsToDelete[0].object_title } + { name: jobsToDelete[0].title } ); const message = intl.formatMessage({ id: 'xpack.reporting.listing.table.deleteConfirmMessage', diff --git a/x-pack/plugins/reporting/public/management/report_download_button.tsx b/x-pack/plugins/reporting/public/management/report_download_button.tsx index 78022b85e2ff86..b4212710377224 100644 --- a/x-pack/plugins/reporting/public/management/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_download_button.tsx @@ -8,7 +8,8 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; import { JOB_STATUSES } from '../../common/constants'; -import { Job as ListingJob, Props as ListingProps } from './report_listing'; +import { Job as ListingJob } from '../lib/job'; +import { Props as ListingProps } from './report_listing'; type Props = { record: ListingJob } & ListingProps; diff --git a/x-pack/plugins/reporting/public/management/report_error_button.tsx b/x-pack/plugins/reporting/public/management/report_error_button.tsx index 0ebdf5ca60b5a0..ee0c0e162cb7d0 100644 --- a/x-pack/plugins/reporting/public/management/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_error_button.tsx @@ -9,8 +9,8 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { JOB_STATUSES } from '../../common/constants'; +import { Job as ListingJob } from '../lib/job'; import { JobContent, ReportingAPIClient } from '../lib/reporting_api_client'; -import { Job as ListingJob } from './report_listing'; interface Props { intl: InjectedIntl; @@ -102,7 +102,7 @@ class ReportErrorButtonUi extends Component { this.setState({ isLoading: true }); try { - const reportContent: JobContent = await apiClient.getContent(record.id); + const reportContent: JobContent = await apiClient.getError(record.id); if (this.mounted) { this.setState({ isLoading: false, error: reportContent.content }); } diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx index 719f1ff341daf8..92acaa386bd568 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.tsx @@ -15,14 +15,16 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiToolTip, } from '@elastic/eui'; -import { get } from 'lodash'; -import React, { Component, Fragment } from 'react'; +import { injectI18n } from '@kbn/i18n/react'; +import React, { Component } from 'react'; import { USES_HEADLESS_JOB_TYPES } from '../../common/constants'; -import { ReportApiJSON } from '../../common/types'; +import { Job } from '../lib/job'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { Props as ListingProps } from './report_listing'; -interface Props { +interface Props extends Pick { jobId: string; apiClient: ReportingAPIClient; } @@ -31,23 +33,23 @@ interface State { isLoading: boolean; isFlyoutVisible: boolean; calloutTitle: string; - info: ReportApiJSON | null; + info: Job | null; error: Error | null; } const NA = 'n/a'; const UNKNOWN = 'unknown'; -const getDimensions = (info: ReportApiJSON): string => { +const getDimensions = (info: Job): string => { const defaultDimensions = { width: null, height: null }; - const { width, height } = get(info, 'payload.layout.dimensions', defaultDimensions); + const { width, height } = info.layout?.dimensions || defaultDimensions; if (width && height) { return `Width: ${width} x Height: ${height}`; } - return NA; + return UNKNOWN; }; -export class ReportInfoButton extends Component { +class ReportInfoButtonUi extends Component { private mounted?: boolean; constructor(props: Props) { @@ -75,133 +77,60 @@ export class ReportInfoButton extends Component { } const jobType = info.jobtype || NA; - - interface JobInfo { - title: string; - description: string; - } - - interface JobInfoMap { - [thing: string]: JobInfo[]; - } - const attempts = info.attempts ? info.attempts.toString() : NA; const maxAttempts = info.max_attempts ? info.max_attempts.toString() : NA; const timeout = info.timeout ? info.timeout.toString() : NA; - const warnings = info.output && info.output.warnings ? info.output.warnings.join(',') : null; + const warnings = info.warnings?.join(',') ?? null; + + const jobInfo = [ + { title: 'Title', description: info.title || NA }, + { title: 'Created By', description: info.created_by || NA }, + { title: 'Created At', description: info.created_at || NA }, + { title: 'Timezone', description: info.browserTimezone || NA }, + { title: 'Status', description: info.status || NA }, + ]; - const jobInfoDateTimes: JobInfo[] = [ - { - title: 'Created By', - description: info.created_by || NA, - }, - { - title: 'Created At', - description: info.created_at || NA, - }, - { - title: 'Started At', - description: info.started_at || NA, - }, - { - title: 'Completed At', - description: info.completed_at || NA, - }, + const processingInfo = [ + { title: 'Started At', description: info.started_at || NA }, + { title: 'Completed At', description: info.completed_at || NA }, { title: 'Processed By', description: - info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : UNKNOWN, - }, - { - title: 'Browser Timezone', - description: get(info, 'payload.browserTimezone') || NA, - }, - ]; - const jobInfoPayload: JobInfo[] = [ - { - title: 'Title', - description: get(info, 'payload.title') || NA, - }, - { - title: 'Layout', - description: get(info, 'meta.layout') || NA, - }, - { - title: 'Dimensions', - description: getDimensions(info), - }, - { - title: 'Job Type', - description: jobType, - }, - { - title: 'Content Type', - description: get(info, 'output.content_type') || NA, - }, - { - title: 'Size in Bytes', - description: get(info, 'output.size') || NA, - }, - ]; - const jobInfoStatus: JobInfo[] = [ - { - title: 'Attempts', - description: attempts, - }, - { - title: 'Max Attempts', - description: maxAttempts, - }, - { - title: 'Timeout', - description: timeout, - }, - { - title: 'Status', - description: info.status || NA, - }, - { - title: 'Browser Type', - description: USES_HEADLESS_JOB_TYPES.includes(jobType) ? info.browser_type || UNKNOWN : NA, + info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, }, + { title: 'Content Type', description: info.content_type || NA }, + { title: 'Size in Bytes', description: info.size?.toString() || NA }, + { title: 'Attempts', description: attempts }, + { title: 'Max Attempts', description: maxAttempts }, + { title: 'Timeout', description: timeout }, ]; - if (warnings) { - jobInfoStatus.push({ - title: 'Errors', - description: warnings, - }); - } + const jobScreenshot = [ + { title: 'Dimensions', description: getDimensions(info) }, + { title: 'Layout', description: info.layout?.id || UNKNOWN }, + { title: 'Browser Type', description: info.browser_type || NA }, + ]; - const jobInfoParts: JobInfoMap = { - datetimes: jobInfoDateTimes, - payload: jobInfoPayload, - status: jobInfoStatus, - }; + const warningInfo = warnings && [{ title: 'Errors', description: warnings }]; return ( - - + <> + - - - - + + {USES_HEADLESS_JOB_TYPES.includes(jobType) ? ( + <> + + + + ) : null} + {warningInfo ? ( + <> + + + + ) : null} + ); } @@ -240,23 +169,31 @@ export class ReportInfoButton extends Component { } return ( - - + <> + + + {flyout} - + ); } private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info: ReportApiJSON = await this.props.apiClient.getInfo(this.props.jobId); + const info = await this.props.apiClient.getInfo(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, info }); } @@ -287,3 +224,5 @@ export class ReportInfoButton extends Component { } }; } + +export const ReportInfoButton = injectI18n(ReportInfoButtonUi); diff --git a/x-pack/plugins/reporting/public/management/report_listing.test.tsx b/x-pack/plugins/reporting/public/management/report_listing.test.tsx index 0b278cbaa0449f..0c9b85c2f8cbb0 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.test.tsx @@ -5,24 +5,20 @@ * 2.0. */ -import React from 'react'; -import { Observable } from 'rxjs'; +import { registerTestBed } from '@kbn/test/jest'; import { UnwrapPromise } from '@kbn/utility-types'; - +import React from 'react'; import { act } from 'react-dom/test-utils'; - -import { registerTestBed } from '@kbn/test/jest'; - -import type { SharePluginSetup, LocatorPublic } from '../../../../../src/plugins/share/public'; +import { Observable } from 'rxjs'; import type { NotificationsSetup } from '../../../../../src/core/public'; import { httpServiceMock, notificationServiceMock } from '../../../../../src/core/public/mocks'; - +import type { LocatorPublic, SharePluginSetup } from '../../../../../src/plugins/share/public'; import type { ILicense } from '../../../licensing/public'; - -import { IlmPolicyMigrationStatus } from '../../common/types'; - -import { ReportingAPIClient, InternalApiClientClientProvider } from '../lib/reporting_api_client'; +import { IlmPolicyMigrationStatus, ReportApiJSON } from '../../common/types'; import { IlmPolicyStatusContextProvider } from '../lib/ilm_policy_status_context'; +import { Job } from '../lib/job'; +import { InternalApiClientClientProvider, ReportingAPIClient } from '../lib/reporting_api_client'; +import { Props, ReportListing } from './report_listing'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { @@ -30,21 +26,20 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { }; }); -import { ReportListing, Props } from './report_listing'; +const mockJobs: ReportApiJSON[] = [ + { id: 'k90e51pk1ieucbae0c3t8wo2', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000 }, // prettier-ignore + { id: 'k90e51pk1ieucbae0c3t8wo1', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000 }, + { id: 'k90cmthd1gv8cbae0c2le8bo', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000 }, + { id: 'k906958e1d4wcbae0c9hip1a', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ] }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000 }, + { id: 'k9067y2a1d4wcbae0cad38n0', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000 }, + { id: 'k9067s1m1d4wcbae0cdnvcms', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000 }, + { id: 'k9065q3s1d4wcbae0c00fxlh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000 }, + { id: 'k905zdw11d34cbae0c3y6tzh', index: '.reporting-2020.04.12', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad' }, output: { content_type: 'application/pdf', size: 80262 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 720, width: 1080 }, id: 'preserve_layout' }, objectType: 'canvas workpad', title: 'My Canvas Workpad' }, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000 }, + { id: 'k8t4ylcb07mi9d006214ifyg', index: '.reporting-2020.04.05', migration_version: '7.15.0', attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization' }, output: { content_type: 'image/png', size: 123456789 }, payload: { browserTimezone: 'America/Phoenix', layout: { dimensions: { height: 1575, width: 1423 }, id: 'png' }, objectType: 'visualization', title: 'count' }, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000 }, +]; // prettier-ignore const reportingAPIClient = { - list: () => - Promise.resolve([ - { _id: 'k90e51pk1ieucbae0c3t8wo2', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 0, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '1970-01-01T00:00:00.000Z', status: 'pending', timeout: 300000, }, sort: [1586898073064], }, // prettier-ignore - { _id: 'k90e51pk1ieucbae0c3t8wo1', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', created_at: '2020-04-14T21:01:13.064Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T21:01:13.062Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T21:06:14.526Z', started_at: '2020-04-14T21:01:14.526Z', status: 'processing', timeout: 300000, }, sort: [1586898073064], }, - { _id: 'k90cmthd1gv8cbae0c2le8bo', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T20:19:14.748Z', created_at: '2020-04-14T20:19:02.977Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T20:19:02.976Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T20:24:04.073Z', started_at: '2020-04-14T20:19:04.073Z', status: 'completed', timeout: 300000, }, sort: [1586895542977], }, - { _id: 'k906958e1d4wcbae0c9hip1a', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:21:08.223Z', created_at: '2020-04-14T17:20:27.326Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 49468, warnings: [ 'An error occurred when trying to read the page for visualization panel info. You may need to increase \'xpack.reporting.capture.timeouts.waitForElements\'. TimeoutError: waiting for selector "[data-shared-item],[data-shared-items-count]" failed: timeout 30000ms exceeded', ], }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:20:27.326Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e8-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:25:29.444Z', started_at: '2020-04-14T17:20:29.444Z', status: 'completed_with_warnings', timeout: 300000, }, sort: [1586884827326], }, - { _id: 'k9067y2a1d4wcbae0cad38n0', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:53.244Z', created_at: '2020-04-14T17:19:31.379Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:31.378Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:39.883Z', started_at: '2020-04-14T17:19:39.883Z', status: 'completed', timeout: 300000, }, sort: [1586884771379], }, - { _id: 'k9067s1m1d4wcbae0cdnvcms', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:19:36.822Z', created_at: '2020-04-14T17:19:23.578Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:19:23.578Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:24:25.247Z', started_at: '2020-04-14T17:19:25.247Z', status: 'completed', timeout: 300000, }, sort: [1586884763578], }, - { _id: 'k9065q3s1d4wcbae0c00fxlh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:18:03.910Z', created_at: '2020-04-14T17:17:47.752Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:17:47.750Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:22:50.379Z', started_at: '2020-04-14T17:17:50.379Z', status: 'completed', timeout: 300000, }, sort: [1586884667752], }, - { _id: 'k905zdw11d34cbae0c3y6tzh', _index: '.reporting-2020.04.12', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-14T17:13:03.719Z', created_at: '2020-04-14T17:12:51.985Z', created_by: 'elastic', jobtype: 'printable_pdf', kibana_id: '5b2de169-2785-441b-ae8c-186a1936b17d', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'preserve_layout', objectType: 'canvas workpad', }, output: { content_type: 'application/pdf', size: 80262, }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-14T17:12:51.984Z', layout: { dimensions: { height: 720, width: 1080, }, id: 'preserve_layout', }, objectType: 'canvas workpad', relativeUrls: [ '/s/hsyjklk/app/canvas#/export/workpad/pdf/workpad-53d38306-eda4-410f-b5e7-efbeca1a8c63/page/1', ], title: 'My Canvas Workpad', }, priority: 10, process_expiration: '2020-04-14T17:17:52.431Z', started_at: '2020-04-14T17:12:52.431Z', status: 'completed', timeout: 300000, }, sort: [1586884371985], }, - { _id: 'k8t4ylcb07mi9d006214ifyg', _index: '.reporting-2020.04.05', _score: null, _source: { attempts: 1, browser_type: 'chromium', completed_at: '2020-04-09T19:10:10.049Z', created_at: '2020-04-09T19:09:52.139Z', created_by: 'elastic', jobtype: 'PNG', kibana_id: 'f2e59b4e-f79b-4a48-8a7d-6d50a3c1d914', kibana_name: 'spicy.local', max_attempts: 1, meta: { layout: 'png', objectType: 'visualization', }, output: { content_type: 'image/png', }, payload: { basePath: '/kbn', browserTimezone: 'America/Phoenix', forceNow: '2020-04-09T19:09:52.137Z', layout: { dimensions: { height: 1575, width: 1423, }, id: 'png', }, objectType: 'visualization', relativeUrl: "/s/hsyjklk/app/visualize#/edit/94d1fe40-7a94-11ea-b373-0749f92ad295?_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!((enabled:!t,id:'1',params:(),schema:metric,type:count)),params:(addLegend:!f,addTooltip:!t,metric:(colorSchema:'Green%20to%20Red',colorsRange:!((from:0,to:10000)),invertColors:!f,labels:(show:!t),metricColorMode:None,percentageMode:!f,style:(bgColor:!f,bgFill:%23000,fontSize:60,labelColor:!f,subText:''),useRanges:!f),type:metric),title:count,type:metric))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-15y,to:now))&indexPattern=d81752b0-7434-11ea-be36-1f978cda44d4&type=metric", title: 'count', }, priority: 10, process_expiration: '2020-04-09T19:14:54.570Z', started_at: '2020-04-09T19:09:54.570Z', status: 'completed', timeout: 300000, }, sort: [1586459392139], }, - ]), // prettier-ignore + list: () => Promise.resolve(mockJobs.map((j) => new Job(j))), total: () => Promise.resolve(18), migrateReportingIndicesIlmPolicy: jest.fn(), } as any; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index dd41314b4883fa..30c9325a0f34f5 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -9,15 +9,14 @@ import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, + EuiLoadingSpinner, EuiPageHeader, EuiSpacer, EuiText, EuiTextColor, - EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; import moment from 'moment'; import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; @@ -26,37 +25,16 @@ import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { checkLicense } from '../lib/license_check'; -import { - JobQueueEntry, - ReportingAPIClient, - useInternalApiClient, -} from '../lib/reporting_api_client'; import { useIlmPolicyStatus, UseIlmPolicyStatusReturn } from '../lib/ilm_policy_status_context'; -import type { SharePluginSetup } from '../shared_imports'; +import { Job } from '../lib/job'; +import { checkLicense } from '../lib/license_check'; +import { ReportingAPIClient, useInternalApiClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; +import type { SharePluginSetup } from '../shared_imports'; import { ReportDeleteButton, ReportDownloadButton, ReportErrorButton, ReportInfoButton } from './'; -import { ReportDiagnostic } from './report_diagnostic'; -import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; import { IlmPolicyLink } from './ilm_policy_link'; - -export interface Job { - id: string; - type: string; - object_type: string; - object_title: string; - created_by?: string | false; - created_at: string; - started_at?: string; - completed_at?: string; - status: string; - statusLabel: string; - max_size_reached?: boolean; - attempts: number; - max_attempts: number; - csv_contains_formulas: boolean; - warnings?: string[]; -} +import { MigrateIlmPolicyCallOut } from './migrate_ilm_policy_callout'; +import { ReportDiagnostic } from './report_diagnostic'; export interface Props { intl: InjectedIntl; @@ -251,7 +229,7 @@ class ReportListingUi extends Component { id: 'xpack.reporting.listing.table.deleteConfim', defaultMessage: `The {reportTitle} report was deleted`, }, - { reportTitle: record.object_title } + { reportTitle: record.title } ) ); } catch (error) { @@ -293,7 +271,7 @@ class ReportListingUi extends Component { this.setState(() => ({ isLoading: true })); } - let jobs: JobQueueEntry[]; + let jobs: Job[]; let total: number; try { jobs = await this.props.apiClient.list(this.state.page); @@ -325,28 +303,7 @@ class ReportListingUi extends Component { this.setState(() => ({ isLoading: false, total, - jobs: jobs.map( - (job: JobQueueEntry): Job => { - const { _source: source } = job; - return { - id: job._id, - type: source.jobtype, - object_type: source.payload.objectType, - object_title: source.payload.title, - created_by: source.created_by, - created_at: source.created_at, - started_at: source.started_at, - completed_at: source.completed_at, - status: source.status, - statusLabel: jobStatusLabelsMap.get(source.status as JobStatuses) || source.status, - max_size_reached: source.output ? source.output.max_size_reached : false, - attempts: source.attempts, - max_attempts: source.max_attempts, - csv_contains_formulas: get(source, 'output.csv_contains_formulas'), - warnings: source.output ? source.output.warnings : undefined, - }; - } - ), + jobs, })); } }; @@ -369,7 +326,7 @@ class ReportListingUi extends Component { const tableColumns = [ { - field: 'object_title', + field: 'title', name: intl.formatMessage({ id: 'xpack.reporting.listing.tableColumns.reportTitle', defaultMessage: 'Report', @@ -379,7 +336,7 @@ class ReportListingUi extends Component {
{objectTitle}
- {record.object_type} + {record.objectType}
); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 70492b415f961d..ec2e443d86c809 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -47,7 +47,6 @@ export function enqueueJobFactory( reporting.getStore(), ]); - const config = reporting.getConfig(); const job = await createJob!(jobParams, context, request); // 1. Add the report to ReportingStore to show as pending @@ -55,7 +54,6 @@ export function enqueueJobFactory( new Report({ jobtype: exportType.jobType, created_by: user ? user.username : false, - max_attempts: config.get('capture', 'maxAttempts'), // NOTE: since max attempts is stored in the document, changing the capture.maxAttempts setting does not affect existing pending reports payload: job, meta: { objectType: jobParams.objectType, diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index a8d14e12a738be..4bc45fd745a567 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -48,7 +48,7 @@ describe('Class Report', () => { index: '.reporting-test-index-12345', jobtype: 'test-report', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt' }, + payload: { objectType: 'testOt' }, meta: { objectType: 'test' }, status: 'pending', timeout: 30000, @@ -109,7 +109,7 @@ describe('Class Report', () => { jobtype: 'test-report', max_attempts: 50, meta: { objectType: 'stange' }, - payload: { headers: 'payload_test_field', objectType: 'testOt' }, + payload: { objectType: 'testOt' }, started_at: undefined, status: 'pending', timeout: 30000, @@ -117,7 +117,7 @@ describe('Class Report', () => { }); it('throws error if converted to task JSON before being synced with ES storage', () => { - const report = new Report({} as any); + const report = new Report({ jobtype: 'spam', payload: {} } as any); expect(() => report.updateWithEsDoc(report)).toThrowErrorMatchingInlineSnapshot( `"Report object from ES has missing fields!"` ); diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index fa5b91527ccc47..0f970ead7c75cb 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { omit } from 'lodash'; import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; @@ -33,23 +34,25 @@ export class Report implements Partial { public _primary_term?: number; // set by ES public _seq_no?: number; // set by ES - public readonly kibana_name: ReportSource['kibana_name']; - public readonly kibana_id: ReportSource['kibana_id']; public readonly jobtype: ReportSource['jobtype']; public readonly created_at: ReportSource['created_at']; public readonly created_by: ReportSource['created_by']; public readonly payload: ReportSource['payload']; public readonly meta: ReportSource['meta']; - public readonly max_attempts: ReportSource['max_attempts']; - public readonly browser_type?: ReportSource['browser_type']; + public readonly browser_type: ReportSource['browser_type']; public readonly status: ReportSource['status']; public readonly attempts: ReportSource['attempts']; - public readonly output?: ReportSource['output']; - public readonly started_at?: ReportSource['started_at']; - public readonly completed_at?: ReportSource['completed_at']; - public readonly timeout?: ReportSource['timeout']; + + // fields with undefined values exist in report jobs that have not been claimed + public readonly kibana_name: ReportSource['kibana_name']; + public readonly kibana_id: ReportSource['kibana_id']; + public readonly output: ReportSource['output']; + public readonly started_at: ReportSource['started_at']; + public readonly completed_at: ReportSource['completed_at']; + public readonly timeout: ReportSource['timeout']; + public readonly max_attempts: ReportSource['max_attempts']; public process_expiration?: ReportSource['process_expiration']; public migration_version: string; @@ -66,20 +69,29 @@ export class Report implements Partial { this.migration_version = MIGRATION_VERSION; - this.payload = opts.payload!; - this.kibana_name = opts.kibana_name!; - this.kibana_id = opts.kibana_id!; - this.jobtype = opts.jobtype!; - this.max_attempts = opts.max_attempts!; - this.attempts = opts.attempts || 0; + // see enqueue_job for all the fields that are expected to exist when adding a report + if (opts.jobtype == null) { + throw new Error(`jobtype is expected!`); + } + if (opts.payload == null) { + throw new Error(`payload is expected!`); + } - this.process_expiration = opts.process_expiration; + this.payload = opts.payload; + this.kibana_id = opts.kibana_id; + this.kibana_name = opts.kibana_name; + this.jobtype = opts.jobtype; + this.max_attempts = opts.max_attempts; + this.attempts = opts.attempts || 0; this.timeout = opts.timeout; + this.browser_type = opts.browser_type; + this.process_expiration = opts.process_expiration; + this.started_at = opts.started_at; + this.completed_at = opts.completed_at; this.created_at = opts.created_at || moment.utc().toISOString(); this.created_by = opts.created_by || false; this.meta = opts.meta || { objectType: 'unknown' }; - this.browser_type = opts.browser_type; this.status = opts.status || JOB_STATUSES.PENDING; this.output = opts.output || null; @@ -113,9 +125,9 @@ export class Report implements Partial { created_by: this.created_by, payload: this.payload, meta: this.meta, - timeout: this.timeout!, + timeout: this.timeout, max_attempts: this.max_attempts, - browser_type: this.browser_type!, + browser_type: this.browser_type, status: this.status, attempts: this.attempts, started_at: this.started_at, @@ -142,7 +154,6 @@ export class Report implements Partial { payload: this.payload, meta: this.meta, attempts: this.attempts, - max_attempts: this.max_attempts, }; } @@ -158,7 +169,6 @@ export class Report implements Partial { jobtype: this.jobtype, created_at: this.created_at, created_by: this.created_by, - payload: this.payload, meta: this.meta, timeout: this.timeout, max_attempts: this.max_attempts, @@ -167,6 +177,9 @@ export class Report implements Partial { attempts: this.attempts, started_at: this.started_at, completed_at: this.completed_at, + migration_version: this.migration_version, + payload: omit(this.payload, 'headers'), + output: omit(this.output, 'content'), }; } } diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 7a7dd20e1b25cf..40a73f294c5a97 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -29,6 +29,7 @@ export type ReportProcessingFields = Required<{ browser_type: Report['browser_type']; attempts: Report['attempts']; started_at: Report['started_at']; + max_attempts: Report['max_attempts']; timeout: Report['timeout']; process_expiration: Report['process_expiration']; }>; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index f9e2cd82b0805c..a52f452436d6db 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -135,9 +135,8 @@ export class ExecuteReportTask implements ReportingTask { const m = moment(); - // check if job has exceeded maxAttempts (stored in job params) and somehow hasn't been marked as failed yet - // NOTE: the max attempts value comes from the stored document, so changing the capture.maxAttempts config setting does not affect existing pending reports - const maxAttempts = task.max_attempts; + // check if job has exceeded the configured maxAttempts + const maxAttempts = this.config.capture.maxAttempts; if (report.attempts >= maxAttempts) { const err = new Error(`Max attempts reached (${maxAttempts}). Queue timeout reached.`); await this._failJob(report, err); @@ -153,6 +152,7 @@ export class ExecuteReportTask implements ReportingTask { kibana_name: this.kibanaName, browser_type: this.config.capture.browser.type, attempts: report.attempts + 1, + max_attempts: maxAttempts, started_at: startTime, timeout: queueTimeout, process_expiration: expirationTime, @@ -195,7 +195,7 @@ export class ExecuteReportTask implements ReportingTask { const completedTime = moment().toISOString(); const doc: ReportFailedFields = { completed_at: completedTime, - output: docOutput, + output: docOutput ?? null, }; return await store.setReportFailed(report, doc); @@ -306,11 +306,14 @@ export class ExecuteReportTask implements ReportingTask { } if (!report) { + this.reporting.untrackReport(jobId); errorLogger(this.logger, `Job ${jobId} could not be claimed. Exiting...`); return; } - const { jobtype: jobType, attempts, max_attempts: maxAttempts } = report; + const { jobtype: jobType, attempts } = report; + const maxAttempts = this.config.capture.maxAttempts; + this.logger.debug( `Starting ${jobType} report ${jobId}: attempt ${attempts} of ${maxAttempts}.` ); diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index c02b06d97adc7d..662528124e6c02 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -27,7 +27,6 @@ export interface ReportTaskParams { created_at: ReportSource['created_at']; created_by: ReportSource['created_by']; jobtype: ReportSource['jobtype']; - max_attempts: ReportSource['max_attempts']; attempts: ReportSource['attempts']; meta: ReportSource['meta']; } diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 3f913dfd1f32f3..3040ea351f7d04 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -152,7 +152,10 @@ describe('GET /api/reporting/jobs/download', () => { it('returns a 401 if not a valid job type', async () => { mockEsClient.search.mockResolvedValueOnce({ - body: getHits({ jobtype: 'invalidJobType' }), + body: getHits({ + jobtype: 'invalidJobType', + payload: { title: 'invalid!' }, + }), } as any); registerJobInfoRoutes(core); @@ -163,7 +166,11 @@ describe('GET /api/reporting/jobs/download', () => { it('when a job is incomplete', async () => { mockEsClient.search.mockResolvedValueOnce({ - body: getHits({ jobtype: 'unencodedJobType', status: 'pending' }), + body: getHits({ + jobtype: 'unencodedJobType', + status: 'pending', + payload: { title: 'incomplete!' }, + }), } as any); registerJobInfoRoutes(core); @@ -182,6 +189,7 @@ describe('GET /api/reporting/jobs/download', () => { jobtype: 'unencodedJobType', status: 'failed', output: { content: 'job failure message' }, + payload: { title: 'failing job!' }, }), } as any); registerJobInfoRoutes(core); @@ -207,9 +215,7 @@ describe('GET /api/reporting/jobs/download', () => { jobtype: jobType, status: 'completed', output: { content: outputContent, content_type: outputContentType }, - payload: { - title, - }, + payload: { title }, }); }; diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 3f2a95a34224ce..0d0332983d6bc3 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -71,7 +71,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { path: `${MAIN_ENTRY}/count`, validate: false, }, - userHandler(async (user, context, req, res) => { + userHandler(async (user, context, _req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -115,22 +115,20 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { } = await reporting.getLicenseInfo(); const jobsQuery = jobsQueryFactory(reporting); - const result = await jobsQuery.get(user, docId, { includeContent: true }); + const result = await jobsQuery.getContent(user, docId); if (!result) { throw Boom.notFound(); } - const { - _source: { jobtype: jobType, output: jobOutput }, - } = result; + const { jobtype: jobType, output } = result; if (!jobTypes.includes(jobType)) { throw Boom.unauthorized(`Sorry, you are not authorized to download ${jobType} reports`); } return res.ok({ - body: jobOutput || {}, + body: output?.content ?? {}, headers: { 'content-type': 'application/json', }, @@ -166,21 +164,14 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { throw Boom.notFound(); } - const { _source: job } = result; - const { jobtype: jobType, payload: jobPayload } = job; + const { jobtype: jobType } = result; if (!jobTypes.includes(jobType)) { throw Boom.unauthorized(`Sorry, you are not authorized to view ${jobType} info`); } return res.ok({ - body: { - ...job, - payload: { - ...jobPayload, - headers: undefined, - }, - }, + body: result, headers: { 'content-type': 'application/json', }, diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 4e8e888e4e2665..2141252c70bfa0 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -7,12 +7,11 @@ // @ts-ignore import contentDisposition from 'content-disposition'; -import { get } from 'lodash'; import { CSV_JOB_TYPE, CSV_JOB_TYPE_DEPRECATED } from '../../../common/constants'; import { ExportTypesRegistry, statuses } from '../../lib'; -import { ReportDocument } from '../../lib/store'; import { TaskRunResult } from '../../lib/tasks'; import { ExportTypeDefinition } from '../../types'; +import { ReportContent } from './jobs_query'; export interface ErrorFromPayload { message: string; @@ -35,8 +34,8 @@ const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeDefini const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE || exportType.jobType === CSV_JOB_TYPE_DEPRECATED) { - const csvContainsFormulas = get(output, 'csv_contains_formulas', false); - const maxSizedReach = get(output, 'max_size_reached', false); + const csvContainsFormulas = output.csv_contains_formulas ?? false; + const maxSizedReach = output.max_size_reached ?? false; metaDataHeaders['kbn-csv-contains-formulas'] = csvContainsFormulas; metaDataHeaders['kbn-max-size-reached'] = maxSizedReach; @@ -98,10 +97,12 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist }; } - return function getDocumentPayload(doc: ReportDocument): Payload { - const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; - const { output } = doc._source; - + return function getDocumentPayload({ + status, + jobtype: jobType, + payload: { title } = { title: 'unknown' }, + output, + }: ReportContent): Payload { if (output) { if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { return getCompleted(output, jobType, title); diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 8ffefa9c8a98cf..f9519f74060f91 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -35,16 +35,14 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { try { const { docId } = params; - const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); + const doc = await jobsQuery.getContent(user, docId); if (!doc) { return res.notFound(); } - const { jobtype: jobType } = doc._source; - - if (!validJobTypes.includes(jobType)) { + if (!validJobTypes.includes(doc.jobtype)) { return res.unauthorized({ - body: `Sorry, you are not authorized to download ${jobType} reports`, + body: `Sorry, you are not authorized to download ${doc.jobtype} reports`, }); } @@ -81,13 +79,13 @@ export function deleteJobResponseHandlerFactory(reporting: ReportingCore) { params: JobResponseHandlerParams ) { const { docId } = params; - const doc = await jobsQuery.get(user, docId, { includeContent: false }); + const doc = await jobsQuery.get(user, docId); if (!doc) { return res.notFound(); } - const { jobtype: jobType } = doc._source; + const { jobtype: jobType } = doc; if (!validJobTypes.includes(jobType)) { return res.unauthorized({ @@ -96,7 +94,7 @@ export function deleteJobResponseHandlerFactory(reporting: ReportingCore) { } try { - const docIndex = doc._index; + const docIndex = doc.index; await jobsQuery.delete(docIndex, docId); return res.ok({ body: { deleted: true }, diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 2fec34470ff1f1..76896a7472d59d 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -5,20 +5,19 @@ * 2.0. */ +import { ApiResponse } from '@elastic/elasticsearch'; +import { DeleteResponse, SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { i18n } from '@kbn/i18n'; import { UnwrapPromise } from '@kbn/utility-types'; import { ElasticsearchClient } from 'src/core/server'; import { ReportingCore } from '../../'; -import { ReportDocument } from '../../lib/store'; +import { ReportApiJSON, ReportDocument, ReportSource } from '../../../common/types'; +import { Report } from '../../lib/store'; import { ReportingUser } from '../../types'; type SearchRequest = Required>[0]; -interface GetOpts { - includeContent?: boolean; -} - const defaultSize = 10; const getUsername = (user: ReportingUser) => (user ? user.username : false); @@ -33,7 +32,25 @@ function getSearchBody(body: SearchRequest['body']): SearchRequest['body'] { }; } -export function jobsQueryFactory(reportingCore: ReportingCore) { +export type ReportContent = Pick & { + payload?: Pick; +}; + +interface JobsQueryFactory { + list( + jobTypes: string[], + user: ReportingUser, + page: number, + size: number, + jobIds: string[] | null + ): Promise; + count(jobTypes: string[], user: ReportingUser): Promise; + get(user: ReportingUser, id: string): Promise; + getContent(user: ReportingUser, id: string): Promise; + delete(deleteIndex: string, id: string): Promise>; +} + +export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory { function getIndex() { const config = reportingCore.getConfig(); @@ -57,13 +74,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore) { } return { - async list( - jobTypes: string[], - user: ReportingUser, - page = 0, - size = defaultSize, - jobIds: string[] | null - ) { + async list(jobTypes, user, page = 0, size = defaultSize, jobIds) { const username = getUsername(user); const body = getSearchBody({ size, @@ -83,15 +94,23 @@ export function jobsQueryFactory(reportingCore: ReportingCore) { }, }); - const response = await execQuery((elasticsearchClient) => + const response = (await execQuery((elasticsearchClient) => elasticsearchClient.search({ body, index: getIndex() }) + )) as ApiResponse>; + + return ( + response?.body.hits?.hits.map((report: SearchHit) => { + const { _source: reportSource, ...reportHead } = report; + if (reportSource) { + const reportInstance = new Report({ ...reportSource, ...reportHead }); + return reportInstance.toApiJSON(); + } + throw new Error(`Search hit did not include _source!`); + }) ?? [] ); - - // FIXME: return the info in ReportApiJSON format; - return response?.body.hits?.hits ?? []; }, - async count(jobTypes: string[], user: ReportingUser) { + async count(jobTypes, user) { const username = getUsername(user); const body = { query: { @@ -112,14 +131,50 @@ export function jobsQueryFactory(reportingCore: ReportingCore) { return response?.body.count ?? 0; }, - async get(user: ReportingUser, id: string, opts: GetOpts = {}): Promise { + async get(user, id) { + const { logger } = reportingCore.getPluginSetupDeps(); + if (!id) { + logger.warning(`No ID provided for GET`); + return; + } + + const username = getUsername(user); + + const body = getSearchBody({ + query: { + constant_score: { + filter: { + bool: { + must: [{ term: { _id: id } }, { term: { created_by: username } }], + }, + }, + }, + }, + size: 1, + }); + + const response = await execQuery((elasticsearchClient) => + elasticsearchClient.search({ body, index: getIndex() }) + ); + + const result = response?.body.hits.hits[0] as SearchHit | undefined; + if (!result || !result._source) { + logger.warning(`No hits resulted in search`); + return; + } + + const report = new Report({ ...result, ...result._source }); + return report.toApiJSON(); + }, + + async getContent(user, id) { if (!id) { return; } const username = getUsername(user); const body: SearchRequest['body'] = { - ...(opts.includeContent ? { _source: { excludes: [] } } : {}), + _source: { excludes: ['payload.headers'] }, query: { constant_score: { filter: { @@ -140,11 +195,17 @@ export function jobsQueryFactory(reportingCore: ReportingCore) { return; } - // FIXME: return the info in ReportApiJSON format; - return response.body.hits.hits[0] as ReportDocument; + const report = response.body.hits.hits[0] as ReportDocument; + + return { + status: report._source.status, + jobtype: report._source.jobtype, + output: report._source.output, + payload: report._source.payload, + }; }, - async delete(deleteIndex: string, id: string) { + async delete(deleteIndex, id) { try { const { asInternalUser: elasticsearchClient } = await reportingCore.getEsClient(); const query = { id, index: deleteIndex, refresh: true }; diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts index 577afb200d4a1a..06f3756593d767 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv.ts @@ -7,18 +7,13 @@ import expect from '@kbn/expect'; import { pick } from 'lodash'; -import { - ReportApiJSON, - ReportDocument, - ReportSource, -} from '../../../plugins/reporting/common/types'; +import { ReportApiJSON } from '../../../plugins/reporting/common/types'; import { FtrProviderContext } from '../ftr_provider_context'; const apiResponseFields = [ 'attempts', 'created_by', 'jobtype', - 'max_attempts', 'meta', 'payload.isDeprecated', 'payload.title', @@ -26,26 +21,13 @@ const apiResponseFields = [ 'status', ]; -// TODO: clean up the /list and /info endpoints to return ReportApiJSON interface data -const documentResponseFields = [ - '_source.attempts', - '_source.created_by', - '_source.jobtype', - '_source.max_attempts', - '_source.meta', - '_source.payload.isDeprecated', - '_source.payload.title', - '_source.payload.type', - '_source.status', -]; - // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertestNoAuth = getService('supertestWithoutAuth'); const reportingAPI = getService('reportingAPI'); - const postJobCSV = async () => { + const postJobCSV = async (): Promise<{ job: ReportApiJSON; path: string }> => { const jobParams = `(browserTimezone:UTC,columns:!('@timestamp',clientip,extension),` + `objectType:search,searchSource:(fields:!((field:'*',include_unmapped:true)),filter:!((meta:(index:'logstash-*',params:()),` + @@ -81,13 +63,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('Posted CSV job is visible in the job count', async () => { - const { job, path }: { job: ReportApiJSON; path: string } = await postJobCSV(); + const { job, path } = await postJobCSV(); expectSnapshot(pick(job, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv_searchsource", - "max_attempts": 1, "meta": Object { "objectType": "search", }, @@ -110,13 +91,12 @@ export default function ({ getService }: FtrProviderContext) { it('Posted CSV job is visible in the status check', async () => { // post a job - const { job, path }: { job: ReportApiJSON; path: string } = await postJobCSV(); + const { job, path } = await postJobCSV(); expectSnapshot(pick(job, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv_searchsource", - "max_attempts": 1, "meta": Object { "objectType": "search", }, @@ -133,24 +113,21 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx'); // verify the top item in the list - const listingJobs: ReportDocument[] = JSON.parse(listText); - expect(listingJobs[0]._id).to.be(job.id); - expectSnapshot(listingJobs.map((j) => pick(j, documentResponseFields))).toMatchInline(` + const listingJobs: ReportApiJSON[] = JSON.parse(listText); + expect(listingJobs[0].id).to.be(job.id); + expectSnapshot(listingJobs.map((j) => pick(j, apiResponseFields))).toMatchInline(` Array [ Object { - "_source": Object { - "attempts": 0, - "created_by": false, - "jobtype": "csv_searchsource", - "max_attempts": 1, - "meta": Object { - "objectType": "search", - }, - "payload": Object { - "title": "A Saved Search With a DATE FILTER", - }, - "status": "pending", + "attempts": 0, + "created_by": false, + "jobtype": "csv_searchsource", + "meta": Object { + "objectType": "search", + }, + "payload": Object { + "title": "A Saved Search With a DATE FILTER", }, + "status": "pending", }, ] `); @@ -161,13 +138,12 @@ export default function ({ getService }: FtrProviderContext) { it('Posted CSV job is visible in the first page of jobs listing', async () => { // post a job - const { job, path }: { job: ReportApiJSON; path: string } = await postJobCSV(); + const { job, path } = await postJobCSV(); expectSnapshot(pick(job, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv_searchsource", - "max_attempts": 1, "meta": Object { "objectType": "search", }, @@ -179,29 +155,27 @@ export default function ({ getService }: FtrProviderContext) { `); // call the listing api - const { text: listText } = await supertestNoAuth + const { text: listText, status } = await supertestNoAuth .get(`/api/reporting/jobs/list?page=0`) .set('kbn-xsrf', 'xxx'); + expect(status).to.be(200); // verify the top item in the list - const listingJobs: ReportDocument[] = JSON.parse(listText); - expect(listingJobs[0]._id).to.be(job.id); - expectSnapshot(listingJobs.map((j) => pick(j, documentResponseFields))).toMatchInline(` + const listingJobs: ReportApiJSON[] = JSON.parse(listText); + expect(listingJobs[0].id).to.be(job.id); + expectSnapshot(listingJobs.map((j) => pick(j, apiResponseFields))).toMatchInline(` Array [ Object { - "_source": Object { - "attempts": 0, - "created_by": false, - "jobtype": "csv_searchsource", - "max_attempts": 1, - "meta": Object { - "objectType": "search", - }, - "payload": Object { - "title": "A Saved Search With a DATE FILTER", - }, - "status": "pending", + "attempts": 0, + "created_by": false, + "jobtype": "csv_searchsource", + "meta": Object { + "objectType": "search", + }, + "payload": Object { + "title": "A Saved Search With a DATE FILTER", }, + "status": "pending", }, ] `); @@ -212,13 +186,12 @@ export default function ({ getService }: FtrProviderContext) { it('Posted CSV job details are visible in the info API', async () => { // post a job - const { job, path }: { job: ReportApiJSON; path: string } = await postJobCSV(); + const { job, path } = await postJobCSV(); expectSnapshot(pick(job, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv_searchsource", - "max_attempts": 1, "meta": Object { "objectType": "search", }, @@ -229,17 +202,17 @@ export default function ({ getService }: FtrProviderContext) { } `); - const { text: infoText } = await supertestNoAuth + const { text: infoText, status } = await supertestNoAuth .get(`/api/reporting/jobs/info/${job.id}`) .set('kbn-xsrf', 'xxx'); + expect(status).to.be(200); - const info: ReportSource = JSON.parse(infoText); + const info = JSON.parse(infoText); expectSnapshot(pick(info, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv_searchsource", - "max_attempts": 1, "meta": Object { "objectType": "search", }, diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv_deprecated.ts b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv_deprecated.ts index 5aafcfb9d29a1a..2d62725e23989a 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv_deprecated.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/job_apis_csv_deprecated.ts @@ -7,11 +7,7 @@ import expect from '@kbn/expect'; import { pick } from 'lodash'; -import { - ReportApiJSON, - ReportDocument, - ReportSource, -} from '../../../plugins/reporting/common/types'; +import { ReportApiJSON } from '../../../plugins/reporting/common/types'; import { FtrProviderContext } from '../ftr_provider_context'; import { JOB_PARAMS_RISON_CSV_DEPRECATED } from '../services/fixtures'; @@ -19,7 +15,6 @@ const apiResponseFields = [ 'attempts', 'created_by', 'jobtype', - 'max_attempts', 'meta', 'payload.isDeprecated', 'payload.title', @@ -27,18 +22,8 @@ const apiResponseFields = [ 'status', ]; -// TODO: clean up the /list and /info endpoints to return ReportApiJSON interface data -const documentResponseFields = [ - '_source.attempts', - '_source.created_by', - '_source.jobtype', - '_source.max_attempts', - '_source.meta', - '_source.payload.isDeprecated', - '_source.payload.title', - '_source.payload.type', - '_source.status', -]; +const parseApiJSON = (apiResponseText: string): { job: ReportApiJSON; path: string } => + JSON.parse(apiResponseText); // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -68,13 +53,12 @@ export default function ({ getService }: FtrProviderContext) { .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); expect(resStatus).to.be(200); - const { job, path }: { job: ReportApiJSON; path: string } = JSON.parse(resText); + const { job, path } = parseApiJSON(resText); expectSnapshot(pick(job, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv", - "max_attempts": 1, "meta": Object {}, "payload": Object { "isDeprecated": true, @@ -103,30 +87,27 @@ export default function ({ getService }: FtrProviderContext) { .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); expect(resStatus).to.be(200); - const { job, path }: { job: ReportApiJSON; path: string } = JSON.parse(resText); + const { job, path } = parseApiJSON(resText); // call the single job listing api (status check) const { text: listText } = await supertestNoAuth .get(`/api/reporting/jobs/list?page=0&ids=${job.id}`) .set('kbn-xsrf', 'xxx'); - const listingJobs: ReportDocument[] = JSON.parse(listText); - expect(listingJobs[0]._id).to.be(job.id); - expectSnapshot(listingJobs.map((j) => pick(j, documentResponseFields))).toMatchInline(` + const listingJobs: ReportApiJSON[] = JSON.parse(listText); + expect(listingJobs[0].id).to.be(job.id); + expectSnapshot(listingJobs.map((j) => pick(j, apiResponseFields))).toMatchInline(` Array [ Object { - "_source": Object { - "attempts": 0, - "created_by": false, - "jobtype": "csv", - "max_attempts": 1, - "meta": Object {}, - "payload": Object { - "isDeprecated": true, - "title": "A Saved Search With a DATE FILTER", - "type": "search", - }, - "status": "pending", + "attempts": 0, + "created_by": false, + "jobtype": "csv", + "meta": Object {}, + "payload": Object { + "isDeprecated": true, + "title": "A Saved Search With a DATE FILTER", + "type": "search", }, + "status": "pending", }, ] `); @@ -141,30 +122,27 @@ export default function ({ getService }: FtrProviderContext) { .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); expect(resStatus).to.be(200); - const { job, path }: { job: ReportApiJSON; path: string } = JSON.parse(resText); + const { job, path } = parseApiJSON(resText); // call the ALL job listing api const { text: listText } = await supertestNoAuth .get(`/api/reporting/jobs/list?page=0`) .set('kbn-xsrf', 'xxx'); - const listingJobs: ReportDocument[] = JSON.parse(listText); - expect(listingJobs[0]._id).to.eql(job.id); - expectSnapshot(listingJobs.map((j) => pick(j, documentResponseFields))).toMatchInline(` + const listingJobs: ReportApiJSON[] = JSON.parse(listText); + expect(listingJobs[0].id).to.eql(job.id); + expectSnapshot(listingJobs.map((j) => pick(j, apiResponseFields))).toMatchInline(` Array [ Object { - "_source": Object { - "attempts": 0, - "created_by": false, - "jobtype": "csv", - "max_attempts": 1, - "meta": Object {}, - "payload": Object { - "isDeprecated": true, - "title": "A Saved Search With a DATE FILTER", - "type": "search", - }, - "status": "pending", + "attempts": 0, + "created_by": false, + "jobtype": "csv", + "meta": Object {}, + "payload": Object { + "isDeprecated": true, + "title": "A Saved Search With a DATE FILTER", + "type": "search", }, + "status": "pending", }, ] `); @@ -179,18 +157,17 @@ export default function ({ getService }: FtrProviderContext) { .send({ jobParams: JOB_PARAMS_RISON_CSV_DEPRECATED }); expect(resStatus).to.be(200); - const { job, path }: { job: ReportApiJSON; path: string } = JSON.parse(resText); + const { job, path } = parseApiJSON(resText); const { text: infoText } = await supertestNoAuth .get(`/api/reporting/jobs/info/${job.id}`) .set('kbn-xsrf', 'xxx'); - const info: ReportSource = JSON.parse(infoText); + const info = JSON.parse(infoText); expectSnapshot(pick(info, apiResponseFields)).toMatchInline(` Object { "attempts": 0, "created_by": false, "jobtype": "csv", - "max_attempts": 1, "meta": Object {}, "payload": Object { "isDeprecated": true, From 1a82942f712e92255cc237d160ac4c7690c5b50f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 15 Jul 2021 06:33:30 -0400 Subject: [PATCH 06/10] [ML] Fixing job wizard with missing description (#105574) (#105718) Co-authored-by: James Gowdy --- .../jobs/new_job/common/job_creator/job_creator.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 45e7247f0bd856..476ba381dd92e5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -222,12 +222,11 @@ export class JobCreator { } public get description(): string { - return this._job_config.description; + return this._job_config.description ?? ''; } public get groups(): string[] { - // @ts-expect-error @elastic-elasticsearch FIXME groups is optional - return this._job_config.groups; + return this._job_config.groups ?? []; } public set groups(groups: string[]) { From 125eee0e7d49c8f08ab797691d522ab3c02315e1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 15 Jul 2021 12:49:13 +0200 Subject: [PATCH 07/10] [7.x] [ML] API integration tests for APM latency correlation. (#104644) (#105719) Adds API integration tests for APM Latency Correlations code. Writing the tests surfaced some glitches fixed as part of this PR: - If the applied filters don't return any docs, we won't throw an error anymore. Instead, the async search service finishes early and just returns no results. - If for whatever reason the async search service throws an error, it will also set its state now to isRunning = false. - If the client triggers a request with a service ID we now make sure that async search service still exists. We throw an error if that service no longer exists. This avoids re-instantiating async search services when they've already finished or failed and for whatever reason a client triggers another request with the same ID. - Refactored requests to reuse APM's own getCorrelationsFilters(). We now require start/end to be set and it will be converted from ISO (client side) to epochmillis (server side) to be more in line with APM's existing code. - The async search service now creates a simple internal log. This gets exposed via the API and we assert it using the API tests. In the future, we might also expose it in the UI to allow for better problem investigation for users and support. - Use 8.0.0 dataset instead of ml_8.0.0 --- .../correlations/ml_latency_correlations.tsx | 34 ++- .../app/correlations/use_correlations.ts | 1 + .../correlations/async_search_service.ts | 66 ++++- .../get_query_with_params.test.ts | 55 +++- .../correlations/get_query_with_params.ts | 65 +++-- .../correlations/query_correlation.test.ts | 2 +- .../query_field_candidates.test.ts | 11 +- .../query_field_value_pairs.test.ts | 2 +- .../correlations/query_fractions.test.ts | 2 +- .../correlations/query_histogram.test.ts | 11 +- .../query_histogram_interval.test.ts | 11 +- ...ts => query_histogram_range_steps.test.ts} | 22 +- ...teps.ts => query_histogram_range_steps.ts} | 25 +- .../correlations/query_percentiles.test.ts | 16 +- .../correlations/query_percentiles.ts | 21 +- .../correlations/query_ranges.test.ts | 11 +- .../correlations/search_strategy.test.ts | 30 +- .../correlations/search_strategy.ts | 35 ++- .../utils/aggregation_utils.test.ts | 94 +++++++ .../tests/correlations/latency_ml.ts | 266 ++++++++++++++++++ .../test/apm_api_integration/tests/index.ts | 4 + 21 files changed, 675 insertions(+), 109 deletions(-) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{query_histogram_rangesteps.test.ts => query_histogram_range_steps.test.ts} (77%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{query_histogram_rangesteps.ts => query_histogram_range_steps.ts} (83%) create mode 100644 x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts diff --git a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx index 4bd20f51977c6c..03fab3e788639a 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/ml_latency_correlations.tsx @@ -61,17 +61,16 @@ export function MlLatencyCorrelations({ onClose }: Props) { } = useApmPluginContext(); const { serviceName } = useParams<{ serviceName: string }>(); - const { urlParams } = useUrlParams(); - - const fetchOptions = useMemo( - () => ({ - ...{ - serviceName, - ...urlParams, - }, - }), - [serviceName, urlParams] - ); + const { + urlParams: { + environment, + kuery, + transactionName, + transactionType, + start, + end, + }, + } = useUrlParams(); const { error, @@ -85,7 +84,15 @@ export function MlLatencyCorrelations({ onClose }: Props) { } = useCorrelations({ index: 'apm-*', ...{ - ...fetchOptions, + ...{ + environment, + kuery, + serviceName, + transactionName, + transactionType, + start, + end, + }, percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, }, }); @@ -322,8 +329,7 @@ export function MlLatencyCorrelations({ onClose }: Props) { { defaultMessage: 'Latency distribution for {name}', values: { - name: - fetchOptions.transactionName ?? fetchOptions.serviceName, + name: transactionName ?? serviceName, }, } )} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts index 8c874571d23dba..2baeb63fa4a239 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts +++ b/x-pack/plugins/apm/public/components/app/correlations/use_correlations.ts @@ -36,6 +36,7 @@ interface RawResponse { took: number; values: SearchServiceValue[]; overallHistogram: HistogramItem[]; + log: string[]; } export const useCorrelations = (params: CorrelationsOptions) => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index 7a511fc60fd064..155cb1f4615bdc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -11,7 +11,7 @@ import { fetchTransactionDurationFieldCandidates } from './query_field_candidate import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; import { fetchTransactionDurationPercentiles } from './query_percentiles'; import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationHistogramRangesteps } from './query_histogram_rangesteps'; +import { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; import type { AsyncSearchProviderProgress, @@ -24,6 +24,8 @@ import { fetchTransactionDurationFractions } from './query_fractions'; const CORRELATION_THRESHOLD = 0.3; const KS_TEST_THRESHOLD = 0.1; +const currentTimeAsString = () => new Date().toISOString(); + export const asyncSearchServiceProvider = ( esClient: ElasticsearchClient, params: SearchServiceParams @@ -31,6 +33,9 @@ export const asyncSearchServiceProvider = ( let isCancelled = false; let isRunning = true; let error: Error; + const log: string[] = []; + const logMessage = (message: string) => + log.push(`${currentTimeAsString()}: ${message}`); const progress: AsyncSearchProviderProgress = { started: Date.now(), @@ -53,13 +58,17 @@ export const asyncSearchServiceProvider = ( let percentileThresholdValue: number; const cancel = () => { + logMessage(`Service cancelled.`); isCancelled = true; }; const fetchCorrelations = async () => { try { // 95th percentile to be displayed as a marker in the log log chart - const percentileThreshold = await fetchTransactionDurationPercentiles( + const { + totalDocs, + percentiles: percentileThreshold, + } = await fetchTransactionDurationPercentiles( esClient, params, params.percentileThreshold ? [params.percentileThreshold] : undefined @@ -67,12 +76,32 @@ export const asyncSearchServiceProvider = ( percentileThresholdValue = percentileThreshold[`${params.percentileThreshold}.0`]; - const histogramRangeSteps = await fetchTransactionDurationHistogramRangesteps( + logMessage( + `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` + ); + + // finish early if we weren't able to identify the percentileThresholdValue. + if (percentileThresholdValue === undefined) { + logMessage( + `Abort service since percentileThresholdValue could not be determined.` + ); + progress.loadedHistogramStepsize = 1; + progress.loadedOverallHistogram = 1; + progress.loadedFieldCanditates = 1; + progress.loadedFieldValuePairs = 1; + progress.loadedHistograms = 1; + isRunning = false; + return; + } + + const histogramRangeSteps = await fetchTransactionDurationHistogramRangeSteps( esClient, params ); progress.loadedHistogramStepsize = 1; + logMessage(`Loaded histogram range steps.`); + if (isCancelled) { isRunning = false; return; @@ -86,6 +115,8 @@ export const asyncSearchServiceProvider = ( progress.loadedOverallHistogram = 1; overallHistogram = overallLogHistogramChartData; + logMessage(`Loaded overall histogram chart data.`); + if (isCancelled) { isRunning = false; return; @@ -93,13 +124,13 @@ export const asyncSearchServiceProvider = ( // Create an array of ranges [2, 4, 6, ..., 98] const percents = Array.from(range(2, 100, 2)); - const percentilesRecords = await fetchTransactionDurationPercentiles( - esClient, - params, - percents - ); + const { + percentiles: percentilesRecords, + } = await fetchTransactionDurationPercentiles(esClient, params, percents); const percentiles = Object.values(percentilesRecords); + logMessage(`Loaded percentiles.`); + if (isCancelled) { isRunning = false; return; @@ -110,6 +141,8 @@ export const asyncSearchServiceProvider = ( params ); + logMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); + progress.loadedFieldCanditates = 1; const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( @@ -119,6 +152,8 @@ export const asyncSearchServiceProvider = ( progress ); + logMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); + if (isCancelled) { isRunning = false; return; @@ -133,6 +168,8 @@ export const asyncSearchServiceProvider = ( totalDocCount, } = await fetchTransactionDurationFractions(esClient, params, ranges); + logMessage(`Loaded fractions and totalDocCount of ${totalDocCount}.`); + async function* fetchTransactionDurationHistograms() { for (const item of shuffle(fieldValuePairs)) { if (item === undefined || isCancelled) { @@ -185,7 +222,11 @@ export const asyncSearchServiceProvider = ( yield undefined; } } catch (e) { - error = e; + // don't fail the whole process for individual correlation queries, just add the error to the internal log. + logMessage( + `Failed to fetch correlation/kstest for '${item.field}/${item.value}'` + ); + yield undefined; } } } @@ -199,10 +240,14 @@ export const asyncSearchServiceProvider = ( progress.loadedHistograms = loadedHistograms / fieldValuePairs.length; } - isRunning = false; + logMessage( + `Identified ${values.length} significant correlations out of ${fieldValuePairs.length} field/value pairs.` + ); } catch (e) { error = e; } + + isRunning = false; }; fetchCorrelations(); @@ -212,6 +257,7 @@ export const asyncSearchServiceProvider = ( return { error, + log, isRunning, loaded: Math.round(progress.getOverallProgress() * 100), overallHistogram, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts index 12e897ab3eec92..016355b3a64159 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts @@ -10,10 +10,23 @@ import { getQueryWithParams } from './get_query_with_params'; describe('correlations', () => { describe('getQueryWithParams', () => { it('returns the most basic query filtering on processor.event=transaction', () => { - const query = getQueryWithParams({ params: { index: 'apm-*' } }); + const query = getQueryWithParams({ + params: { index: 'apm-*', start: '2020', end: '2021' }, + }); expect(query).toEqual({ bool: { - filter: [{ term: { 'processor.event': 'transaction' } }], + filter: [ + { term: { 'processor.event': 'transaction' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, + ], }, }); }); @@ -24,8 +37,8 @@ describe('correlations', () => { index: 'apm-*', serviceName: 'actualServiceName', transactionName: 'actualTransactionName', - start: '01-01-2021', - end: '31-01-2021', + start: '2020', + end: '2021', environment: 'dev', percentileThresholdValue: 75, }, @@ -33,22 +46,17 @@ describe('correlations', () => { expect(query).toEqual({ bool: { filter: [ - { term: { 'processor.event': 'transaction' } }, - { - term: { - 'service.name': 'actualServiceName', - }, - }, { term: { - 'transaction.name': 'actualTransactionName', + 'processor.event': 'transaction', }, }, { range: { '@timestamp': { - gte: '01-01-2021', - lte: '31-01-2021', + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, }, }, }, @@ -57,6 +65,16 @@ describe('correlations', () => { 'service.environment': 'dev', }, }, + { + term: { + 'service.name': 'actualServiceName', + }, + }, + { + term: { + 'transaction.name': 'actualTransactionName', + }, + }, { range: { 'transaction.duration.us': { @@ -71,7 +89,7 @@ describe('correlations', () => { it('returns a query considering a custom field/value pair', () => { const query = getQueryWithParams({ - params: { index: 'apm-*' }, + params: { index: 'apm-*', start: '2020', end: '2021' }, fieldName: 'actualFieldName', fieldValue: 'actualFieldValue', }); @@ -79,6 +97,15 @@ describe('correlations', () => { bool: { filter: [ { term: { 'processor.event': 'transaction' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, { term: { actualFieldName: 'actualFieldValue', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts index a13b156242f594..e0ddfc1b053b5f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts @@ -5,16 +5,19 @@ * 2.0. */ +import { pipe } from 'fp-ts/lib/pipeable'; +import { getOrElse } from 'fp-ts/lib/Either'; +import { failure } from 'io-ts/lib/PathReporter'; +import * as t from 'io-ts'; + import type { estypes } from '@elastic/elasticsearch'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; import type { SearchServiceParams } from '../../../../common/search_strategies/correlations/types'; -import { environmentQuery as getEnvironmentQuery } from '../../../../common/utils/environment_query'; -import { ProcessorEvent } from '../../../../common/processor_event'; +import { rangeRt } from '../../../routes/default_api_types'; + +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +import { getCorrelationsFilters } from '../../correlations/get_filters'; const getPercentileThresholdValueQuery = ( percentileThresholdValue: number | undefined @@ -39,26 +42,6 @@ export const getTermsQuery = ( return fieldName && fieldValue ? [{ term: { [fieldName]: fieldValue } }] : []; }; -const getRangeQuery = ( - start?: string, - end?: string -): estypes.QueryDslQueryContainer[] => { - if (start === undefined && end === undefined) { - return []; - } - - return [ - { - range: { - '@timestamp': { - ...(start !== undefined ? { gte: start } : {}), - ...(end !== undefined ? { lte: end } : {}), - }, - }, - }, - ]; -}; - interface QueryParams { params: SearchServiceParams; fieldName?: string; @@ -71,21 +54,37 @@ export const getQueryWithParams = ({ }: QueryParams) => { const { environment, + kuery, serviceName, start, end, percentileThresholdValue, + transactionType, transactionName, } = params; + + // converts string based start/end to epochmillis + const setup = pipe( + rangeRt.decode({ start, end }), + getOrElse((errors) => { + throw new Error(failure(errors).join('\n')); + }) + ) as Setup & SetupTimeRange; + + const filters = getCorrelationsFilters({ + setup, + environment, + kuery, + serviceName, + transactionType, + transactionName, + }); + return { bool: { filter: [ - ...getTermsQuery(PROCESSOR_EVENT, ProcessorEvent.transaction), - ...getTermsQuery(SERVICE_NAME, serviceName), - ...getTermsQuery(TRANSACTION_NAME, transactionName), + ...filters, ...getTermsQuery(fieldName, fieldValue), - ...getRangeQuery(start, end), - ...getEnvironmentQuery(environment), ...getPercentileThresholdValueQuery(percentileThresholdValue), ] as estypes.QueryDslQueryContainer[], }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts index 24741ebaa2daef..678328dce1a190 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts @@ -15,7 +15,7 @@ import { BucketCorrelation, } from './query_correlation'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; const expectations = [1, 3, 5]; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; const fractions = [1, 2, 4, 5]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts index 89bdd4280d3249..8929b31b3ecb17 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts @@ -16,7 +16,7 @@ import { shouldBeExcluded, } from './query_field_candidates'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; describe('query_field_candidates', () => { describe('shouldBeExcluded', () => { @@ -61,6 +61,15 @@ describe('query_field_candidates', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts index ea5a1f55bc9246..7ffbc5208e41e7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts @@ -16,7 +16,7 @@ import { getTermsAggRequest, } from './query_field_value_pairs'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; describe('query_field_value_pairs', () => { describe('getTermsAggRequest', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts index 6052841d277c3d..3e7d4a52e4de2e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts @@ -14,7 +14,7 @@ import { getTransactionDurationRangesRequest, } from './query_fractions'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; describe('query_fractions', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts index 2be94463522605..ace91779479601 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts @@ -14,7 +14,7 @@ import { getTransactionDurationHistogramRequest, } from './query_histogram'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; const interval = 100; describe('query_histogram', () => { @@ -40,6 +40,15 @@ describe('query_histogram', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts index 9ed529ccabddbe..ebd78f12485102 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts @@ -14,7 +14,7 @@ import { getHistogramIntervalRequest, } from './query_histogram_interval'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; describe('query_histogram_interval', () => { describe('getHistogramIntervalRequest', () => { @@ -43,6 +43,15 @@ describe('query_histogram_interval', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts similarity index 77% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts index bb366ea29fed48..76aab1cd979c94 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts @@ -10,13 +10,13 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; import { - fetchTransactionDurationHistogramRangesteps, + fetchTransactionDurationHistogramRangeSteps, getHistogramIntervalRequest, -} from './query_histogram_rangesteps'; +} from './query_histogram_range_steps'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; -describe('query_histogram_rangesteps', () => { +describe('query_histogram_range_steps', () => { describe('getHistogramIntervalRequest', () => { it('returns the request body for the histogram interval request', () => { const req = getHistogramIntervalRequest(params); @@ -43,6 +43,15 @@ describe('query_histogram_rangesteps', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, @@ -53,13 +62,14 @@ describe('query_histogram_rangesteps', () => { }); }); - describe('fetchTransactionDurationHistogramRangesteps', () => { + describe('fetchTransactionDurationHistogramRangeSteps', () => { it('fetches the range steps for the log histogram', async () => { const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { body: estypes.SearchResponse; } => { return { body: ({ + hits: { total: { value: 10 } }, aggregations: { transaction_duration_max: { value: 10000, @@ -76,7 +86,7 @@ describe('query_histogram_rangesteps', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; - const resp = await fetchTransactionDurationHistogramRangesteps( + const resp = await fetchTransactionDurationHistogramRangeSteps( esClientMock, params ); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts index e537165ca53f37..6ee5dd6bcdf83b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_rangesteps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts @@ -23,6 +23,14 @@ import type { SearchServiceParams } from '../../../../common/search_strategies/c import { getQueryWithParams } from './get_query_with_params'; +const getHistogramRangeSteps = (min: number, max: number, steps: number) => { + // A d3 based scale function as a helper to get equally distributed bins on a log scale. + const logFn = scaleLog().domain([min, max]).range([1, steps]); + return [...Array(steps).keys()] + .map(logFn.invert) + .map((d) => (isNaN(d) ? 0 : d)); +}; + export const getHistogramIntervalRequest = ( params: SearchServiceParams ): estypes.SearchRequest => ({ @@ -37,19 +45,24 @@ export const getHistogramIntervalRequest = ( }, }); -export const fetchTransactionDurationHistogramRangesteps = async ( +export const fetchTransactionDurationHistogramRangeSteps = async ( esClient: ElasticsearchClient, params: SearchServiceParams ): Promise => { + const steps = 100; + const resp = await esClient.search(getHistogramIntervalRequest(params)); + if ((resp.body.hits.total as estypes.SearchTotalHits).value === 0) { + return getHistogramRangeSteps(0, 1, 100); + } + if (resp.body.aggregations === undefined) { throw new Error( - 'fetchTransactionDurationHistogramInterval failed, did not return aggregations.' + 'fetchTransactionDurationHistogramRangeSteps failed, did not return aggregations.' ); } - const steps = 100; const min = (resp.body.aggregations .transaction_duration_min as estypes.AggregationsValueAggregate).value; const max = @@ -57,9 +70,5 @@ export const fetchTransactionDurationHistogramRangesteps = async ( .transaction_duration_max as estypes.AggregationsValueAggregate).value * 2; - // A d3 based scale function as a helper to get equally distributed bins on a log scale. - const logFn = scaleLog().domain([min, max]).range([1, steps]); - return [...Array(steps).keys()] - .map(logFn.invert) - .map((d) => (isNaN(d) ? 0 : d)); + return getHistogramRangeSteps(min, max, steps); }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts index 0c319aee0fb2b7..f0d01a4849f9fd 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts @@ -14,7 +14,7 @@ import { getTransactionDurationPercentilesRequest, } from './query_percentiles'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; describe('query_percentiles', () => { describe('getTransactionDurationPercentilesRequest', () => { @@ -41,10 +41,20 @@ describe('query_percentiles', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, size: 0, + track_total_hits: true, }, index: params.index, }); @@ -53,6 +63,7 @@ describe('query_percentiles', () => { describe('fetchTransactionDurationPercentiles', () => { it('fetches the percentiles', async () => { + const totalDocs = 10; const percentilesValues = { '1.0': 5.0, '5.0': 25.0, @@ -68,6 +79,7 @@ describe('query_percentiles', () => { } => { return { body: ({ + hits: { total: { value: totalDocs } }, aggregations: { transaction_duration_percentiles: { values: percentilesValues, @@ -86,7 +98,7 @@ describe('query_percentiles', () => { params ); - expect(resp).toEqual(percentilesValues); + expect(resp).toEqual({ percentiles: percentilesValues, totalDocs }); expect(esClientSearchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts index 18dcefb59a11a5..c80f5d836c0ef1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts @@ -38,6 +38,7 @@ export const getTransactionDurationPercentilesRequest = ( return { index: params.index, body: { + track_total_hits: true, query, size: 0, aggs: { @@ -61,7 +62,7 @@ export const fetchTransactionDurationPercentiles = async ( percents?: number[], fieldName?: string, fieldValue?: string -): Promise> => { +): Promise<{ totalDocs: number; percentiles: Record }> => { const resp = await esClient.search( getTransactionDurationPercentilesRequest( params, @@ -71,14 +72,22 @@ export const fetchTransactionDurationPercentiles = async ( ) ); + // return early with no results if the search didn't return any documents + if ((resp.body.hits.total as estypes.SearchTotalHits).value === 0) { + return { totalDocs: 0, percentiles: {} }; + } + if (resp.body.aggregations === undefined) { throw new Error( 'fetchTransactionDurationPercentiles failed, did not return aggregations.' ); } - return ( - (resp.body.aggregations - .transaction_duration_percentiles as estypes.AggregationsTDigestPercentilesAggregate) - .values ?? {} - ); + + return { + totalDocs: (resp.body.hits.total as estypes.SearchTotalHits).value, + percentiles: + (resp.body.aggregations + .transaction_duration_percentiles as estypes.AggregationsTDigestPercentilesAggregate) + .values ?? {}, + }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts index 9451928e47ded3..7d18efc360563f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts @@ -14,7 +14,7 @@ import { getTransactionDurationRangesRequest, } from './query_ranges'; -const params = { index: 'apm-*' }; +const params = { index: 'apm-*', start: '2020', end: '2021' }; const rangeSteps = [1, 3, 5]; describe('query_ranges', () => { @@ -59,6 +59,15 @@ describe('query_ranges', () => { 'processor.event': 'transaction', }, }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts index 6d4bfcdde99943..09775cb2eb0347 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.test.ts @@ -122,6 +122,8 @@ describe('APM Correlations search strategy', () => { } as unknown) as SearchStrategyDependencies; params = { index: 'apm-*', + start: '2020', + end: '2021', }; }); @@ -154,10 +156,22 @@ describe('APM Correlations search strategy', () => { }, query: { bool: { - filter: [{ term: { 'processor.event': 'transaction' } }], + filter: [ + { term: { 'processor.event': 'transaction' } }, + { + range: { + '@timestamp': { + format: 'epoch_millis', + gte: 1577836800000, + lte: 1609459200000, + }, + }, + }, + ], }, }, size: 0, + track_total_hits: true, }) ); }); @@ -167,11 +181,17 @@ describe('APM Correlations search strategy', () => { it('retrieves the current request', async () => { const searchStrategy = await apmCorrelationsSearchStrategyProvider(); const response = await searchStrategy - .search({ id: 'my-search-id', params }, {}, mockDeps) + .search({ params }, {}, mockDeps) .toPromise(); - expect(response).toEqual( - expect.objectContaining({ id: 'my-search-id' }) + const searchStrategyId = response.id; + + const response2 = await searchStrategy + .search({ id: searchStrategyId, params }, {}, mockDeps) + .toPromise(); + + expect(response2).toEqual( + expect.objectContaining({ id: searchStrategyId }) ); }); }); @@ -226,7 +246,7 @@ describe('APM Correlations search strategy', () => { expect(response2.id).toEqual(response1.id); expect(response2).toEqual( - expect.objectContaining({ loaded: 10, isRunning: false }) + expect.objectContaining({ loaded: 100, isRunning: false }) ); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts index d6b4e0e7094b35..8f2e6913c0d062 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/search_strategy.ts @@ -41,14 +41,40 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< throw new Error('Invalid request parameters.'); } - const id = request.id ?? uuid(); + // The function to fetch the current state of the async search service. + // This will be either an existing service for a follow up fetch or a new one for new requests. + let getAsyncSearchServiceState: ReturnType< + typeof asyncSearchServiceProvider + >; + + // If the request includes an ID, we require that the async search service already exists + // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. + // This also avoids instantiating async search services when the service gets called with random IDs. + if (typeof request.id === 'string') { + const existingGetAsyncSearchServiceState = asyncSearchServiceMap.get( + request.id + ); - const getAsyncSearchServiceState = - asyncSearchServiceMap.get(id) ?? - asyncSearchServiceProvider(deps.esClient.asCurrentUser, request.params); + if (typeof existingGetAsyncSearchServiceState === 'undefined') { + throw new Error( + `AsyncSearchService with ID '${request.id}' does not exist.` + ); + } + + getAsyncSearchServiceState = existingGetAsyncSearchServiceState; + } else { + getAsyncSearchServiceState = asyncSearchServiceProvider( + deps.esClient.asCurrentUser, + request.params + ); + } + + // Reuse the request's id or create a new one. + const id = request.id ?? uuid(); const { error, + log, isRunning, loaded, started, @@ -76,6 +102,7 @@ export const apmCorrelationsSearchStrategyProvider = (): ISearchStrategy< isRunning, isPartial: isRunning, rawResponse: { + log, took, values, percentileThresholdValue, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts index 63de0a59d4894a..4313ad58ecbc09 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts @@ -14,6 +14,7 @@ describe('aggregation utils', () => { expect(expectations).toEqual([0, 0.5, 1]); expect(ranges).toEqual([{ to: 0 }, { from: 0, to: 1 }, { from: 1 }]); }); + it('returns expectations and ranges based on given percentiles #2', async () => { const { expectations, ranges } = computeExpectationsAndRanges([1, 3, 5]); expect(expectations).toEqual([1, 2, 4, 5]); @@ -24,6 +25,7 @@ describe('aggregation utils', () => { { from: 5 }, ]); }); + it('returns expectations and ranges with adjusted fractions', async () => { const { expectations, ranges } = computeExpectationsAndRanges([ 1, @@ -45,5 +47,97 @@ describe('aggregation utils', () => { { from: 5 }, ]); }); + + // TODO identify these results derived from the array of percentiles are usable with the ES correlation aggregation + it('returns expectation and ranges adjusted when percentiles have equal values', async () => { + const { expectations, ranges } = computeExpectationsAndRanges([ + 5000, + 5000, + 3090428, + 3090428, + 3090428, + 3618812, + 3618812, + 3618812, + 3618812, + 3696636, + 3696636, + 3696636, + 3696636, + 3696636, + 3696636, + ]); + expect(expectations).toEqual([ + 5000, + 1856256.7999999998, + 3392361.714285714, + 3665506.4, + 3696636, + ]); + expect(ranges).toEqual([ + { + to: 5000, + }, + { + from: 5000, + to: 5000, + }, + { + from: 5000, + to: 3090428, + }, + { + from: 3090428, + to: 3090428, + }, + { + from: 3090428, + to: 3090428, + }, + { + from: 3090428, + to: 3618812, + }, + { + from: 3618812, + to: 3618812, + }, + { + from: 3618812, + to: 3618812, + }, + { + from: 3618812, + to: 3618812, + }, + { + from: 3618812, + to: 3696636, + }, + { + from: 3696636, + to: 3696636, + }, + { + from: 3696636, + to: 3696636, + }, + { + from: 3696636, + to: 3696636, + }, + { + from: 3696636, + to: 3696636, + }, + { + from: 3696636, + to: 3696636, + }, + { + from: 3696636, + }, + ]); + }); }); }); diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts new file mode 100644 index 00000000000000..38e2ab54498373 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/correlations/latency_ml.ts @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import request from 'superagent'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +import { PartialSearchRequest } from '../../../../plugins/apm/server/lib/search_strategies/correlations/search_strategy'; + +function parseBfetchResponse(resp: request.Response): Array> { + return resp.text + .trim() + .split('\n') + .map((item) => JSON.parse(item)); +} + +export default function ApiTest({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const supertest = getService('supertest'); + + const getRequestBody = () => { + const partialSearchRequest: PartialSearchRequest = { + params: { + index: 'apm-*', + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + percentileThreshold: 95, + }, + }; + + return { + batch: [ + { + request: partialSearchRequest, + options: { strategy: 'apmCorrelationsSearchStrategy' }, + }, + ], + }; + }; + + registry.when( + 'correlations latency_ml overall without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const intialResponse = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(getRequestBody()); + + expect(intialResponse.status).to.eql( + 200, + `Expected status to be '200', got '${intialResponse.status}'` + ); + expect(intialResponse.body).to.eql( + {}, + `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( + intialResponse.body + )}'` + ); + + const body = parseBfetchResponse(intialResponse)[0]; + + expect(typeof body.result).to.be('object'); + const { result } = body; + + expect(typeof result?.id).to.be('string'); + + // pass on id for follow up queries + const searchStrategyId = result.id; + + // follow up request body including search strategy ID + const reqBody = getRequestBody(); + reqBody.batch[0].request.id = searchStrategyId; + + let followUpResponse: Record = {}; + + // continues querying until the search strategy finishes + await retry.waitForWithTimeout( + 'search strategy eventually completes and returns full results', + 5000, + async () => { + const response = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(reqBody); + + followUpResponse = parseBfetchResponse(response)[0]; + + return ( + followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined + ); + } + ); + + expect(followUpResponse?.error).to.eql( + undefined, + `search strategy should not return an error, got: ${JSON.stringify( + followUpResponse?.error + )}` + ); + + const followUpResult = followUpResponse.result; + expect(followUpResult?.isRunning).to.eql(false, 'search strategy should not be running'); + expect(followUpResult?.isPartial).to.eql( + false, + 'search strategy result should not be partial' + ); + expect(followUpResult?.id).to.eql( + searchStrategyId, + 'search strategy id should match original id' + ); + expect(followUpResult?.isRestored).to.eql( + true, + 'search strategy response should be restored' + ); + expect(followUpResult?.loaded).to.eql(100, 'loaded state should be 100'); + expect(followUpResult?.total).to.eql(100, 'total state should be 100'); + + expect(typeof followUpResult?.rawResponse).to.be('object'); + + const { rawResponse: finalRawResponse } = followUpResult; + + expect(typeof finalRawResponse?.took).to.be('number'); + expect(finalRawResponse?.percentileThresholdValue).to.be(undefined); + expect(finalRawResponse?.overallHistogram).to.be(undefined); + expect(finalRawResponse?.values.length).to.be(0); + expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ + 'Fetched 95th percentile value of undefined based on 0 documents.', + 'Abort service since percentileThresholdValue could not be determined.', + ]); + }); + } + ); + + registry.when( + 'Correlations latency_ml with data and opbeans-node args', + { config: 'trial', archives: ['8.0.0'] }, + () => { + // putting this into a single `it` because the responses depend on each other + it('queries the search strategy and returns results', async () => { + const intialResponse = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(getRequestBody()); + + expect(intialResponse.status).to.eql( + 200, + `Expected status to be '200', got '${intialResponse.status}'` + ); + expect(intialResponse.body).to.eql( + {}, + `Expected response body to be an empty object, actual response is in the text attribute. Got: '${JSON.stringify( + intialResponse.body + )}'` + ); + + const body = parseBfetchResponse(intialResponse)[0]; + + expect(typeof body?.result).to.be('object'); + const { result } = body; + + expect(typeof result?.id).to.be('string'); + + // pass on id for follow up queries + const searchStrategyId = result.id; + + expect(result?.loaded).to.be(0); + expect(result?.total).to.be(100); + expect(result?.isRunning).to.be(true); + expect(result?.isPartial).to.be(true); + expect(result?.isRestored).to.eql( + false, + `Expected response result to be not restored. Got: '${result?.isRestored}'` + ); + expect(typeof result?.rawResponse).to.be('object'); + + const { rawResponse } = result; + + expect(typeof rawResponse?.took).to.be('number'); + expect(rawResponse?.values).to.eql([]); + + // follow up request body including search strategy ID + const reqBody = getRequestBody(); + reqBody.batch[0].request.id = searchStrategyId; + + let followUpResponse: Record = {}; + + // continues querying until the search strategy finishes + await retry.waitForWithTimeout( + 'search strategy eventually completes and returns full results', + 5000, + async () => { + const response = await supertest + .post(`/internal/bsearch`) + .set('kbn-xsrf', 'foo') + .send(reqBody); + followUpResponse = parseBfetchResponse(response)[0]; + + return ( + followUpResponse?.result?.isRunning === false || followUpResponse?.error !== undefined + ); + } + ); + + expect(followUpResponse?.error).to.eql( + undefined, + `Finished search strategy should not return an error, got: ${JSON.stringify( + followUpResponse?.error + )}` + ); + + const followUpResult = followUpResponse.result; + expect(followUpResult?.isRunning).to.eql( + false, + `Expected finished result not to be running. Got: ${followUpResult?.isRunning}` + ); + expect(followUpResult?.isPartial).to.eql( + false, + `Expected finished result not to be partial. Got: ${followUpResult?.isPartial}` + ); + expect(followUpResult?.id).to.be(searchStrategyId); + expect(followUpResult?.isRestored).to.be(true); + expect(followUpResult?.loaded).to.be(100); + expect(followUpResult?.total).to.be(100); + + expect(typeof followUpResult?.rawResponse).to.be('object'); + + const { rawResponse: finalRawResponse } = followUpResult; + + expect(typeof finalRawResponse?.took).to.be('number'); + expect(finalRawResponse?.percentileThresholdValue).to.be(1309695.875); + expect(finalRawResponse?.overallHistogram.length).to.be(101); + + expect(finalRawResponse?.values.length).to.eql( + 13, + `Expected 13 identified correlations, got ${finalRawResponse?.values.length}.` + ); + expect(finalRawResponse?.log.map((d: string) => d.split(': ')[1])).to.eql([ + 'Fetched 95th percentile value of 1309695.875 based on 1244 documents.', + 'Loaded histogram range steps.', + 'Loaded overall histogram chart data.', + 'Loaded percentiles.', + 'Identified 69 fieldCandidates.', + 'Identified 379 fieldValuePairs.', + 'Loaded fractions and totalDocCount of 1244.', + 'Identified 13 significant correlations out of 379 field/value pairs.', + ]); + + const correlation = finalRawResponse?.values[0]; + expect(typeof correlation).to.be('object'); + expect(correlation?.field).to.be('transaction.result'); + expect(correlation?.value).to.be('success'); + expect(correlation?.correlation).to.be(0.6275246559191225); + expect(correlation?.ksTest).to.be(4.806503252860024e-13); + expect(correlation?.histogram.length).to.be(101); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 813e0e4f3cdb89..a00fa1723fa3ec 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -32,6 +32,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./correlations/latency_slow_transactions')); }); + describe('correlations/latency_ml', function () { + loadTestFile(require.resolve('./correlations/latency_ml')); + }); + describe('correlations/latency_overall', function () { loadTestFile(require.resolve('./correlations/latency_overall')); }); From d54d0b58524b8247adbab0882cd8c3a42b64d752 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Thu, 15 Jul 2021 14:41:16 +0300 Subject: [PATCH 08/10] [Canvas] Expression error (#103048) (#105724) * Basic setup of error plugin. * Removed not used `function` files at `error` expression. * Moved related components from canvas. * Changed imports of components. * Fixed renderer and storybook. * Fixed types errors. * Added limits. * Removed useless translations and fixed .i18nrc.json * added `done` handler call. * Added more fixes fo i18nc. * Added docs. * More fixes of i18nrc. * Fixed async functions. Written current code, based on https://github.com/storybookjs/storybook/issues/7745 * Fixed one test with Expression input. After changing the way of rendering in stories, all elements are mounting and componentDidMount is involved. The previous snapshot was without mounted `monaco` editor. * Fixed storybook error. * More fixes. * removed unused translations. * Removed handlers and changed the way of handling `resize` and calling `done`. * Fixed i18n error. * Fixed storybook. * Replaced Popover with EuiPopover. * Moved `Popover` back to `canvas` * Removed `Popover` export from presentation_utils components. * Moved error_component and debug_component from presentation_util to expression_error. * Fixed translations and imports. * Moved `debug renderer` to `expression_error` plugin. * Fixed error. * Fixed lazy exports. * Fixed imports * Fixed storybook snapshot. * Removed `.i18nrc.json`. * Fixed color of `error`. * Exported concrete elements from popover. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # packages/kbn-optimizer/limits.yml # src/plugins/expression_error/public/components/debug/debug.tsx --- .i18nrc.json | 1 + docs/developer/plugin-list.asciidoc | 4 ++ packages/kbn-optimizer/limits.yml | 1 + src/dev/storybook/aliases.ts | 1 + .../expression_error/.storybook/main.js | 10 +++ src/plugins/expression_error/README.md | 9 +++ .../expression_error/common/constants.ts | 11 +++ src/plugins/expression_error/common/index.ts | 9 +++ .../common/types/expression_renderers.ts | 18 +++++ .../expression_error/common/types/index.ts | 9 +++ src/plugins/expression_error/jest.config.js | 13 ++++ src/plugins/expression_error/kibana.json | 10 +++ .../__snapshots__/debug.stories.storyshot | 0 .../debug/__stories__/debug.stories.tsx | 5 +- .../components/debug/__stories__/helpers.tsx | 5 +- .../public/components/debug/debug.scss | 0 .../public/components/debug/debug.tsx | 13 ++-- .../public/components/debug/index.tsx | 10 +++ .../public/components/debug_component.tsx | 52 ++++++++++++++ .../public/components/error/error.tsx | 16 ++--- .../public/components/error/index.ts | 10 +++ .../components/error/show_debugging.tsx | 12 ++-- .../public/components/error_component.tsx | 67 +++++++++++++++++++ .../public/components/index.ts | 14 ++++ .../__snapshots__/error.stories.storyshot | 0 .../__stories__/error_renderer.stories.tsx | 11 +-- .../expression_renderers/debug_renderer.tsx | 42 ++++++++++++ .../expression_renderers/error_renderer.tsx | 51 ++++++++++++++ .../public/expression_renderers/index.ts | 14 ++++ src/plugins/expression_error/public/index.ts | 18 +++++ src/plugins/expression_error/public/plugin.ts | 34 ++++++++++ src/plugins/expression_error/tsconfig.json | 21 ++++++ .../canvas_plugin_src/renderers/core.ts | 4 -- .../canvas_plugin_src/renderers/debug.tsx | 38 ----------- .../renderers/error/error.scss | 16 ----- .../renderers/error/index.tsx | 58 ---------------- .../canvas_plugin_src/renderers/external.ts | 3 +- x-pack/plugins/canvas/i18n/renderers.ts | 10 --- x-pack/plugins/canvas/kibana.json | 1 + .../arg_add_popover/arg_add_popover.tsx | 6 +- .../color_picker_popover.tsx | 3 +- .../datasource_component.stories.tsx | 2 + .../datasource/datasource_component.js | 3 + .../datasource_preview/datasource_preview.js | 5 +- .../canvas/public/components/debug/index.tsx | 8 --- .../canvas/public/components/error/index.ts | 8 --- .../expression_input.stories.storyshot | 13 +++- .../home/__snapshots__/home.stories.storyshot | 19 +++++- .../public/components/popover/popover.tsx | 1 - .../edit_menu/edit_menu.component.tsx | 3 +- .../element_menu/element_menu.component.tsx | 3 +- .../share_menu/share_menu.component.tsx | 3 +- .../view_menu/view_menu.component.tsx | 3 +- x-pack/plugins/canvas/public/style/index.scss | 1 - .../shareable_runtime/supported_renderers.js | 6 +- .../canvas/storybook/storyshots.test.tsx | 21 +++++- x-pack/plugins/canvas/tsconfig.json | 1 + .../translations/translations/ja-JP.json | 13 ++-- .../translations/translations/zh-CN.json | 13 ++-- 59 files changed, 538 insertions(+), 218 deletions(-) create mode 100644 src/plugins/expression_error/.storybook/main.js create mode 100755 src/plugins/expression_error/README.md create mode 100644 src/plugins/expression_error/common/constants.ts create mode 100755 src/plugins/expression_error/common/index.ts create mode 100644 src/plugins/expression_error/common/types/expression_renderers.ts create mode 100644 src/plugins/expression_error/common/types/index.ts create mode 100644 src/plugins/expression_error/jest.config.js create mode 100755 src/plugins/expression_error/kibana.json rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot (100%) rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/debug/__stories__/debug.stories.tsx (72%) rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/debug/__stories__/helpers.tsx (99%) rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/debug/debug.scss (100%) rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/debug/debug.tsx (66%) create mode 100644 src/plugins/expression_error/public/components/debug/index.tsx create mode 100644 src/plugins/expression_error/public/components/debug_component.tsx rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/error/error.tsx (76%) create mode 100644 src/plugins/expression_error/public/components/error/index.ts rename {x-pack/plugins/canvas => src/plugins/expression_error}/public/components/error/show_debugging.tsx (77%) create mode 100644 src/plugins/expression_error/public/components/error_component.tsx create mode 100644 src/plugins/expression_error/public/components/index.ts rename {x-pack/plugins/canvas/canvas_plugin_src/renderers/error => src/plugins/expression_error/public/expression_renderers}/__stories__/__snapshots__/error.stories.storyshot (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx => src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx (51%) create mode 100644 src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx create mode 100644 src/plugins/expression_error/public/expression_renderers/error_renderer.tsx create mode 100644 src/plugins/expression_error/public/expression_renderers/index.ts create mode 100755 src/plugins/expression_error/public/index.ts create mode 100755 src/plugins/expression_error/public/plugin.ts create mode 100644 src/plugins/expression_error/tsconfig.json delete mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx delete mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/error/error.scss delete mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/debug/index.tsx delete mode 100644 x-pack/plugins/canvas/public/components/error/index.ts diff --git a/.i18nrc.json b/.i18nrc.json index 390e5e917d08e7..732644b43e1f7c 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -16,6 +16,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", + "expressionError": "src/plugins/expression_error", "expressionRevealImage": "src/plugins/expression_reveal_image", "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 63c55d59441154..0223b89a41ba72 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -70,6 +70,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/expression_error/README.md[expressionError] +|Expression Error plugin adds an error renderer to the expression plugin. The renderer will display the error image. + + |{kib-repo}blob/{branch}/src/plugins/expression_reveal_image/README.md[expressionRevealImage] |Expression Reveal Image plugin adds a revealImage function to the expression plugin and an associated renderer. The renderer will display the given percentage of a given image. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 83a834e40fc9a3..24b8b9853fff9f 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -112,3 +112,4 @@ pageLoadAssetSize: visTypePie: 35583 expressionRevealImage: 25675 cases: 144442 + expressionError: 22127 diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 6fc0841551fad8..15497258d45747 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -17,6 +17,7 @@ export const storybookAliases = { dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', data_enhanced: 'x-pack/plugins/data_enhanced/.storybook', embeddable: 'src/plugins/embeddable/.storybook', + expression_error: 'src/plugins/expression_error/.storybook', expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook', infra: 'x-pack/plugins/infra/.storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', diff --git a/src/plugins/expression_error/.storybook/main.js b/src/plugins/expression_error/.storybook/main.js new file mode 100644 index 00000000000000..742239e638b8ac --- /dev/null +++ b/src/plugins/expression_error/.storybook/main.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line import/no-commonjs +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/src/plugins/expression_error/README.md b/src/plugins/expression_error/README.md new file mode 100755 index 00000000000000..5e22d8fc652c79 --- /dev/null +++ b/src/plugins/expression_error/README.md @@ -0,0 +1,9 @@ +# expressionRevealImage + +Expression Error plugin adds an `error` renderer to the expression plugin. The renderer will display the error image. + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/expression_error/common/constants.ts b/src/plugins/expression_error/common/constants.ts new file mode 100644 index 00000000000000..3a522d200090d5 --- /dev/null +++ b/src/plugins/expression_error/common/constants.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const PLUGIN_ID = 'expressionError'; +export const PLUGIN_NAME = 'expressionError'; + +export const JSON = 'JSON'; diff --git a/src/plugins/expression_error/common/index.ts b/src/plugins/expression_error/common/index.ts new file mode 100755 index 00000000000000..d8989abcc3d6f1 --- /dev/null +++ b/src/plugins/expression_error/common/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './constants'; diff --git a/src/plugins/expression_error/common/types/expression_renderers.ts b/src/plugins/expression_error/common/types/expression_renderers.ts new file mode 100644 index 00000000000000..25a9d5edac4adb --- /dev/null +++ b/src/plugins/expression_error/common/types/expression_renderers.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type OriginString = 'bottom' | 'left' | 'top' | 'right'; + +export interface ErrorRendererConfig { + error: Error; +} + +export interface NodeDimensions { + width: number; + height: number; +} diff --git a/src/plugins/expression_error/common/types/index.ts b/src/plugins/expression_error/common/types/index.ts new file mode 100644 index 00000000000000..22961a0dc2fe04 --- /dev/null +++ b/src/plugins/expression_error/common/types/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './expression_renderers'; diff --git a/src/plugins/expression_error/jest.config.js b/src/plugins/expression_error/jest.config.js new file mode 100644 index 00000000000000..64f3e9292ff073 --- /dev/null +++ b/src/plugins/expression_error/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/expression_error'], +}; diff --git a/src/plugins/expression_error/kibana.json b/src/plugins/expression_error/kibana.json new file mode 100755 index 00000000000000..9d8dd566d5b3a1 --- /dev/null +++ b/src/plugins/expression_error/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "expressionError", + "version": "1.0.0", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["expressions", "presentationUtil"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/x-pack/plugins/canvas/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot b/src/plugins/expression_error/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot rename to src/plugins/expression_error/public/components/debug/__stories__/__snapshots__/debug.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/debug/__stories__/debug.stories.tsx b/src/plugins/expression_error/public/components/debug/__stories__/debug.stories.tsx similarity index 72% rename from x-pack/plugins/canvas/public/components/debug/__stories__/debug.stories.tsx rename to src/plugins/expression_error/public/components/debug/__stories__/debug.stories.tsx index f29ab4b7bfda40..7dce5e8f1862b3 100644 --- a/x-pack/plugins/canvas/public/components/debug/__stories__/debug.stories.tsx +++ b/src/plugins/expression_error/public/components/debug/__stories__/debug.stories.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; diff --git a/x-pack/plugins/canvas/public/components/debug/__stories__/helpers.tsx b/src/plugins/expression_error/public/components/debug/__stories__/helpers.tsx similarity index 99% rename from x-pack/plugins/canvas/public/components/debug/__stories__/helpers.tsx rename to src/plugins/expression_error/public/components/debug/__stories__/helpers.tsx index 38ff5977254f9d..666731e199b4d8 100644 --- a/x-pack/plugins/canvas/public/components/debug/__stories__/helpers.tsx +++ b/src/plugins/expression_error/public/components/debug/__stories__/helpers.tsx @@ -1,8 +1,9 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ export const largePayload = { diff --git a/x-pack/plugins/canvas/public/components/debug/debug.scss b/src/plugins/expression_error/public/components/debug/debug.scss similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/debug.scss rename to src/plugins/expression_error/public/components/debug/debug.scss diff --git a/x-pack/plugins/canvas/public/components/debug/debug.tsx b/src/plugins/expression_error/public/components/debug/debug.tsx similarity index 66% rename from x-pack/plugins/canvas/public/components/debug/debug.tsx rename to src/plugins/expression_error/public/components/debug/debug.tsx index 243325228f80a3..925616d5550b11 100644 --- a/x-pack/plugins/canvas/public/components/debug/debug.tsx +++ b/src/plugins/expression_error/public/components/debug/debug.tsx @@ -1,13 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; -import PropTypes from 'prop-types'; import { EuiCode } from '@elastic/eui'; +import './debug.scss'; const LimitRows = (key: string, value: any) => { if (key === 'rows') { @@ -16,14 +17,10 @@ const LimitRows = (key: string, value: any) => { return value; }; -export const Debug = ({ payload }: any) => ( +export const Debug = ({ payload }: { payload: unknown }) => (
       {JSON.stringify(payload, LimitRows, 2)}
     
); - -Debug.propTypes = { - payload: PropTypes.object.isRequired, -}; diff --git a/src/plugins/expression_error/public/components/debug/index.tsx b/src/plugins/expression_error/public/components/debug/index.tsx new file mode 100644 index 00000000000000..1984eecfe4e39f --- /dev/null +++ b/src/plugins/expression_error/public/components/debug/index.tsx @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line import/no-default-export +export { Debug as default } from './debug'; diff --git a/src/plugins/expression_error/public/components/debug_component.tsx b/src/plugins/expression_error/public/components/debug_component.tsx new file mode 100644 index 00000000000000..6cb927380d6699 --- /dev/null +++ b/src/plugins/expression_error/public/components/debug_component.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { useResizeObserver } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from '../../../expressions'; +import { withSuspense } from '../../../presentation_util/public'; +import { NodeDimensions } from '../../common/types'; +import { LazyDebugComponent } from '.'; + +const Debug = withSuspense(LazyDebugComponent); + +interface DebugComponentProps { + onLoaded: IInterpreterRenderHandlers['done']; + parentNode: HTMLElement; + payload: any; +} + +function DebugComponent({ onLoaded, parentNode, payload }: DebugComponentProps) { + const parentNodeDimensions = useResizeObserver(parentNode); + const [dimensions, setDimensions] = useState({ + width: parentNode.offsetWidth, + height: parentNode.offsetHeight, + }); + + const updateDebugView = useCallback(() => { + setDimensions({ + width: parentNode.offsetWidth, + height: parentNode.offsetHeight, + }); + onLoaded(); + }, [parentNode, onLoaded]); + + useEffect(() => { + updateDebugView(); + }, [parentNodeDimensions, updateDebugView]); + + return ( +
+ +
+ ); +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { DebugComponent as default }; diff --git a/x-pack/plugins/canvas/public/components/error/error.tsx b/src/plugins/expression_error/public/components/error/error.tsx similarity index 76% rename from x-pack/plugins/canvas/public/components/error/error.tsx rename to src/plugins/expression_error/public/components/error/error.tsx index cb2c2cd5d58c18..99318357d86024 100644 --- a/x-pack/plugins/canvas/public/components/error/error.tsx +++ b/src/plugins/expression_error/public/components/error/error.tsx @@ -1,28 +1,28 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { FC } from 'react'; -import PropTypes from 'prop-types'; import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; - import { ShowDebugging } from './show_debugging'; const strings = { getDescription: () => - i18n.translate('xpack.canvas.errorComponent.description', { + i18n.translate('expressionError.errorComponent.description', { defaultMessage: 'Expression failed with the message:', }), getTitle: () => - i18n.translate('xpack.canvas.errorComponent.title', { + i18n.translate('expressionError.errorComponent.title', { defaultMessage: 'Whoops! Expression failed', }), }; + export interface Props { payload: { error: Error; @@ -46,7 +46,3 @@ export const Error: FC = ({ payload }) => { ); }; - -Error.propTypes = { - payload: PropTypes.object.isRequired, -}; diff --git a/src/plugins/expression_error/public/components/error/index.ts b/src/plugins/expression_error/public/components/error/index.ts new file mode 100644 index 00000000000000..4edaa4260d1ce7 --- /dev/null +++ b/src/plugins/expression_error/public/components/error/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// eslint-disable-next-line import/no-default-export +export { Error as default } from './error'; diff --git a/x-pack/plugins/canvas/public/components/error/show_debugging.tsx b/src/plugins/expression_error/public/components/error/show_debugging.tsx similarity index 77% rename from x-pack/plugins/canvas/public/components/error/show_debugging.tsx rename to src/plugins/expression_error/public/components/error/show_debugging.tsx index 844bd9fdbff6e9..5ce3f79a6139b6 100644 --- a/x-pack/plugins/canvas/public/components/error/show_debugging.tsx +++ b/src/plugins/expression_error/public/components/error/show_debugging.tsx @@ -1,14 +1,14 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { FC, useState } from 'react'; -import PropTypes from 'prop-types'; import { EuiButtonEmpty } from '@elastic/eui'; -import { Debug } from '../debug'; +import Debug from '../debug'; import { Props } from './error'; export const ShowDebugging: FC = ({ payload }) => { @@ -30,7 +30,3 @@ export const ShowDebugging: FC = ({ payload }) => { ); }; - -ShowDebugging.propTypes = { - payload: PropTypes.object.isRequired, -}; diff --git a/src/plugins/expression_error/public/components/error_component.tsx b/src/plugins/expression_error/public/components/error_component.tsx new file mode 100644 index 00000000000000..58161d8a068a22 --- /dev/null +++ b/src/plugins/expression_error/public/components/error_component.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { EuiIcon, useResizeObserver, EuiPopover } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from '../../../expressions'; +import { withSuspense } from '../../../presentation_util/public'; +import { ErrorRendererConfig } from '../../common/types'; +import { LazyErrorComponent } from '.'; + +const Error = withSuspense(LazyErrorComponent); + +interface ErrorComponentProps extends ErrorRendererConfig { + onLoaded: IInterpreterRenderHandlers['done']; + parentNode: HTMLElement; +} + +function ErrorComponent({ onLoaded, parentNode, error }: ErrorComponentProps) { + const getButtonSize = (node: HTMLElement) => Math.min(node.clientHeight, node.clientWidth); + const parentNodeDimensions = useResizeObserver(parentNode); + + const [buttonSize, setButtonSize] = useState(getButtonSize(parentNode)); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const handlePopoverClick = () => setPopoverOpen(!isPopoverOpen); + const closePopover = () => setPopoverOpen(false); + + const updateErrorView = useCallback(() => { + setButtonSize(getButtonSize(parentNode)); + onLoaded(); + }, [parentNode, onLoaded]); + + useEffect(() => { + updateErrorView(); + }, [parentNodeDimensions, updateErrorView]); + + return ( +
+ + } + isOpen={isPopoverOpen} + > + + +
+ ); +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { ErrorComponent as default }; diff --git a/src/plugins/expression_error/public/components/index.ts b/src/plugins/expression_error/public/components/index.ts new file mode 100644 index 00000000000000..23eb02fa063a7c --- /dev/null +++ b/src/plugins/expression_error/public/components/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { lazy } from 'react'; + +export const LazyErrorComponent = lazy(() => import('./error')); +export const LazyDebugComponent = lazy(() => import('./debug')); + +export const LazyErrorRenderComponent = lazy(() => import('./error_component')); +export const LazyDebugRenderComponent = lazy(() => import('./debug_component')); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot b/src/plugins/expression_error/public/expression_renderers/__stories__/__snapshots__/error.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot rename to src/plugins/expression_error/public/expression_renderers/__stories__/__snapshots__/error.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx similarity index 51% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx rename to src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx index 9598fa00e4e358..9081a8512c11ad 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx +++ b/src/plugins/expression_error/public/expression_renderers/__stories__/error_renderer.stories.tsx @@ -1,19 +1,20 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { error } from '../'; -import { Render } from '../../__stories__/render'; +import { errorRenderer } from '../error_renderer'; +import { Render } from '../../../../presentation_util/public/__stories__'; storiesOf('renderers/error', module).add('default', () => { const thrownError = new Error('There was an error'); const config = { error: thrownError, }; - return ; + return ; }); diff --git a/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx new file mode 100644 index 00000000000000..e3cf86b67148f1 --- /dev/null +++ b/src/plugins/expression_error/public/expression_renderers/debug_renderer.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { render, unmountComponentAtNode } from 'react-dom'; +import React from 'react'; +import { ExpressionRenderDefinition } from 'src/plugins/expressions/common'; +import { i18n } from '@kbn/i18n'; +import { withSuspense } from '../../../../../src/plugins/presentation_util/public'; +import { LazyDebugRenderComponent } from '../components'; +import { JSON } from '../../common'; + +const Debug = withSuspense(LazyDebugRenderComponent); + +const strings = { + getDisplayName: () => + i18n.translate('expressionError.renderer.debug.displayName', { + defaultMessage: 'Debug', + }), + getHelpDescription: () => + i18n.translate('expressionError.renderer.debug.helpDescription', { + defaultMessage: 'Render debug output as formatted {JSON}', + values: { + JSON, + }, + }), +}; + +export const debugRenderer = (): ExpressionRenderDefinition => ({ + name: 'debug', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render(domNode, config, handlers) { + handlers.onDestroy(() => unmountComponentAtNode(domNode)); + render(, domNode); + }, +}); diff --git a/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx b/src/plugins/expression_error/public/expression_renderers/error_renderer.tsx new file mode 100644 index 00000000000000..8ce4d5fdbbbca9 --- /dev/null +++ b/src/plugins/expression_error/public/expression_renderers/error_renderer.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { withSuspense } from '../../../presentation_util/public'; +import { ErrorRendererConfig } from '../../common/types'; +import { LazyErrorRenderComponent } from '../components'; + +const errorStrings = { + getDisplayName: () => + i18n.translate('expressionError.renderer.error.displayName', { + defaultMessage: 'Error information', + }), + getHelpDescription: () => + i18n.translate('expressionError.renderer.error.helpDescription', { + defaultMessage: 'Render error data in a way that is helpful to users', + }), +}; + +const ErrorComponent = withSuspense(LazyErrorRenderComponent); + +export const errorRenderer = (): ExpressionRenderDefinition => ({ + name: 'error', + displayName: errorStrings.getDisplayName(), + help: errorStrings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + config: ErrorRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/expression_error/public/expression_renderers/index.ts b/src/plugins/expression_error/public/expression_renderers/index.ts new file mode 100644 index 00000000000000..237ee5644cdc05 --- /dev/null +++ b/src/plugins/expression_error/public/expression_renderers/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errorRenderer } from './error_renderer'; +import { debugRenderer } from './debug_renderer'; + +export const renderers = [errorRenderer, debugRenderer]; + +export { errorRenderer, debugRenderer }; diff --git a/src/plugins/expression_error/public/index.ts b/src/plugins/expression_error/public/index.ts new file mode 100755 index 00000000000000..04c29a96b853a9 --- /dev/null +++ b/src/plugins/expression_error/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionErrorPlugin } from './plugin'; + +export type { ExpressionErrorPluginSetup, ExpressionErrorPluginStart } from './plugin'; + +export function plugin() { + return new ExpressionErrorPlugin(); +} + +export * from './expression_renderers'; +export { LazyDebugComponent, LazyErrorComponent } from './components'; diff --git a/src/plugins/expression_error/public/plugin.ts b/src/plugins/expression_error/public/plugin.ts new file mode 100755 index 00000000000000..3727cab5436c9f --- /dev/null +++ b/src/plugins/expression_error/public/plugin.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public'; +import { errorRenderer, debugRenderer } from './expression_renderers'; + +interface SetupDeps { + expressions: ExpressionsSetup; +} + +interface StartDeps { + expression: ExpressionsStart; +} + +export type ExpressionErrorPluginSetup = void; +export type ExpressionErrorPluginStart = void; + +export class ExpressionErrorPlugin + implements Plugin { + public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionErrorPluginSetup { + expressions.registerRenderer(errorRenderer); + expressions.registerRenderer(debugRenderer); + } + + public start(core: CoreStart): ExpressionErrorPluginStart {} + + public stop() {} +} diff --git a/src/plugins/expression_error/tsconfig.json b/src/plugins/expression_error/tsconfig.json new file mode 100644 index 00000000000000..aa4562ec735765 --- /dev/null +++ b/src/plugins/expression_error/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "isolatedModules": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../presentation_util/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.ts index 3295332bb63165..906f1646757a7f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/core.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { debug } from './debug'; -import { error } from './error'; import { image } from './image'; import { markdown } from './markdown'; import { metric } from './metric'; @@ -19,8 +17,6 @@ import { table } from './table'; import { text } from './text'; export const renderFunctions = [ - debug, - error, image, markdown, metric, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx deleted file mode 100644 index 5870338ff68949..00000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import ReactDOM from 'react-dom'; -import React from 'react'; -import { Debug } from '../../public/components/debug'; -import { RendererStrings } from '../../i18n'; -import { RendererFactory } from '../../types'; - -const { debug: strings } = RendererStrings; - -export const debug: RendererFactory = () => ({ - name: 'debug', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render(domNode, config, handlers) { - const renderDebug = () => ( -
- -
- ); - - ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - - if (handlers.onResize) { - handlers.onResize(() => { - ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - }); - } - - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/error.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/error.scss deleted file mode 100644 index 9229c1f88a096a..00000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/error.scss +++ /dev/null @@ -1,16 +0,0 @@ -.canvasRenderError { - display: flex; - justify-content: center; - align-items: center; - - .canvasRenderError__icon { - opacity: .4; - stroke: $euiColorEmptyShade; - stroke-width: .2px; - - &:hover { - opacity: .6; - cursor: pointer; - } - } -} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx deleted file mode 100644 index dcf83c68f0c75a..00000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import ReactDOM from 'react-dom'; -import React, { MouseEventHandler } from 'react'; -import { EuiIcon } from '@elastic/eui'; -import { Error } from '../../../public/components/error'; -import { Popover } from '../../../public/components/popover'; -import { RendererStrings } from '../../../i18n'; -import { RendererFactory } from '../../../types'; - -export interface Config { - error: Error; -} - -const { error: strings } = RendererStrings; - -export const error: RendererFactory = () => ({ - name: 'error', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render(domNode, config, handlers) { - const draw = () => { - const buttonSize = Math.min(domNode.clientHeight, domNode.clientWidth); - const button = (handleClick: MouseEventHandler) => ( - - ); - - ReactDOM.render( -
- {() => } -
, - - domNode, - () => handlers.done() - ); - }; - - draw(); - - handlers.onResize(draw); - - handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - }, -}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts index bf9b6a744e686e..1d032aa829bc07 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts @@ -6,6 +6,7 @@ */ import { revealImageRenderer } from '../../../../../src/plugins/expression_reveal_image/public'; +import { errorRenderer, debugRenderer } from '../../../../../src/plugins/expression_error/public'; -export const renderFunctions = [revealImageRenderer]; +export const renderFunctions = [revealImageRenderer, errorRenderer, debugRenderer]; export const renderFunctionFactories = []; diff --git a/x-pack/plugins/canvas/i18n/renderers.ts b/x-pack/plugins/canvas/i18n/renderers.ts index 29687155818e7b..fa1fbc063dbe6e 100644 --- a/x-pack/plugins/canvas/i18n/renderers.ts +++ b/x-pack/plugins/canvas/i18n/renderers.ts @@ -55,16 +55,6 @@ export const RendererStrings = { defaultMessage: 'Renders an embeddable Saved Object from other parts of Kibana', }), }, - error: { - getDisplayName: () => - i18n.translate('xpack.canvas.renderer.error.displayName', { - defaultMessage: 'Error information', - }), - getHelpDescription: () => - i18n.translate('xpack.canvas.renderer.error.helpDescription', { - defaultMessage: 'Render error data in a way that is helpful to users', - }), - }, image: { getDisplayName: () => i18n.translate('xpack.canvas.renderer.image.displayName', { diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 85d2e0709cb3e7..545eae742a89e5 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -10,6 +10,7 @@ "charts", "data", "embeddable", + "expressionError", "expressionRevealImage", "expressions", "features", diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index d9df1e4661fbf2..e1cd5c55393fb5 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -9,9 +9,7 @@ import React, { MouseEventHandler, FC } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - -// @ts-expect-error untyped local -import { Popover, PopoverChildrenProps } from '../popover'; +import { Popover } from '../popover'; import { ArgAdd } from '../arg_add'; // @ts-expect-error untyped local import { Arg } from '../../expression_types/arg'; @@ -49,7 +47,7 @@ export const ArgAddPopover: FC = ({ options }) => { panelPaddingSize="none" button={button} > - {({ closePopover }: PopoverChildrenProps) => + {({ closePopover }) => options.map((opt) => ( )) .add('datasource with expression arguments', () => ( @@ -90,5 +91,6 @@ storiesOf('components/datasource/DatasourceComponent', module) setPreviewing={action('setPreviewing')} isInvalid={false} setInvalid={action('setInvalid')} + renderError={action('renderError')} /> )); diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js index f09ce4c925820c..c1df18fc06d559 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_component.js @@ -60,6 +60,7 @@ export class DatasourceComponent extends PureComponent { setPreviewing: PropTypes.func, isInvalid: PropTypes.bool, setInvalid: PropTypes.func, + renderError: PropTypes.func, }; state = { defaultIndex: '' }; @@ -125,6 +126,7 @@ export class DatasourceComponent extends PureComponent { setPreviewing, isInvalid, setInvalid, + renderError, } = this.props; const { defaultIndex } = this.state; @@ -155,6 +157,7 @@ export class DatasourceComponent extends PureComponent { isInvalid, setInvalid, defaultIndex, + renderError, }); const hasExpressionArgs = Object.values(stateArgs).some((a) => a && typeof a[0] === 'object'); diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index 2eb42c5cb98dc5..a45613f4bc96b4 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -20,8 +20,11 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { withSuspense } from '../../../../../../../src/plugins/presentation_util/public'; +import { LazyErrorComponent } from '../../../../../../../src/plugins/expression_error/public'; import { Datatable } from '../../datatable'; -import { Error } from '../../error'; + +const Error = withSuspense(LazyErrorComponent); const strings = { getEmptyFirstLineDescription: () => diff --git a/x-pack/plugins/canvas/public/components/debug/index.tsx b/x-pack/plugins/canvas/public/components/debug/index.tsx deleted file mode 100644 index 8adec03e854087..00000000000000 --- a/x-pack/plugins/canvas/public/components/debug/index.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Debug } from './debug'; diff --git a/x-pack/plugins/canvas/public/components/error/index.ts b/x-pack/plugins/canvas/public/components/error/index.ts deleted file mode 100644 index 65c4af8a369aee..00000000000000 --- a/x-pack/plugins/canvas/public/components/error/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Error } from './error'; diff --git a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot index 5c17eb2b681373..99d5dc3c115be1 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/expression_input/__stories__/__snapshots__/expression_input.stories.storyshot @@ -16,7 +16,18 @@ exports[`Storyshots components/ExpressionInput default 1`] = ` id="generated-id" onBlur={[Function]} onFocus={[Function]} - /> + > +
+
+
diff --git a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot index 770e94ec4b174c..02f54723abd42b 100644 --- a/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/home/__snapshots__/home.stories.storyshot @@ -123,9 +123,22 @@ exports[`Storyshots Home Home Page 1`] = `
- +
+
+ +
+
diff --git a/x-pack/plugins/canvas/public/components/popover/popover.tsx b/x-pack/plugins/canvas/public/components/popover/popover.tsx index 275d800fe2ca1e..a2793b3759f1e2 100644 --- a/x-pack/plugins/canvas/public/components/popover/popover.tsx +++ b/x-pack/plugins/canvas/public/components/popover/popover.tsx @@ -5,7 +5,6 @@ * 2.0. */ -/* eslint react/no-did-mount-set-state: 0, react/forbid-elements: 0 */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiPopover, EuiToolTip } from '@elastic/eui'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx index efc7f2fae8f8b1..f501410b26a74b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.component.tsx @@ -9,10 +9,9 @@ import React, { Fragment, FunctionComponent, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { Popover, ClosePopoverFn } from '../../popover'; import { ShortcutStrings } from '../../../../i18n/shortcuts'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { Popover, ClosePopoverFn } from '../../popover'; import { CustomElementModal } from '../../custom_element_modal'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants'; import { PositionedElement } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index e1d69163e07619..2907e8c4d5dd72 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -15,12 +15,11 @@ import { EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { Popover, ClosePopoverFn } from '../../popover'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { getId } from '../../../lib/get_id'; -import { Popover, ClosePopoverFn } from '../../popover'; import { AssetManager } from '../../asset_manager'; import { SavedElementsModal } from '../../saved_elements_modal'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx index e59bf284258fc4..de712b26983591 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx @@ -9,12 +9,11 @@ import React, { FunctionComponent, useState } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { Popover, ClosePopoverFn } from '../../popover'; import { ReportingStart } from '../../../../../reporting/public'; import { PDF, JSON } from '../../../../i18n/constants'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { usePlatformService } from '../../../services'; -import { ClosePopoverFn, Popover } from '../../popover'; import { ShareWebsiteFlyout } from './flyout'; import { CanvasWorkpadSharingData, getPdfJobParams } from './utils'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index b001ad04caa441..b2c6d97a51748d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -14,7 +14,7 @@ import { EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { Popover, ClosePopoverFn } from '../../popover'; import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, @@ -22,7 +22,6 @@ import { } from '../../../../common/lib/constants'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; -import { Popover, ClosePopoverFn } from '../../popover'; import { AutoRefreshControls } from './auto_refresh_controls'; import { KioskControls } from './kiosk_controls'; diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index aac898c3dd374b..0860bfd5afe6a0 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -19,7 +19,6 @@ @import '../components/datasource/datasource'; @import '../components/datasource/datasource_preview/datasource_preview'; @import '../components/datatable/datatable'; -@import '../components/debug/debug'; @import '../components/dom_preview/dom_preview'; @import '../components/element_card/element_card'; @import '../components/element_content/element_content'; diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js index 60987e987f63ac..df894a65afab17 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -5,8 +5,6 @@ * 2.0. */ -import { debug } from '../canvas_plugin_src/renderers/debug'; -import { error } from '../canvas_plugin_src/renderers/error'; import { image } from '../canvas_plugin_src/renderers/image'; import { repeatImage } from '../canvas_plugin_src/renderers/repeat_image'; import { markdown } from '../canvas_plugin_src/renderers/markdown'; @@ -18,6 +16,10 @@ import { shape } from '../canvas_plugin_src/renderers/shape'; import { table } from '../canvas_plugin_src/renderers/table'; import { text } from '../canvas_plugin_src/renderers/text'; import { revealImageRenderer as revealImage } from '../../../../src/plugins/expression_reveal_image/public'; +import { + errorRenderer as error, + debugRenderer as debug, +} from '../../../../src/plugins/expression_error/public'; /** * This is a collection of renderers which are bundled with the runtime. If diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 84ac1a26281e05..ddb52a22d2f17a 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -6,13 +6,15 @@ */ import fs from 'fs'; -import { ReactChildren } from 'react'; +import { ReactChildren, createElement } from 'react'; import path from 'path'; import moment from 'moment'; import 'moment-timezone'; import ReactDOM from 'react-dom'; +import { shallow } from 'enzyme'; +import { create, act } from 'react-test-renderer'; -import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; +import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots'; // @ts-expect-error untyped library import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; import { addSerializer } from 'jest-specific-snapshot'; @@ -114,11 +116,24 @@ jest.mock('../public/lib/es_service', () => ({ addSerializer(styleSheetSerializer); +const converter = new Stories2SnapsConverter(); + // Initialize Storyshots and build the Jest Snapshots initStoryshots({ configPath: path.resolve(__dirname), framework: 'react', - test: multiSnapshotWithOptions(), + // test: multiSnapshotWithOptions({}), + asyncJest: true, + test: async ({ story, context, done }) => { + const renderer = create(createElement(story.render)); + // wait until the element will perform all renders and resolve all promises (lazy loading, especially) + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + // save each snapshot to a different file (similar to "multiSnapshotWithOptions") + const snapshotFileName = converter.getSnapshotFileName(context); + expect(renderer).toMatchSpecificSnapshot(snapshotFileName); + done?.(); + }, // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, + renderer: shallow, }); diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 84581d7be85a37..bf9544a173f16f 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -31,6 +31,7 @@ { "path": "../../../src/plugins/discover/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../src/plugins/expression_error/tsconfig.json" }, { "path": "../../../src/plugins/expression_reveal_image/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/inspector/tsconfig.json" }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a6e643c03276a8..a4886bacb3b27a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6109,9 +6109,8 @@ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} ワークパッドに必要なプロパティの一部が欠けています。 {JSON} ファイルを編集して正しいプロパティ値を入力し、再試行してください。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "ワークパッドを作成できませんでした", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "ID でワークパッドを読み込めませんでした", - "xpack.canvas.errorComponent.description": "表現が失敗し次のメッセージが返されました:", - "xpack.canvas.errorComponent.title": "おっと!表現が失敗しました", - "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "「{fileName}」をアップロードできませんでした", + "expressionError.errorComponent.description": "表現が失敗し次のメッセージが返されました:", + "expressionError.errorComponent.title": "おっと!表現が失敗しました", "xpack.canvas.expression.cancelButtonLabel": "キャンセル", "xpack.canvas.expression.closeButtonLabel": "閉じる", "xpack.canvas.expression.learnLinkText": "表現構文の詳細", @@ -6523,15 +6522,15 @@ "xpack.canvas.renderer.advancedFilter.displayName": "高度なフィルター", "xpack.canvas.renderer.advancedFilter.helpDescription": "Canvas フィルター表現をレンダリングします。", "xpack.canvas.renderer.advancedFilter.inputPlaceholder": "フィルター表現を入力", - "xpack.canvas.renderer.debug.displayName": "デバッグ", - "xpack.canvas.renderer.debug.helpDescription": "デバッグアウトプットをフォーマットされた {JSON} としてレンダリングします", + "expressionError.renderer.debug.displayName": "デバッグ", + "expressionError.renderer.debug.helpDescription": "デバッグアウトプットをフォーマットされた {JSON} としてレンダリングします", "xpack.canvas.renderer.dropdownFilter.displayName": "ドロップダウンフィルター", "xpack.canvas.renderer.dropdownFilter.helpDescription": "「{exactly}」フィルターの値を選択できるドロップダウンです", "xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel": "すべて", "xpack.canvas.renderer.embeddable.displayName": "埋め込み可能", "xpack.canvas.renderer.embeddable.helpDescription": "Kibana の他の部分から埋め込み可能な保存済みオブジェクトをレンダリングします", - "xpack.canvas.renderer.error.displayName": "エラー情報", - "xpack.canvas.renderer.error.helpDescription": "エラーデータをユーザーにわかるようにレンダリングします", + "expressionError.renderer.error.displayName": "エラー情報", + "expressionError.renderer.error.helpDescription": "エラーデータをユーザーにわかるようにレンダリングします", "xpack.canvas.renderer.image.displayName": "画像", "xpack.canvas.renderer.image.helpDescription": "画像をレンダリングします", "xpack.canvas.renderer.markdown.displayName": "マークダウン", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bd8ca2f1b60554..dd3fcba3e2bdd8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6148,9 +6148,8 @@ "xpack.canvas.error.useImportWorkpad.missingPropertiesErrorMessage": "{CANVAS} Workpad 所需的某些属性缺失。 编辑 {JSON} 文件以提供正确的属性值,然后重试。", "xpack.canvas.error.workpadRoutes.createFailureErrorMessage": "无法创建 Workpad", "xpack.canvas.error.workpadRoutes.loadFailureErrorMessage": "无法加载具有以下 ID 的 Workpad", - "xpack.canvas.errorComponent.description": "表达式失败,并显示消息:", - "xpack.canvas.errorComponent.title": "哎哟!表达式失败", - "xpack.canvas.errors.useImportWorkpad.fileUploadFileWithFileNameErrorMessage": "无法上传“{fileName}”", + "expressionError.errorComponent.description": "表达式失败,并显示消息:", + "expressionError.errorComponent.title": "哎哟!表达式失败", "xpack.canvas.expression.cancelButtonLabel": "取消", "xpack.canvas.expression.closeButtonLabel": "关闭", "xpack.canvas.expression.learnLinkText": "学习表达式语法", @@ -6563,15 +6562,15 @@ "xpack.canvas.renderer.advancedFilter.displayName": "高级筛选", "xpack.canvas.renderer.advancedFilter.helpDescription": "呈现 Canvas 筛选表达式", "xpack.canvas.renderer.advancedFilter.inputPlaceholder": "输入筛选表达式", - "xpack.canvas.renderer.debug.displayName": "故障排查", - "xpack.canvas.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", + "expressionError.renderer.debug.displayName": "故障排查", + "expressionError.renderer.debug.helpDescription": "将故障排查输出呈现为带格式的 {JSON}", "xpack.canvas.renderer.dropdownFilter.displayName": "下拉列表筛选", "xpack.canvas.renderer.dropdownFilter.helpDescription": "可以从其中为“{exactly}”筛选选择值的下拉列表", "xpack.canvas.renderer.dropdownFilter.matchAllOptionLabel": "任意", "xpack.canvas.renderer.embeddable.displayName": "可嵌入", "xpack.canvas.renderer.embeddable.helpDescription": "从 Kibana 的其他部分呈现可嵌入的已保存对象", - "xpack.canvas.renderer.error.displayName": "错误信息", - "xpack.canvas.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", + "expressionError.renderer.error.displayName": "错误信息", + "expressionError.renderer.error.helpDescription": "以用户友好的方式呈现错误数据", "xpack.canvas.renderer.image.displayName": "图像", "xpack.canvas.renderer.image.helpDescription": "呈现图像", "xpack.canvas.renderer.markdown.displayName": "Markdown", From dd212707897a6470b6413f8b03b7809db2d1bf8e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 15 Jul 2021 08:24:16 -0400 Subject: [PATCH 09/10] [ML] Functional tests - re-activate a11y tests (#105198) (#105727) This PR re-activates the ML a11y test suite `with data loaded`. Co-authored-by: Robert Oskamp --- x-pack/test/accessibility/apps/ml.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/accessibility/apps/ml.ts b/x-pack/test/accessibility/apps/ml.ts index 382086728da019..4babe0bd6ff884 100644 --- a/x-pack/test/accessibility/apps/ml.ts +++ b/x-pack/test/accessibility/apps/ml.ts @@ -59,8 +59,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/103538 - describe.skip('with data loaded', function () { + describe('with data loaded', function () { const adJobId = 'fq_single_a11y'; const dfaOutlierJobId = 'iph_outlier_a11y'; const calendarId = 'calendar_a11y'; From d9f8663c0a2f4831ec61e8800f15cb2b6aa3e51d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 15 Jul 2021 08:35:02 -0400 Subject: [PATCH 10/10] [Remote Clusters] Fixed remote clusters details flyout for long strings (#105592) (#105729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com> --- .../sections/remote_cluster_list/detail_panel/detail_panel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js index 6969f98e5f092a..5c8ef874ec7ed1 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js @@ -494,6 +494,7 @@ export class DetailPanel extends Component { aria-labelledby="remoteClusterDetailsFlyoutTitle" size="m" maxWidth={550} + className="eui-textBreakAll" >