Skip to content

Commit

Permalink
[Infra ML] Add feedback button: Anomaly detection for hosts and pods (e…
Browse files Browse the repository at this point in the history
…lastic#177615)

Closes elastic#175792

## Summary

This PR adds a feedback button to the ML flyout in Infra.

<img width="1834" alt="image"
src="https://github.com/elastic/kibana/assets/14139027/296ea04d-38d0-41bd-9b9b-28da28d3d403">


## Small UI fixes

This PR makes the tabs inside the flyout consistent with other UI
flyouts ( smaller size and aligns them with the title and places them in
the header)

| Before | After |
| ------- | -------------- |
|
![image](https://github.com/elastic/kibana/assets/14139027/80a66498-56bf-44f9-af0d-50c2423498f3)
|
![image](https://github.com/elastic/kibana/assets/14139027/300d2281-71a6-421e-aef9-5f3c57d668b4)
|

## Testing 
- Go to Inventory and click on the Anomaly Detection (Top menu, next to
Settings)
   - Feedback button should be visible
   - Click on the feedback button
- The first step (without any jobs enabled) should not prefill the first
question (click on `Enable` for host/pod job)
- The second step (host or pod) should prefill the first question with
the host or pod answer
- Same on 3rd step (follow the URL at the bottom of the screen to check
the params):
     

https://github.com/elastic/kibana/assets/14139027/8b583a4b-aefa-4694-ab17-98066df97b6b

- (note that it shouldn't prefill the first question if both pod and
host jobs are enabled)

![image](https://github.com/elastic/kibana/assets/14139027/d3e0a1b3-02ea-44e1-9ee8-18f1ce216c4f)

---------

Co-authored-by: Cauê Marcondes <[email protected]>
  • Loading branch information
jennypavlova and cauemarcondes authored Feb 23, 2024
1 parent c98ee2f commit 043d12a
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@
* 2.0.
*/

import React, { useState, useCallback, useEffect } from 'react';
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiSpacer } from '@elastic/eui';
import React, { useState, useCallback, useEffect, useContext } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiIcon,
EuiSpacer,
EuiTabs,
EuiTab,
EuiText,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { EuiButtonEmpty } from '@elastic/eui';
import moment from 'moment';
import { EuiTabs } from '@elastic/eui';
import { EuiTab } from '@elastic/eui';
import { MLJobsAwaitingNodeWarning } from '@kbn/ml-plugin/public';
import { useLinkProps } from '@kbn/observability-shared-plugin/public';
import { FeatureFeedbackButton, useLinkProps } from '@kbn/observability-shared-plugin/public';
import { css } from '@emotion/react';
import { KibanaEnvironmentContext } from '../../../../../../hooks/use_kibana';
import { SubscriptionSplashPrompt } from '../../../../../../components/subscription_splash_content';
import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
import {
Expand All @@ -36,6 +48,10 @@ interface Props {
}

type Tab = 'jobs' | 'anomalies';

export const INFRA_ML_FLYOUT_FEEDBACK_LINK =
'https://docs.google.com/forms/d/e/1FAIpQLSfBixH_1HTuqeMCy38iK9w1mB8vl_eVvcLUlSPAPiWKBHeHiQ/viewform';

export const FlyoutHome = (props: Props) => {
const [tab, setTab] = useState<Tab>('jobs');
const { goToSetup, closeFlyout } = props;
Expand All @@ -51,6 +67,8 @@ export const FlyoutHome = (props: Props) => {
} = useMetricK8sModuleContext();
const { hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities } =
useInfraMLCapabilitiesContext();
const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext);
const { euiTheme } = useEuiTheme();

const createHosts = useCallback(() => {
goToSetup('hosts');
Expand Down Expand Up @@ -78,6 +96,14 @@ export const FlyoutHome = (props: Props) => {
pathname: '/jobs',
});

// Used for prefilling the feedback form (if both types are enabled do not prefill)
const mlJobTypeByNode =
hostJobSummaries.length > 0 && k8sJobSummaries.length === 0
? 'host'
: hostJobSummaries.length === 0 && k8sJobSummaries.length > 0
? 'pod'
: undefined;

if (!hasInfraMLCapabilities) {
return <SubscriptionSplashPrompt />;
} else if (!hasInfraMLReadCapabilities) {
Expand All @@ -95,33 +121,62 @@ export const FlyoutHome = (props: Props) => {
} else {
return (
<>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>
<FormattedMessage
defaultMessage="Machine Learning anomaly detection"
id="xpack.infra.ml.anomalyFlyout.flyoutHeader"
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h2>
<FormattedMessage
defaultMessage="Machine Learning anomaly detection"
id="xpack.infra.ml.anomalyFlyout.flyoutHeader"
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
css={css`
margin-right: ${euiTheme.size.l};
`}
>
<FeatureFeedbackButton
data-test-subj={
mlJobTypeByNode
? `infraML${mlJobTypeByNode}FlyoutFeedbackLink`
: 'infraMLFlyoutFeedbackLink'
}
formUrl={INFRA_ML_FLYOUT_FEEDBACK_LINK}
kibanaVersion={kibanaVersion}
isCloudEnv={isCloudEnv}
isServerlessEnv={isServerlessEnv}
nodeType={mlJobTypeByNode}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>

<EuiTabs>
<EuiTab isSelected={tab === 'jobs'} onClick={() => setTab('jobs')}>
{i18n.translate('xpack.infra.ml.anomalyFlyout.jobsTabLabel', {
defaultMessage: 'Jobs',
})}
</EuiTab>
<EuiTab
isSelected={tab === 'anomalies'}
onClick={() => setTab('anomalies')}
data-test-subj="anomalyFlyoutAnomaliesTab"
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiTabs
bottomBorder
css={css`
margin-bottom: calc(-1 * (${euiTheme.size.l} + 1px));
`}
size="s"
>
{i18n.translate('xpack.infra.ml.anomalyFlyout.anomaliesTabLabel', {
defaultMessage: 'Anomalies',
})}
</EuiTab>
</EuiTabs>
<EuiTab isSelected={tab === 'jobs'} onClick={() => setTab('jobs')}>
{i18n.translate('xpack.infra.ml.anomalyFlyout.jobsTabLabel', {
defaultMessage: 'Jobs',
})}
</EuiTab>
<EuiTab
isSelected={tab === 'anomalies'}
onClick={() => setTab('anomalies')}
data-test-subj="anomalyFlyoutAnomaliesTab"
>
{i18n.translate('xpack.infra.ml.anomalyFlyout.anomaliesTabLabel', {
defaultMessage: 'Anomalies',
})}
</EuiTab>
</EuiTabs>
</EuiFlyoutHeader>

<EuiFlyoutBody
banner={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,41 @@
* 2.0.
*/
import { debounce } from 'lodash';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
import { EuiText, EuiSpacer } from '@elastic/eui';
import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import React, { useState, useCallback, useMemo, useEffect, useContext } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCode,
EuiCallOut,
EuiForm,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiComboBox,
EuiDescribedFormGroup,
EuiLoadingSpinner,
EuiText,
EuiSpacer,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlyoutFooter } from '@elastic/eui';
import { EuiButton } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import moment, { Moment } from 'moment';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiLoadingSpinner } from '@elastic/eui';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { EuiCallOut } from '@elastic/eui';
import { EuiCode } from '@elastic/eui';
import { FeatureFeedbackButton, useUiTracker } from '@kbn/observability-shared-plugin/public';
import { css } from '@emotion/react';
import { KibanaEnvironmentContext } from '../../../../../../hooks/use_kibana';
import { useSourceContext } from '../../../../../../containers/metrics_source';
import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module';
import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module';
import { FixedDatePicker } from '../../../../../../components/fixed_datepicker';
import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor';
import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar';
import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery';
import { INFRA_ML_FLYOUT_FEEDBACK_LINK } from './flyout_home';

interface Props {
jobType: 'hosts' | 'kubernetes';
Expand All @@ -39,46 +52,48 @@ export const JobSetupScreen = (props: Props) => {
const { goHome } = props;
const [startDate, setStartDate] = useState<Moment>(now.clone().subtract(4, 'weeks'));
const [partitionField, setPartitionField] = useState<string[] | null>(null);
const h = useMetricHostsModuleContext();
const k = useMetricK8sModuleContext();
const host = useMetricHostsModuleContext();
const kubernetes = useMetricK8sModuleContext();
const [filter, setFilter] = useState<string>('');
const [filterQuery, setFilterQuery] = useState<string>('');
const trackMetric = useUiTracker({ app: 'infra_metrics' });
const { createDerivedIndexPattern } = useSourceContext();
const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext);
const { euiTheme } = useEuiTheme();

const indicies = h.sourceConfiguration.indices;
const indices = host.sourceConfiguration.indices;

const setupStatus = useMemo(() => {
if (props.jobType === 'kubernetes') {
return k.setupStatus;
return kubernetes.setupStatus;
} else {
return h.setupStatus;
return host.setupStatus;
}
}, [props.jobType, k.setupStatus, h.setupStatus]);
}, [props.jobType, kubernetes.setupStatus, host.setupStatus]);

const cleanUpAndSetUpModule = useMemo(() => {
if (props.jobType === 'kubernetes') {
return k.cleanUpAndSetUpModule;
return kubernetes.cleanUpAndSetUpModule;
} else {
return h.cleanUpAndSetUpModule;
return host.cleanUpAndSetUpModule;
}
}, [props.jobType, k.cleanUpAndSetUpModule, h.cleanUpAndSetUpModule]);
}, [props.jobType, kubernetes.cleanUpAndSetUpModule, host.cleanUpAndSetUpModule]);

const setUpModule = useMemo(() => {
if (props.jobType === 'kubernetes') {
return k.setUpModule;
return kubernetes.setUpModule;
} else {
return h.setUpModule;
return host.setUpModule;
}
}, [props.jobType, k.setUpModule, h.setUpModule]);
}, [props.jobType, kubernetes.setUpModule, host.setUpModule]);

const hasSummaries = useMemo(() => {
if (props.jobType === 'kubernetes') {
return k.jobSummaries.length > 0;
return kubernetes.jobSummaries.length > 0;
} else {
return h.jobSummaries.length > 0;
return host.jobSummaries.length > 0;
}
}, [props.jobType, k.jobSummaries, h.jobSummaries]);
}, [props.jobType, kubernetes.jobSummaries, host.jobSummaries]);

