Skip to content

Commit

Permalink
[ML] Adding feedback button to Add feedback anomaly explorer and sing…
Browse files Browse the repository at this point in the history
…le 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
  • Loading branch information
jgowdyelastic authored Jan 29, 2024
1 parent 83790a6 commit 9629e66
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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 = (
<FormattedMessage
id="xpack.ml.featureFeedbackButton.tellUsWhatYouThinkLink"
defaultMessage="Tell us what you think!"
/>
),
}: FeatureFeedbackButtonProps) => {
const deploymentType =
isCloudEnv !== undefined || isServerlessEnv !== undefined
? getDeploymentType(isCloudEnv, isServerlessEnv)
: undefined;

return (
<EuiButton
href={getSurveyFeedbackURL(formUrl, kibanaVersion, deploymentType, sanitizedPath)}
target="_blank"
color={defaultButton ? undefined : 'warning'}
iconType={defaultButton ? undefined : 'editorComment'}
data-test-subj={dts}
onClickCapture={onClickCapture}
>
{surveyButtonText}
</EuiButton>
);
};
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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<string | null>(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 (
<FeatureFeedbackButton
data-test-subj="mlFeatureFeedbackButton"
formUrl={getFormUrl(formId)}
kibanaVersion={kibanaVersion}
isCloudEnv={isCloud}
isServerlessEnv={showNodeInfo === false}
/>
);
};

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`;
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -183,6 +185,15 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J
})}
</EuiButtonEmpty>
</EuiFlexItem>

<EuiFlexItem />

<EuiFlexItem grow={false}>
<FeedBackButton
jobIds={selectedIds}
page={singleSelection ? ML_PAGES.SINGLE_METRIC_VIEWER : ML_PAGES.ANOMALY_EXPLORER}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />
</>
Expand Down

0 comments on commit 9629e66

Please sign in to comment.