mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Auto-scalable ML node integrations improvements (#112264)
* [ML] Lazy ML node integrations improvements * adding checks to metric and uptime * updating callout * adding callout to security * cleaning up logs changes * further clean up * cleaning up callout code * reverting churn * linting * improvements to bundle size * adding link to cloud * text changes * fixing test * translation id Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cf6bb10bb1
commit
02ca808dc2
24 changed files with 285 additions and 27 deletions
|
@ -25,6 +25,7 @@ import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
|||
import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
|
||||
import { AnomalyDetectionApiResponse } from './index';
|
||||
import { LegacyJobsCallout } from './legacy_jobs_callout';
|
||||
import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public';
|
||||
|
||||
type Jobs = AnomalyDetectionApiResponse['jobs'];
|
||||
|
||||
|
@ -67,6 +68,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<MLJobsAwaitingNodeWarning jobIds={jobs.map((j) => j.job_id)} />
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText"
|
||||
|
|
|
@ -12,6 +12,7 @@ export type JobStatus =
|
|||
| 'initializing'
|
||||
| 'stopped'
|
||||
| 'started'
|
||||
| 'starting'
|
||||
| 'finished'
|
||||
| 'failed';
|
||||
|
||||
|
@ -35,10 +36,10 @@ export type SetupStatus =
|
|||
* before this state was reached.
|
||||
*/
|
||||
export const isJobStatusWithResults = (jobStatus: JobStatus) =>
|
||||
['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
|
||||
['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus);
|
||||
|
||||
export const isHealthyJobStatus = (jobStatus: JobStatus) =>
|
||||
['started', 'finished'].includes(jobStatus);
|
||||
['started', 'starting', 'finished'].includes(jobStatus);
|
||||
|
||||
/**
|
||||
* Maps a setup status to the possibility that results have already been
|
||||
|
|
|
@ -12,6 +12,7 @@ export type JobStatus =
|
|||
| 'initializing'
|
||||
| 'stopped'
|
||||
| 'started'
|
||||
| 'starting'
|
||||
| 'finished'
|
||||
| 'failed';
|
||||
|
||||
|
@ -35,10 +36,10 @@ export type SetupStatus =
|
|||
* before this state was reached.
|
||||
*/
|
||||
export const isJobStatusWithResults = (jobStatus: JobStatus) =>
|
||||
['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
|
||||
['started', 'starting', 'finished', 'stopped', 'failed'].includes(jobStatus);
|
||||
|
||||
export const isHealthyJobStatus = (jobStatus: JobStatus) =>
|
||||
['started', 'finished'].includes(jobStatus);
|
||||
['started', 'starting', 'finished'].includes(jobStatus);
|
||||
|
||||
/**
|
||||
* Maps a setup status to the possibility that results have already been
|
||||
|
|
|
@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf<typeof fetchJobStatusReques
|
|||
|
||||
const datafeedStateRT = rt.keyof({
|
||||
started: null,
|
||||
starting: null,
|
||||
stopped: null,
|
||||
stopping: null,
|
||||
'': null,
|
||||
|
@ -89,6 +90,7 @@ export const jobSummaryRT = rt.intersection([
|
|||
jobState: jobStateRT,
|
||||
}),
|
||||
rt.partial({
|
||||
awaitingNodeAssignment: rt.boolean,
|
||||
datafeedIndices: rt.array(rt.string),
|
||||
datafeedState: datafeedStateRT,
|
||||
fullJob: rt.partial({
|
||||
|
|
|
@ -117,6 +117,7 @@ const datafeedSetupResponseRT = rt.intersection([
|
|||
success: rt.boolean,
|
||||
}),
|
||||
rt.partial({
|
||||
awaitingNodeAssignment: rt.boolean,
|
||||
error: setupErrorResponseRT,
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -99,7 +99,7 @@ const createStatusReducer =
|
|||
{} as Record<JobType, JobStatus>
|
||||
);
|
||||
const nextSetupStatus: SetupStatus = Object.values<JobStatus>(nextJobStatus).every(
|
||||
(jobState) => jobState === 'started'
|
||||
(jobState) => jobState === 'started' || jobState === 'starting'
|
||||
)
|
||||
? { type: 'succeeded' }
|
||||
: {
|
||||
|
@ -224,9 +224,17 @@ const getJobStatus =
|
|||
jobSummary.datafeedState === 'stopped'
|
||||
) {
|
||||
return 'stopped';
|
||||
} else if (jobSummary.jobState === 'opening') {
|
||||
} else if (
|
||||
jobSummary.jobState === 'opening' &&
|
||||
jobSummary.awaitingNodeAssignment === false
|
||||
) {
|
||||
return 'initializing';
|
||||
} else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') {
|
||||
} else if (
|
||||
(jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') ||
|
||||
(jobSummary.jobState === 'opening' &&
|
||||
jobSummary.datafeedState === 'starting' &&
|
||||
jobSummary.awaitingNodeAssignment === true)
|
||||
) {
|
||||
return 'started';
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ export type FetchJobStatusRequestPayload = rt.TypeOf<typeof fetchJobStatusReques
|
|||
|
||||
const datafeedStateRT = rt.keyof({
|
||||
started: null,
|
||||
starting: null,
|
||||
stopped: null,
|
||||
stopping: null,
|
||||
'': null,
|
||||
|
@ -77,6 +78,7 @@ export const jobSummaryRT = rt.intersection([
|
|||
jobState: jobStateRT,
|
||||
}),
|
||||
rt.partial({
|
||||
awaitingNodeAssignment: rt.boolean,
|
||||
datafeedIndices: rt.array(rt.string),
|
||||
datafeedState: datafeedStateRT,
|
||||
fullJob: rt.partial({
|
||||
|
|
|
@ -224,9 +224,17 @@ const getJobStatus =
|
|||
jobSummary.datafeedState === 'stopped'
|
||||
) {
|
||||
return 'stopped';
|
||||
} else if (jobSummary.jobState === 'opening') {
|
||||
} else if (
|
||||
jobSummary.jobState === 'opening' &&
|
||||
jobSummary.awaitingNodeAssignment === false
|
||||
) {
|
||||
return 'initializing';
|
||||
} else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') {
|
||||
} else if (
|
||||
(jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') ||
|
||||
(jobSummary.jobState === 'opening' &&
|
||||
jobSummary.datafeedState === 'starting' &&
|
||||
jobSummary.awaitingNodeAssignment === true)
|
||||
) {
|
||||
return 'started';
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { RecreateJobButton } from '../../../components/logging/log_analysis_setu
|
|||
import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results';
|
||||
import { useMlHref, ML_PAGES } from '../../../../../ml/public';
|
||||
import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector';
|
||||
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
|
||||
|
||||
const JOB_STATUS_POLLING_INTERVAL = 30000;
|
||||
|
||||
|
@ -246,6 +247,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MLJobsAwaitingNodeWarning jobIds={Object.values(jobIds)} />
|
||||
<CategoryJobNoticesSection
|
||||
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
|
||||
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
|
||||
|
|
|
@ -33,6 +33,7 @@ import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_
|
|||
import { isJobStatusWithResults } from '../../../../common/log_analysis';
|
||||
import { LogsPageTemplate } from '../page_template';
|
||||
import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button';
|
||||
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
|
||||
|
||||
export const SORT_DEFAULTS = {
|
||||
direction: 'desc' as const,
|
||||
|
@ -234,6 +235,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{
|
|||
onRecreateMlJobForReconfiguration={showLogEntryRateSetup}
|
||||
onRecreateMlJobForUpdate={showLogEntryRateSetup}
|
||||
/>
|
||||
<MLJobsAwaitingNodeWarning jobIds={jobIds} />
|
||||
<CategoryJobNoticesSection
|
||||
hasOutdatedJobConfigurations={hasOutdatedLogEntryCategoriesJobConfigurations}
|
||||
hasOutdatedJobDefinitions={hasOutdatedLogEntryCategoriesJobDefinitions}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { EuiButtonEmpty } from '@elastic/eui';
|
|||
import moment from 'moment';
|
||||
import { EuiTabs } from '@elastic/eui';
|
||||
import { EuiTab } from '@elastic/eui';
|
||||
import { MLJobsAwaitingNodeWarning } from '../../../../../../../../ml/public';
|
||||
import { SubscriptionSplashPrompt } from '../../../../../../components/subscription_splash_content';
|
||||
import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
|
||||
import {
|
||||
|
@ -120,14 +121,18 @@ export const FlyoutHome = (props: Props) => {
|
|||
|
||||
<EuiFlyoutBody
|
||||
banner={
|
||||
tab === 'jobs' &&
|
||||
hasJobs && (
|
||||
<JobsEnabledCallout
|
||||
hasHostJobs={hostJobSummaries.length > 0}
|
||||
hasK8sJobs={k8sJobSummaries.length > 0}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
)
|
||||
<>
|
||||
{tab === 'jobs' && hasJobs && (
|
||||
<>
|
||||
<JobsEnabledCallout
|
||||
hasHostJobs={hostJobSummaries.length > 0}
|
||||
hasK8sJobs={k8sJobSummaries.length > 0}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<MLJobsAwaitingNodeWarning jobIds={jobIds} />
|
||||
</>
|
||||
}
|
||||
>
|
||||
{tab === 'jobs' && (
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { JobsAwaitingNodeWarning } from './jobs_awaiting_node_warning';
|
||||
export { NewJobAwaitingNodeWarning } from './new_job_awaiting_node';
|
||||
export { MLJobsAwaitingNodeWarning } from './new_job_awaiting_node_shared';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, FC } from 'react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -21,7 +21,7 @@ export const JobsAwaitingNodeWarning: FC<Props> = ({ jobCount }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
@ -43,6 +43,6 @@ export const JobsAwaitingNodeWarning: FC<Props> = ({ jobCount }) => {
|
|||
</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, FC } from 'react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -22,7 +22,7 @@ export const NewJobAwaitingNodeWarning: FC<Props> = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
@ -41,6 +41,6 @@ export const NewJobAwaitingNodeWarning: FC<Props> = () => {
|
|||
</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 { MLJobsAwaitingNodeWarning } from './lazy_loader';
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
|
||||
const MLJobsAwaitingNodeWarningComponent = React.lazy(
|
||||
() => import('./new_job_awaiting_node_shared')
|
||||
);
|
||||
|
||||
interface Props {
|
||||
jobIds: string[];
|
||||
}
|
||||
|
||||
export const MLJobsAwaitingNodeWarning: FC<Props> = ({ jobIds }) => {
|
||||
return (
|
||||
<React.Suspense fallback={<div />}>
|
||||
<MLJobsAwaitingNodeWarningComponent jobIds={jobIds} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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, { FC, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
|
||||
import { EuiCallOut, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { JOB_STATE } from '../../../../../common/constants/states';
|
||||
import { mlApiServicesProvider } from '../../../services/ml_api_service';
|
||||
import { HttpService } from '../../../services/http_service';
|
||||
import { extractDeploymentId, CloudInfo } from '../../../services/ml_server_info';
|
||||
|
||||
interface Props {
|
||||
jobIds: string[];
|
||||
}
|
||||
|
||||
function isJobAwaitingNodeAssignment(job: estypes.MlJobStats) {
|
||||
return job.node === undefined && job.state === JOB_STATE.OPENING;
|
||||
}
|
||||
|
||||
const MLJobsAwaitingNodeWarning: FC<Props> = ({ jobIds }) => {
|
||||
const { http } = useKibana().services;
|
||||
const ml = useMemo(() => mlApiServicesProvider(new HttpService(http!)), [http]);
|
||||
|
||||
const [unassignedJobCount, setUnassignedJobCount] = useState<number>(0);
|
||||
const [cloudInfo, setCloudInfo] = useState<CloudInfo | null>(null);
|
||||
|
||||
const checkNodes = useCallback(async () => {
|
||||
try {
|
||||
if (jobIds.length === 0) {
|
||||
setUnassignedJobCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const { lazyNodeCount } = await ml.mlNodeCount();
|
||||
if (lazyNodeCount === 0) {
|
||||
setUnassignedJobCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const { jobs } = await ml.getJobStats({ jobId: jobIds.join(',') });
|
||||
const unassignedJobs = jobs.filter(isJobAwaitingNodeAssignment);
|
||||
setUnassignedJobCount(unassignedJobs.length);
|
||||
} catch (error) {
|
||||
setUnassignedJobCount(0);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Could not determine ML node information', error);
|
||||
}
|
||||
}, [jobIds]);
|
||||
|
||||
const checkCloudInfo = useCallback(async () => {
|
||||
if (unassignedJobCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await ml.mlInfo();
|
||||
const cloudId = resp.cloudId ?? null;
|
||||
setCloudInfo({
|
||||
isCloud: cloudId !== null,
|
||||
cloudId,
|
||||
deploymentId: cloudId === null ? null : extractDeploymentId(cloudId),
|
||||
});
|
||||
} catch (error) {
|
||||
setCloudInfo(null);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Could not determine cloud information', error);
|
||||
}
|
||||
}, [unassignedJobCount]);
|
||||
|
||||
useEffect(() => {
|
||||
checkCloudInfo();
|
||||
}, [unassignedJobCount]);
|
||||
|
||||
useEffect(() => {
|
||||
checkNodes();
|
||||
}, [jobIds]);
|
||||
|
||||
if (unassignedJobCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.title"
|
||||
defaultMessage="Awaiting machine learning node"
|
||||
/>
|
||||
}
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.noMLNodesAvailableDescription"
|
||||
defaultMessage="There {jobCount, plural, one {is} other {are}} {jobCount, plural, one {# job} other {# jobs}} waiting for machine learning nodes to start."
|
||||
values={{
|
||||
jobCount: unassignedJobCount,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{cloudInfo &&
|
||||
(cloudInfo.isCloud ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.isCloud"
|
||||
defaultMessage="Elastic Cloud deployments can autoscale to add more ML capacity. This may take 5-20 minutes. "
|
||||
/>
|
||||
{cloudInfo.deploymentId === null ? null : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.isCloud.link"
|
||||
defaultMessage="You can monitor progress in the {link}."
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href={`https://cloud.elastic.co/deployments?q=${cloudInfo.deploymentId}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.linkToCloud.linkText"
|
||||
defaultMessage="Elastic Cloud admin console"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.notCloud"
|
||||
defaultMessage="Only Elastic Cloud deployments can autoscale; you must add machine learning nodes. {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink
|
||||
href={
|
||||
'https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-node.html#ml-node'
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.jobsAwaitingNodeWarningShared.linkToCloud.learnMore"
|
||||
defaultMessage="Learn more."
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default MLJobsAwaitingNodeWarning;
|
|
@ -11,6 +11,7 @@ import { MlServerDefaults, MlServerLimits } from '../../../common/types/ml_serve
|
|||
export interface CloudInfo {
|
||||
cloudId: string | null;
|
||||
isCloud: boolean;
|
||||
deploymentId: string | null;
|
||||
}
|
||||
|
||||
let defaults: MlServerDefaults = {
|
||||
|
@ -22,6 +23,7 @@ let limits: MlServerLimits = {};
|
|||
const cloudInfo: CloudInfo = {
|
||||
cloudId: null,
|
||||
isCloud: false,
|
||||
deploymentId: null,
|
||||
};
|
||||
|
||||
export async function loadMlServerInfo() {
|
||||
|
@ -31,6 +33,7 @@ export async function loadMlServerInfo() {
|
|||
limits = resp.limits;
|
||||
cloudInfo.cloudId = resp.cloudId || null;
|
||||
cloudInfo.isCloud = resp.cloudId !== undefined;
|
||||
cloudInfo.deploymentId = !resp.cloudId ? null : extractDeploymentId(resp.cloudId);
|
||||
return { defaults, limits, cloudId: cloudInfo };
|
||||
} catch (error) {
|
||||
return { defaults, limits, cloudId: cloudInfo };
|
||||
|
@ -54,7 +57,7 @@ export function isCloud(): boolean {
|
|||
}
|
||||
|
||||
export function getCloudDeploymentId(): string | null {
|
||||
return cloudInfo.cloudId === null ? null : extractDeploymentId(cloudInfo.cloudId);
|
||||
return cloudInfo.deploymentId;
|
||||
}
|
||||
|
||||
export function extractDeploymentId(cloudId: string) {
|
||||
|
|
|
@ -64,3 +64,5 @@ export const getMlSharedImports = async () => {
|
|||
// Helper to get Type returned by getMlSharedImports.
|
||||
type AwaitReturnType<T> = T extends PromiseLike<infer U> ? U : T;
|
||||
export type GetMlSharedImportsReturnType = AwaitReturnType<ReturnType<typeof getMlSharedImports>>;
|
||||
|
||||
export { MLJobsAwaitingNodeWarning } from './application/components/jobs_awaiting_node_warning/new_job_awaiting_node_shared';
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import moment from 'moment';
|
||||
import React, { Dispatch, useCallback, useReducer, useState } from 'react';
|
||||
import React, { Dispatch, useCallback, useReducer, useState, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
|
@ -30,6 +30,7 @@ import * as i18n from './translations';
|
|||
import { JobsFilters, SecurityJob } from './types';
|
||||
import { UpgradeContents } from './upgrade_contents';
|
||||
import { useSecurityJobs } from './hooks/use_security_jobs';
|
||||
import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public';
|
||||
|
||||
const PopoverContentsDiv = styled.div`
|
||||
max-width: 684px;
|
||||
|
@ -116,6 +117,10 @@ export const MlPopover = React.memo(() => {
|
|||
});
|
||||
|
||||
const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length;
|
||||
const installedJobsIds = useMemo(
|
||||
() => jobs.filter((j) => j.isInstalled).map((j) => j.id),
|
||||
[jobs]
|
||||
);
|
||||
|
||||
if (!isLicensed) {
|
||||
// If the user does not have platinum show upgrade UI
|
||||
|
@ -216,6 +221,7 @@ export const MlPopover = React.memo(() => {
|
|||
</>
|
||||
)}
|
||||
|
||||
<MLJobsAwaitingNodeWarning jobIds={installedJobsIds} />
|
||||
<JobsTable
|
||||
isLoading={isLoadingSecurityJobs || isLoading}
|
||||
jobs={filteredJobs}
|
||||
|
|
|
@ -41,6 +41,7 @@ const showMLJobNotification = (
|
|||
basePath: string,
|
||||
range: { to: string; from: string },
|
||||
success: boolean,
|
||||
awaitingNodeAssignment: boolean,
|
||||
error?: Error
|
||||
) => {
|
||||
if (success) {
|
||||
|
@ -51,7 +52,9 @@ const showMLJobNotification = (
|
|||
),
|
||||
text: toMountPoint(
|
||||
<p>
|
||||
{labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||
{awaitingNodeAssignment
|
||||
? labels.JOB_CREATED_LAZY_SUCCESS_MESSAGE
|
||||
: labels.JOB_CREATED_SUCCESS_MESSAGE}
|
||||
<MLJobLink monitorId={monitorId} basePath={basePath} dateRange={range}>
|
||||
{labels.VIEW_JOB}
|
||||
</MLJobLink>
|
||||
|
@ -107,7 +110,8 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
|||
monitorId as string,
|
||||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
true
|
||||
true,
|
||||
hasMLJob.awaitingNodeAssignment
|
||||
);
|
||||
const loadMLJob = (jobId: string) =>
|
||||
dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string }));
|
||||
|
@ -123,6 +127,7 @@ export const MachineLearningFlyout: React.FC<Props> = ({ onClose }) => {
|
|||
basePath,
|
||||
{ to: dateRangeEnd, from: dateRangeStart },
|
||||
false,
|
||||
false,
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,14 @@ export const JOB_CREATED_SUCCESS_MESSAGE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const JOB_CREATED_LAZY_SUCCESS_MESSAGE = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreatedLazyNotificationText',
|
||||
{
|
||||
defaultMessage:
|
||||
'The analysis is waiting for an ML node to become available. It might take a while before results are added to the response times graph.',
|
||||
}
|
||||
);
|
||||
|
||||
export const JOB_CREATION_FAILED = i18n.translate(
|
||||
'xpack.uptime.ml.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle',
|
||||
{
|
||||
|
|
|
@ -48,6 +48,7 @@ export interface MonitorDetailsActionPayload {
|
|||
export interface CreateMLJobSuccess {
|
||||
count: number;
|
||||
jobId: string;
|
||||
awaitingNodeAssignment: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteJobResults {
|
||||
|
|
|
@ -57,10 +57,12 @@ export const createMLJob = async ({
|
|||
const response: DataRecognizerConfigResponse = await apiService.post(url, data);
|
||||
if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) {
|
||||
const jobResponse = response.jobs[0];
|
||||
const datafeedResponse = response.datafeeds[0];
|
||||
if (jobResponse.success) {
|
||||
return {
|
||||
count: 1,
|
||||
jobId: jobResponse.id,
|
||||
awaitingNodeAssignment: datafeedResponse.awaitingMlNodeAllocation === true,
|
||||
};
|
||||
} else {
|
||||
const { error } = jobResponse;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue