Skip to content

Commit

Permalink
[ML] Single Metric Viewer embeddable in dashboards: move all config t…
Browse files Browse the repository at this point in the history
…o flyout (#182756)

## Summary

Part of #182042
This PR also fixes the issue in the swimlane embeddable that fails to
close the flyout if navigating into the main dashboards page.

Related meta issue: #181272
Item: `https://github.com/elastic/kibana/issues/181272`


<img width="1293" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/c003f620-1f06-4716-b001-5a186fd1b7c6">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
alvarezmelissa87 and kibanamachine authored May 14, 2024
1 parent 7cc61db commit f0f2ca0
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 207 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/ml/public/alerting/job_selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export const JobSelectorControl: FC<JobSelectorControlProps> = ({

return (
<EuiFormRow
data-test-subj="mlAnomalyJobSelectionControls"
fullWidth
label={
label ?? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const getDefaultFieldConfig = (
interface SeriesControlsProps {
appStateHandler: Function;
bounds: any;
direction?: 'column' | 'row';
functionDescription?: string;
job?: CombinedJob | MlJob;
selectedDetectorIndex: number;
Expand All @@ -89,6 +90,7 @@ export const SeriesControls: FC<PropsWithChildren<SeriesControlsProps>> = ({
appStateHandler,
bounds,
children,
direction = 'row',
functionDescription,
job,
selectedDetectorIndex,
Expand Down Expand Up @@ -297,7 +299,7 @@ export const SeriesControls: FC<PropsWithChildren<SeriesControlsProps>> = ({

return (
<div data-test-subj="mlSingleMetricViewerSeriesControls">
<EuiFlexGroup>
<EuiFlexGroup direction={direction}>
<EuiFlexItem grow={false}>
<EuiFormRow
label={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ export const getSingleMetricViewerEmbeddableFactory = (

const api = buildApi(
{
isEditingEnabled: () => true,
getTypeDisplayName: () =>
i18n.translate('xpack.ml.singleMetricViewerEmbeddable.typeDisplayName', {
defaultMessage: 'single metric viewer',
}),
onEdit: async () => {
try {
const { resolveEmbeddableSingleMetricViewerUserInput } = await import(
'./single_metric_viewer_setup_flyout'
);
const [coreStart, { data, share }, { mlApiServices }] = services;
const result = await resolveEmbeddableSingleMetricViewerUserInput(
coreStart,
parentApi,
uuid,
{ data, share },
mlApiServices,
{
...serializeTitles(),
...serializeSingleMetricViewerState(),
}
);

singleMetricViewerControlsApi.updateUserInput(result);
} catch (e) {
return Promise.reject();
}
},
...titlesApi,
...timeRangeApi,
...singleMetricViewerControlsApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,103 @@
*/

import type { FC } from 'react';
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
EuiFieldText,
EuiModal,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import useMountedState from 'react-use/lib/useMountedState';
import { extractErrorMessage } from '@kbn/ml-error-utils';
import type { MlJob } from '@elastic/elasticsearch/lib/api/types';
import type { TimeRangeBounds } from '@kbn/ml-time-buckets';
import type { MlApiServices } from '../../application/services/ml_api_service';
import type { SingleMetricViewerEmbeddableInput } from '..';
import { ML_PAGES } from '../../../common/constants/locator';
import { SeriesControls } from '../../application/timeseriesexplorer/components/series_controls';
import {
APP_STATE_ACTION,
type TimeseriesexplorerActionType,
} from '../../application/timeseriesexplorer/timeseriesexplorer_constants';
import { useMlLink } from '../../application/contexts/kibana';
import { JobSelectorControl } from '../../alerting/job_selector';
import type { SingleMetricViewerEmbeddableUserInput, MlEntity } from '..';
import { getDefaultSingleMetricViewerPanelTitle } from './get_default_panel_title';

export interface SingleMetricViewerInitializerProps {
bounds: TimeRangeBounds;
defaultTitle: string;
initialInput?: Partial<SingleMetricViewerEmbeddableInput>;
job: MlJob;
onCreate: (props: Partial<SingleMetricViewerEmbeddableUserInput>) => void;
mlApiServices: MlApiServices;
onCreate: (props: SingleMetricViewerEmbeddableUserInput) => void;
onCancel: () => void;
}

export const SingleMetricViewerInitializer: FC<SingleMetricViewerInitializerProps> = ({
defaultTitle,
bounds,
initialInput,
job,
onCreate,
onCancel,
mlApiServices,
}) => {
const isNewJob = initialInput?.jobIds !== undefined && initialInput?.jobIds[0] !== job.job_id;
const isMounted = useMountedState();
const newJobUrl = useMlLink({ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB });
const [jobId, setJobId] = useState<string | undefined>(
initialInput?.jobIds && initialInput?.jobIds[0]
);
const titleManuallyChanged = useRef(!!initialInput?.title);

const [panelTitle, setPanelTitle] = useState<string>(defaultTitle);
const [job, setJob] = useState<MlJob | undefined>();
const [panelTitle, setPanelTitle] = useState<string>(initialInput?.title ?? '');
const [functionDescription, setFunctionDescription] = useState<string | undefined>(
initialInput?.functionDescription
);
// Reset detector index and entities if the job has changed
const [selectedDetectorIndex, setSelectedDetectorIndex] = useState<number>(
!isNewJob && initialInput?.selectedDetectorIndex ? initialInput.selectedDetectorIndex : 0
initialInput?.selectedDetectorIndex ?? 0
);
const [selectedEntities, setSelectedEntities] = useState<MlEntity | undefined>(
!isNewJob && initialInput?.selectedEntities ? initialInput.selectedEntities : undefined
initialInput?.selectedEntities
);

const [errorMessage, setErrorMessage] = useState<string | undefined>();
const isPanelTitleValid = panelTitle.length > 0;

useEffect(
function setUpPanel() {
async function fetchJob() {
const { jobs } = await mlApiServices.getJobs({ jobId });

if (isMounted() && jobs.length === 1) {
setJob(jobs[0]);
setErrorMessage(undefined);
}
}

if (jobId) {
if (!titleManuallyChanged.current) {
setPanelTitle(getDefaultSingleMetricViewerPanelTitle(jobId));
}
// Fetch job if a jobId has been selected and if there is no corresponding fetched job or the job selection has changed
if (mlApiServices && jobId && jobId !== job?.job_id) {
fetchJob().catch((error) => {
const errorMsg = extractErrorMessage(error);
setErrorMessage(errorMsg);
});
}
}
},
[isMounted, jobId, mlApiServices, panelTitle, job?.job_id]
);

const handleStateUpdate = (
action: TimeseriesexplorerActionType,
payload: string | number | MlEntity
Expand All @@ -84,23 +123,33 @@ export const SingleMetricViewerInitializer: FC<SingleMetricViewerInitializerProp
};

return (
<EuiModal
maxWidth={false}
initialFocus="[name=panelTitle]"
onClose={onCancel}
data-test-subj={'mlSingleMetricViewerEmbeddableInitializer'}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
<FormattedMessage
id="xpack.ml.SingleMetricViewerEmbeddable.setupModal.title"
defaultMessage="Single metric viewer configuration"
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
<>
<EuiFlyoutHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.ml.SingleMetricViewerEmbeddable.setupModal.title"
defaultMessage="Single metric viewer configuration"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>

<EuiModalBody>
<EuiFlyoutBody>
<EuiForm>
<JobSelectorControl
adJobsApiService={mlApiServices.jobs}
createJobUrl={newJobUrl}
jobsAndGroupIds={jobId ? [jobId] : undefined}
onChange={(update) => {
setJobId(update?.jobIds && update?.jobIds[0]);
// Reset values when selected job has changed
setSelectedDetectorIndex(0);
setSelectedEntities(undefined);
setFunctionDescription(undefined);
}}
{...(errorMessage && { errors: [errorMessage] })}
/>
<EuiFormRow
label={
<FormattedMessage
Expand All @@ -109,58 +158,71 @@ export const SingleMetricViewerInitializer: FC<SingleMetricViewerInitializerProp
/>
}
isInvalid={!isPanelTitleValid}
fullWidth
>
<EuiFieldText
data-test-subj="panelTitleInput"
id="panelTitle"
name="panelTitle"
value={panelTitle}
onChange={(e) => setPanelTitle(e.target.value)}
onChange={(e) => {
titleManuallyChanged.current = true;
setPanelTitle(e.target.value);
}}
isInvalid={!isPanelTitleValid}
fullWidth
/>
</EuiFormRow>
<EuiSpacer />
<SeriesControls
selectedJobId={job.job_id}
job={job}
appStateHandler={handleStateUpdate}
selectedDetectorIndex={selectedDetectorIndex}
selectedEntities={selectedEntities}
bounds={bounds}
functionDescription={functionDescription}
setFunctionDescription={setFunctionDescription}
/>
{job?.job_id && jobId && jobId === job.job_id ? (
<SeriesControls
selectedJobId={jobId}
job={job}
direction="column"
appStateHandler={handleStateUpdate}
selectedDetectorIndex={selectedDetectorIndex}
selectedEntities={selectedEntities}
bounds={bounds}
functionDescription={functionDescription}
setFunctionDescription={setFunctionDescription}
/>
) : null}
</EuiForm>
</EuiModalBody>

<EuiModalFooter>
<EuiButtonEmpty
onClick={onCancel}
data-test-subj="mlsingleMetricViewerInitializerCancelButton"
>
<FormattedMessage
id="xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>

<EuiButton
data-test-subj="mlSingleMetricViewerInitializerConfirmButton"
isDisabled={!isPanelTitleValid}
onClick={onCreate.bind(null, {
functionDescription,
panelTitle,
selectedDetectorIndex,
selectedEntities,
})}
fill
>
<FormattedMessage
id="xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel"
defaultMessage="Confirm configurations"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent={'spaceBetween'}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={onCancel}
data-test-subj="mlsingleMetricViewerInitializerCancelButton"
>
<FormattedMessage
id="xpack.ml.singleMetricViewerEmbeddable.setupModal.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="mlSingleMetricViewerInitializerConfirmButton"
isDisabled={!isPanelTitleValid || errorMessage !== undefined || !jobId || !job}
onClick={onCreate.bind(null, {
jobIds: jobId ? [jobId] : [],
functionDescription,
panelTitle,
selectedDetectorIndex,
selectedEntities,
})}
fill
>
<FormattedMessage
id="xpack.ml.singleMetricViewerEmbeddable.setupModal.confirmButtonLabel"
defaultMessage="Confirm"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};
Loading

0 comments on commit f0f2ca0

Please sign in to comment.