From 9629e661348609d66d04df710140d747242b2f2b Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 29 Jan 2024 17:25:30 +0000 Subject: [PATCH] [ML] Adding feedback button to Add feedback anomaly explorer and single metric viewer (#175613) Adds a user feedback button to anomaly explorer and single metric viewer. Button is only shown if any of the selected jobs have the created by label `ml-module-metrics-ui-hosts` Fixes https://github.com/elastic/kibana/issues/174898 To test, it's possible to change the URL of the recognizer page to create jobs: Open the recognizer page for the `Kibana Sample Data Logs` index. In the URL change `sample_data_weblogs` to `metrics_ui_hosts` The created jobs will not find any anomalies, but they will open in the anomaly explorer and single metric viewer. https://github.com/elastic/kibana/assets/22172091/9a8aef8e-5a8d-4283-b547-909df9b9c2a0 --- .../feature_feedback_button.tsx | 91 ++++++++++++++++++ .../feedback_button/feedback_button.tsx | 94 +++++++++++++++++++ .../components/feedback_button/index.ts | 8 ++ .../components/job_selector/job_selector.tsx | 11 +++ 4 files changed, 204 insertions(+) create mode 100644 x-pack/plugins/ml/public/application/components/feedback_button/feature_feedback_button.tsx create mode 100644 x-pack/plugins/ml/public/application/components/feedback_button/feedback_button.tsx create mode 100644 x-pack/plugins/ml/public/application/components/feedback_button/index.ts diff --git a/x-pack/plugins/ml/public/application/components/feedback_button/feature_feedback_button.tsx b/x-pack/plugins/ml/public/application/components/feedback_button/feature_feedback_button.tsx new file mode 100644 index 0000000000000..ae96787202cf9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/feedback_button/feature_feedback_button.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactElement } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const KIBANA_VERSION_QUERY_PARAM = 'entry.548460210'; +const KIBANA_DEPLOYMENT_TYPE_PARAM = 'entry.573002982'; +const SANITIZED_PATH_PARAM = 'entry.1876422621'; + +const getDeploymentType = (isCloudEnv?: boolean, isServerlessEnv?: boolean): string | undefined => { + if (isServerlessEnv) { + return 'Serverless (fully-managed projects)'; + } + if (isCloudEnv) { + return 'Elastic Cloud (we manage)'; + } + return 'Self-Managed (you manage)'; +}; + +const getSurveyFeedbackURL = ( + formUrl: string, + kibanaVersion?: string, + deploymentType?: string, + sanitizedPath?: string +) => { + const url = new URL(formUrl); + if (kibanaVersion) { + url.searchParams.append(KIBANA_VERSION_QUERY_PARAM, kibanaVersion); + } + if (deploymentType) { + url.searchParams.append(KIBANA_DEPLOYMENT_TYPE_PARAM, deploymentType); + } + if (sanitizedPath) { + url.searchParams.append(SANITIZED_PATH_PARAM, sanitizedPath); + } + + return url.href; +}; + +interface FeatureFeedbackButtonProps { + formUrl: string; + 'data-test-subj': string; + surveyButtonText?: ReactElement; + onClickCapture?: () => void; + defaultButton?: boolean; + kibanaVersion?: string; + isCloudEnv?: boolean; + isServerlessEnv?: boolean; + sanitizedPath?: string; +} + +export const FeatureFeedbackButton = ({ + formUrl, + 'data-test-subj': dts, + onClickCapture, + defaultButton, + kibanaVersion, + isCloudEnv, + isServerlessEnv, + sanitizedPath, + surveyButtonText = ( + + ), +}: FeatureFeedbackButtonProps) => { + const deploymentType = + isCloudEnv !== undefined || isServerlessEnv !== undefined + ? getDeploymentType(isCloudEnv, isServerlessEnv) + : undefined; + + return ( + + {surveyButtonText} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/feedback_button/feedback_button.tsx b/x-pack/plugins/ml/public/application/components/feedback_button/feedback_button.tsx new file mode 100644 index 0000000000000..b543debda347a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/feedback_button/feedback_button.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, FC, useMemo } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { MlPages, ML_PAGES } from '../../../locator'; +import { useMlKibana } from '../../contexts/kibana'; +import { useEnabledFeatures } from '../../contexts/ml'; + +import { useJobsApiService } from '../../services/ml_api_service/jobs'; +import { useCloudCheck } from '../node_available_warning/hooks'; +import { FeatureFeedbackButton } from './feature_feedback_button'; + +interface Props { + jobIds: string[]; + page: MlPages; +} + +const FORM_IDS = { + SINGLE_METRIC_VIEWER: '1FAIpQLSdlMYe3wuJh2KtBLajI4EVoUljAhGjJwjZI7zUY_Kn_Sr2lug', + ANOMALY_EXPLORER: '1FAIpQLSfF1Ry561b4lYrY7iiyXhuZpxFzAmy2c9BFUT3J2AJUevY1iw', +}; + +const MATCHED_CREATED_BY_TAGS = ['ml-module-metrics-ui-hosts']; + +export const FeedBackButton: FC = ({ jobIds, page }) => { + const { jobs: getJobs } = useJobsApiService(); + const { + services: { kibanaVersion }, + } = useMlKibana(); + const { isCloud } = useCloudCheck(); + // ML does not have an explicit isServerless flag, + // it does however have individual feature flags which are set depending + // whether the environment is serverless or not. + // showNodeInfo will always be false in a serverless environment + // and true in a non-serverless environment. + const { showNodeInfo } = useEnabledFeatures(); + + const [jobIdsString, setJobIdsString] = useState(null); + const [showButton, setShowButton] = useState(false); + + const formId = useMemo(() => getFormId(page), [page]); + const isMounted = useMountedState(); + + useEffect(() => { + const tempJobIdsString = jobIds.join(','); + if (tempJobIdsString === jobIdsString || tempJobIdsString === '') { + return; + } + setShowButton(false); + setJobIdsString(tempJobIdsString); + + getJobs(jobIds).then((resp) => { + if (isMounted()) { + setShowButton( + resp.some((job) => MATCHED_CREATED_BY_TAGS.includes(job.custom_settings?.created_by)) + ); + } + }); + }, [jobIds, getJobs, jobIdsString, isMounted]); + + if (showButton === false || formId === null) { + return null; + } + + return ( + + ); +}; + +function getFormId(page: MlPages) { + switch (page) { + case ML_PAGES.SINGLE_METRIC_VIEWER: + return FORM_IDS.SINGLE_METRIC_VIEWER; + case ML_PAGES.ANOMALY_EXPLORER: + return FORM_IDS.ANOMALY_EXPLORER; + default: + return null; + } +} + +function getFormUrl(formId: string) { + return `https://docs.google.com/forms/d/e/${formId}/viewform?usp=pp_url`; +} diff --git a/x-pack/plugins/ml/public/application/components/feedback_button/index.ts b/x-pack/plugins/ml/public/application/components/feedback_button/index.ts new file mode 100644 index 0000000000000..00eb71bf09e5e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/feedback_button/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FeedBackButton } from './feedback_button'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index fa2808638cbf5..6bf9febf338f7 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -20,6 +20,7 @@ import { useUrlState } from '@kbn/ml-url-state'; import './_index.scss'; import { useStorage } from '@kbn/ml-local-storage'; +import { ML_PAGES } from '../../../locator'; import { Dictionary } from '../../../../common/types/common'; import { IdBadges } from './id_badges'; import { @@ -29,6 +30,7 @@ import { } from './job_selector_flyout'; import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage'; +import { FeedBackButton } from '../feedback_button'; interface GroupObj { groupId: string; @@ -183,6 +185,15 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J })} + + + + + +