Skip to content

Commit

Permalink
[APM] Offer users upgrade to multi-metric job
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar committed Nov 30, 2021
1 parent 3223032 commit dff3019
Show file tree
Hide file tree
Showing 23 changed files with 956 additions and 263 deletions.
17 changes: 17 additions & 0 deletions x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { DATAFEED_STATE, JOB_STATE } from '../../../ml/common/constants/states';
import { Environment } from '../environment_rt';

export interface ApmMlJob {
environment: Environment;
version: number;
jobId: string;
jobState?: JOB_STATE;
datafeedId?: string;
datafeedState?: DATAFEED_STATE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.
*/
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { FETCH_STATUS } from '../../public/hooks/use_fetcher';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import type { APIReturnType } from '../../public/services/rest/createCallApmApi';
import { ENVIRONMENT_ALL } from '../environment_filter_values';

export enum AnomalyDetectionSetupState {
Loading = 'pending',
Failure = 'failure',
Unknown = 'unknown',
NoJobs = 'noJobs',
NoJobsForEnvironment = 'noJobsForEnvironment',
UpgradeableJobs = 'upgradeableJobs',
UpToDate = 'upToDate',
}

export function getAnomalyDetectionSetupState({
environment,
jobs,
fetchStatus,
isAuthorized,
}: {
environment: string;
jobs: APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>['jobs'];
fetchStatus: FETCH_STATUS;
isAuthorized: boolean;
}): AnomalyDetectionSetupState {
if (!isAuthorized) {
return AnomalyDetectionSetupState.Unknown;
}

if (fetchStatus === FETCH_STATUS.LOADING) {
return AnomalyDetectionSetupState.Loading;
}

if (fetchStatus === FETCH_STATUS.FAILURE) {
return AnomalyDetectionSetupState.Failure;
}

if (fetchStatus !== FETCH_STATUS.SUCCESS) {
return AnomalyDetectionSetupState.Unknown;
}

const jobsForEnvironment =
environment === ENVIRONMENT_ALL.value
? jobs
: jobs.filter((job) => job.environment === environment);

const hasV2Jobs = jobsForEnvironment.some((job) => job.version === 2);
const hasV3Jobs = jobsForEnvironment.some((job) => job.version === 3);
const hasAnyJobs = jobs.length;

if (hasV3Jobs) {
return AnomalyDetectionSetupState.UpToDate;
}

if (hasV2Jobs) {
return AnomalyDetectionSetupState.UpgradeableJobs;
}

if (hasAnyJobs) {
return AnomalyDetectionSetupState.NoJobsForEnvironment;
}

return AnomalyDetectionSetupState.NoJobs;
}
12 changes: 7 additions & 5 deletions x-pack/plugins/apm/common/environment_rt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import {
ENVIRONMENT_NOT_DEFINED,
} from './environment_filter_values';

export const environmentStringRt = t.union([
t.literal(ENVIRONMENT_NOT_DEFINED.value),
t.literal(ENVIRONMENT_ALL.value),
nonEmptyStringRt,
]);

export const environmentRt = t.type({
environment: t.union([
t.literal(ENVIRONMENT_NOT_DEFINED.value),
t.literal(ENVIRONMENT_ALL.value),
nonEmptyStringRt,
]),
environment: environmentStringRt,
});

export type Environment = t.TypeOf<typeof environmentRt>['environment'];
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,14 @@ import { ML_ERRORS } from '../../../../../common/anomaly_detection';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { LicensePrompt } from '../../../shared/license_prompt';
import { useLicenseContext } from '../../../../context/license/use_license_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context';

export type AnomalyDetectionApiResponse =
APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>;

const DEFAULT_VALUE: AnomalyDetectionApiResponse = {
jobs: [],
hasLegacyJobs: false,
};

export function AnomalyDetection() {
const plugin = useApmPluginContext();
const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs;
Expand All @@ -33,20 +28,14 @@ export function AnomalyDetection() {
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);

const {
refetch,
data = DEFAULT_VALUE,
status,
} = useFetcher(
(callApmApi) => {
if (canGetJobs) {
return callApmApi({
endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`,
});
}
},
[canGetJobs],
{ preservePreviousData: false, showToastOnError: false }
);
anomalyDetectionJobsStatus,
anomalyDetectionJobsRefetch,
anomalyDetectionJobsData = {
jobs: [],
hasLegacyJobs: false,
} as AnomalyDetectionApiResponse,
anomalyDetectionSetupState,
} = useAnomalyDetectionJobsContext();

if (!hasValidLicense) {
return (
Expand All @@ -71,9 +60,11 @@ export function AnomalyDetection() {
<>
{viewAddEnvironments ? (
<AddEnvironments
currentEnvironments={data.jobs.map(({ environment }) => environment)}
currentEnvironments={anomalyDetectionJobsData.jobs.map(
({ environment }) => environment
)}
onCreateJobSuccess={() => {
refetch();
anomalyDetectionJobsRefetch();
setViewAddEnvironments(false);
}}
onCancel={() => {
Expand All @@ -82,11 +73,15 @@ export function AnomalyDetection() {
/>
) : (
<JobsList
data={data}
status={status}
data={anomalyDetectionJobsData}
status={anomalyDetectionJobsStatus}
setupState={anomalyDetectionSetupState}
onAddEnvironments={() => {
setViewAddEnvironments(true);
}}
onUpdateComplete={() => {
anomalyDetectionJobsRefetch();
}}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,34 @@
* 2.0.
*/

import { EuiSwitch } from '@elastic/eui';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiText,
EuiTitle,
EuiToolTip,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import React, { useState } from 'react';
import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public';
import { AnomalyDetectionSetupState } from '../../../../../common/anomaly_detection/get_anomaly_detection_setup_state';
import { getEnvironmentLabel } from '../../../../../common/environment_filter_values';
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href';
import { MLExplorerLink } from '../../../shared/Links/MachineLearningLinks/MLExplorerLink';
import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
import { AnomalyDetectionApiResponse } from './index';
import { JobsListStatus } from './jobs_list_status';
import { LegacyJobsCallout } from './legacy_jobs_callout';
import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public';
import { UpdateJobsCallout } from './update_jobs_callout';

type Jobs = AnomalyDetectionApiResponse['jobs'];

Expand All @@ -36,7 +43,24 @@ const columns: Array<ITableColumn<Jobs[0]>> = [
'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel',
{ defaultMessage: 'Environment' }
),
render: getEnvironmentLabel,
width: '100%',
render: (_, { environment, jobId, jobState, datafeedState, version }) => {
return (
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
{getEnvironmentLabel(environment)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<JobsListStatus
jobId={jobId}
version={version}
jobState={jobState}
datafeedState={datafeedState}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
},
},
{
field: 'job_id',
Expand All @@ -45,30 +69,72 @@ const columns: Array<ITableColumn<Jobs[0]>> = [
'xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel',
{ defaultMessage: 'Action' }
),
render: (_, { job_id: jobId }) => (
<MLExplorerLink jobId={jobId}>
{i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText',
{
defaultMessage: 'View job in ML',
}
)}
</MLExplorerLink>
),
render: (_, { jobId }) => {
return (
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText',
{
defaultMessage: 'Manage job',
}
)}
>
{/* setting the key to remount the element as a workaround for https://github.com/elastic/kibana/issues/119951*/}
<MLManageJobsLink jobId={jobId} key={jobId}>
<EuiIcon type="gear" />
</MLManageJobsLink>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText',
{
defaultMessage: 'Open in Anomaly Explorer',
}
)}
>
<MLExplorerLink jobId={jobId}>
<EuiIcon type="visTable" />
</MLExplorerLink>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
},
},
];

interface Props {
data: AnomalyDetectionApiResponse;
setupState: AnomalyDetectionSetupState;
status: FETCH_STATUS;
onAddEnvironments: () => void;
onUpdateComplete: () => void;
}
export function JobsList({ data, status, onAddEnvironments }: Props) {

export function JobsList({
data,
status,
onAddEnvironments,
setupState,
onUpdateComplete,
}: Props) {
const { jobs, hasLegacyJobs } = data;

const [showLegacyJobs, setShowLegacyJobs] = useState(false);

const mlManageJobsHref = useMlManageJobsHref();

const filteredJobs = showLegacyJobs
? jobs
: jobs.filter((job) => job.version >= 3);

return (
<>
<MLJobsAwaitingNodeWarning jobIds={jobs.map((j) => j.job_id)} />
<MLJobsAwaitingNodeWarning jobIds={filteredJobs.map((j) => j.jobId)} />
<EuiText color="subdued">
<FormattedMessage
id="xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText"
Expand All @@ -90,7 +156,14 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {

<EuiSpacer size="m" />

<EuiFlexGroup>
{setupState === AnomalyDetectionSetupState.UpgradeableJobs && (
<>
<UpdateJobsCallout onUpdateComplete={onUpdateComplete} />
<EuiSpacer size="m" />
</>
)}

<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiTitle size="s">
<h2>
Expand All @@ -103,6 +176,30 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
checked={showLegacyJobs}
onChange={(e) => {
setShowLegacyJobs(e.target.checked);
}}
label={i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.showLegacyJobsCheckboxText',
{
defaultMessage: 'Show legacy jobs',
}
)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton href={mlManageJobsHref} color="primary">
{i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.manageMlJobsButtonText',
{
defaultMessage: 'Manage ML Jobs',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill iconType="plusInCircle" onClick={onAddEnvironments}>
{i18n.translate(
Expand All @@ -120,7 +217,8 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
<ManagedTable
noItemsMessage={getNoItemsMessage({ status })}
columns={columns}
items={jobs}
items={filteredJobs}
tableLayout="auto"
/>
<EuiSpacer size="l" />

Expand Down
Loading

0 comments on commit dff3019

Please sign in to comment.