const derivedIndexPattern = useMemo(
() => createDerivedIndexPattern(),
Expand All @@ -92,15 +107,15 @@ export const JobSetupScreen = (props: Props) => {
const createJobs = useCallback(() => {
if (hasSummaries) {
cleanUpAndSetUpModule(
indicies,
indices,
moment(startDate).toDate().getTime(),
undefined,
filterQuery,
partitionField ? partitionField[0] : undefined
);
} else {
setUpModule(
indicies,
indices,
moment(startDate).toDate().getTime(),
undefined,
filterQuery,
Expand All @@ -112,7 +127,7 @@ export const JobSetupScreen = (props: Props) => {
filterQuery,
setUpModule,
hasSummaries,
indicies,
indices,
partitionField,
startDate,
]);
Expand Down Expand Up @@ -163,15 +178,34 @@ export const JobSetupScreen = (props: Props) => {
return (
<>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>
<FormattedMessage
defaultMessage="Enable machine learning for {nodeType}"
id="xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader"
values={{ nodeType: props.jobType }}
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h2>
<FormattedMessage
defaultMessage="Enable machine learning for {nodeType}"
id="xpack.infra.ml.aomalyFlyout.jobSetup.flyoutHeader"
values={{ nodeType: props.jobType }}
/>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
css={css`
margin-right: ${euiTheme.size.l};
`}
>
<FeatureFeedbackButton
data-test-subj={`infraML${props.jobType}FlyoutFeedbackLink`}
formUrl={INFRA_ML_FLYOUT_FEEDBACK_LINK}
kibanaVersion={kibanaVersion}
isCloudEnv={isCloudEnv}
isServerlessEnv={isServerlessEnv}
nodeType={props.jobType === 'kubernetes' ? 'pod' : 'host'}
/>
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{setupStatus.type === 'pending' ? (
Expand Down
Loading

0 comments on commit 043d12a

Please sign in to comment.