[ML] Fixing cloud behaviour when ML nodes are lazily assigned (#90051)

* [ML] Fixing cloud behaviour when ML nodes are lazily assigned

* fixing duplicate id

* updating snapshot

* removing comments

* fixing tests

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2021-02-03 17:01:05 +00:00 committed by GitHub
parent d00266b3ff
commit 501f763713
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 386 additions and 71 deletions

View file

@ -32,6 +32,7 @@ export interface MlSummaryJob {
deleting?: boolean; deleting?: boolean;
latestTimestampSortValue?: number; latestTimestampSortValue?: number;
earliestStartTimestampMs?: number; earliestStartTimestampMs?: number;
awaitingNodeAssignment: boolean;
} }
export interface AuditMessage { export interface AuditMessage {

View file

@ -31,3 +31,8 @@ export interface MlInfoResponse {
upgrade_mode: boolean; upgrade_mode: boolean;
cloudId?: string; cloudId?: string;
} }
export interface MlNodeCount {
count: number;
lazyNodeCount: number;
}

View file

@ -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;
* you may not use this file except in compliance with the Elastic License.
*/
export { JobsAwaitingNodeWarning } from './jobs_awaiting_node_warning';
export { NewJobAwaitingNodeWarning } from './new_job_awaiting_node';

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { isCloud } from '../../services/ml_server_info';
interface Props {
jobCount: number;
}
export const JobsAwaitingNodeWarning: FC<Props> = ({ jobCount }) => {
if (isCloud() === false || jobCount === 0) {
return null;
}
return (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.ml.jobsAwaitingNodeWarning.title"
defaultMessage="Awaiting ML node provisioning"
/>
}
color="primary"
iconType="iInCircle"
>
<div>
<FormattedMessage
id="xpack.ml.jobsAwaitingNodeWarning.noMLNodesAvailableDescription"
defaultMessage="There {jobCount, plural, one {is} other {are}} {jobCount, plural, one {# job} other {# jobs}} waiting to be started while ML nodes are being provisioned."
values={{
jobCount,
}}
/>
</div>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
);
};

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC } from 'react';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { JobType } from '../../../../common/types/saved_objects';
interface Props {
jobType: JobType;
}
export const NewJobAwaitingNodeWarning: FC<Props> = () => {
return (
<Fragment>
<EuiCallOut
title={
<FormattedMessage
id="xpack.ml.jobsAwaitingNodeWarning.title"
defaultMessage="Awaiting ML node provisioning"
/>
}
color="primary"
iconType="iInCircle"
>
<div>
<FormattedMessage
id="xpack.ml.newJobAwaitingNodeWarning.noMLNodesAvailableDescription"
defaultMessage="Job cannot be started straight away, an ML node needs to be started. This will happen automatically."
/>
</div>
</EuiCallOut>
<EuiSpacer size="m" />
</Fragment>
);
};

View file

@ -41,7 +41,7 @@ export const NodeAvailableWarning: FC = () => {
defaultMessage="You will not be able to create or run jobs." defaultMessage="You will not be able to create or run jobs."
/> />
</div> </div>
{isCloud && id !== null && ( {isCloud() && id !== null && (
<div> <div>
<FormattedMessage <FormattedMessage
id="xpack.ml.jobsList.nodeAvailableWarning.linkToCloudDescription" id="xpack.ml.jobsList.nodeAvailableWarning.linkToCloudDescription"

View file

@ -19,6 +19,7 @@ import { BackToListPanel } from '../back_to_list_panel';
import { ViewResultsPanel } from '../view_results_panel'; import { ViewResultsPanel } from '../view_results_panel';
import { ProgressStats } from './progress_stats'; import { ProgressStats } from './progress_stats';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
import { NewJobAwaitingNodeWarning } from '../../../../../components/jobs_awaiting_node_warning';
export const PROGRESS_REFRESH_INTERVAL_MS = 1000; export const PROGRESS_REFRESH_INTERVAL_MS = 1000;
@ -41,6 +42,7 @@ export const CreateStepFooter: FC<Props> = ({ jobId, jobType, showProgress }) =>
const [currentProgress, setCurrentProgress] = useState<AnalyticsProgressStats | undefined>( const [currentProgress, setCurrentProgress] = useState<AnalyticsProgressStats | undefined>(
undefined undefined
); );
const [showJobAssignWarning, setShowJobAssignWarning] = useState(false);
const { const {
services: { notifications }, services: { notifications },
@ -58,6 +60,10 @@ export const CreateStepFooter: FC<Props> = ({ jobId, jobType, showProgress }) =>
? analyticsStats.data_frame_analytics[0] ? analyticsStats.data_frame_analytics[0]
: undefined; : undefined;
setShowJobAssignWarning(
jobStats?.state === DATA_FRAME_TASK_STATE.STARTING && jobStats?.node === undefined
);
if (jobStats !== undefined) { if (jobStats !== undefined) {
const progressStats = getDataFrameAnalyticsProgressPhase(jobStats); const progressStats = getDataFrameAnalyticsProgressPhase(jobStats);
@ -106,25 +112,28 @@ export const CreateStepFooter: FC<Props> = ({ jobId, jobType, showProgress }) =>
}, [initialized]); }, [initialized]);
return ( return (
<EuiFlexGroup direction="column"> <>
<EuiFlexItem grow={false}> {showJobAssignWarning && <NewJobAwaitingNodeWarning jobType="data-frame-analytics" />}
{showProgress && ( <EuiFlexGroup direction="column">
<ProgressStats currentProgress={currentProgress} failedJobMessage={failedJobMessage} /> <EuiFlexItem grow={false}>
)} {showProgress && (
</EuiFlexItem> <ProgressStats currentProgress={currentProgress} failedJobMessage={failedJobMessage} />
<EuiFlexItem grow={false}>
<EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<BackToListPanel />
</EuiFlexItem>
{jobFinished === true && (
<EuiFlexItem grow={false}>
<ViewResultsPanel jobId={jobId} analysisType={jobType} />
</EuiFlexItem>
)} )}
</EuiFlexGroup> </EuiFlexItem>
</EuiFlexItem> <EuiFlexItem grow={false}>
</EuiFlexGroup> <EuiHorizontalRule />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<BackToListPanel />
</EuiFlexItem>
{jobFinished === true && (
<EuiFlexItem grow={false}>
<ViewResultsPanel jobId={jobId} analysisType={jobType} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</>
); );
}; };

View file

@ -36,6 +36,7 @@ import { AnalyticsEmptyPrompt } from './empty_prompt';
import { useTableSettings } from './use_table_settings'; import { useTableSettings } from './use_table_settings';
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
import { ListingPageUrlState } from '../../../../../../../common/types/common'; import { ListingPageUrlState } from '../../../../../../../common/types/common';
import { JobsAwaitingNodeWarning } from '../../../../../components/jobs_awaiting_node_warning';
const filters: EuiSearchBarProps['filters'] = [ const filters: EuiSearchBarProps['filters'] = [
{ {
@ -114,6 +115,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
); );
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]); const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
const [errorMessage, setErrorMessage] = useState<any>(undefined); const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0);
const disabled = const disabled =
!checkPermission('canCreateDataFrameAnalytics') || !checkPermission('canCreateDataFrameAnalytics') ||
@ -124,6 +126,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
setAnalyticsStats, setAnalyticsStats,
setErrorMessage, setErrorMessage,
setIsInitialized, setIsInitialized,
setJobsAwaitingNodeCount,
blockRefresh, blockRefresh,
isManagementTable isManagementTable
); );
@ -261,6 +264,7 @@ export const DataFrameAnalyticsList: FC<Props> = ({
<div data-test-subj="mlAnalyticsJobList"> <div data-test-subj="mlAnalyticsJobList">
{modals} {modals}
{!isManagementTable && <EuiSpacer size="m" />} {!isManagementTable && <EuiSpacer size="m" />}
<JobsAwaitingNodeWarning jobCount={jobsAwaitingNodeCount} />
<EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexGroup justifyContent="spaceBetween">
{!isManagementTable && stats} {!isManagementTable && stats}
{isManagementTable && managementStats} {isManagementTable && managementStats}

View file

@ -26,6 +26,7 @@ import {
} from '../../components/analytics_list/common'; } from '../../components/analytics_list/common';
import { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; import { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
import { DATA_FRAME_TASK_STATE } from '../../../../../../../common/constants/data_frame_analytics';
export const isGetDataFrameAnalyticsStatsResponseOk = ( export const isGetDataFrameAnalyticsStatsResponseOk = (
arg: any arg: any
@ -106,6 +107,7 @@ export const getAnalyticsFactory = (
React.SetStateAction<GetDataFrameAnalyticsStatsResponseError | undefined> React.SetStateAction<GetDataFrameAnalyticsStatsResponseError | undefined>
>, >,
setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>, setIsInitialized: React.Dispatch<React.SetStateAction<boolean>>,
setJobsAwaitingNodeCount: React.Dispatch<React.SetStateAction<number>>,
blockRefresh: boolean, blockRefresh: boolean,
isManagementTable: boolean isManagementTable: boolean
): GetAnalytics => { ): GetAnalytics => {
@ -134,6 +136,8 @@ export const getAnalyticsFactory = (
? getAnalyticsJobsStats(analyticsStats) ? getAnalyticsJobsStats(analyticsStats)
: undefined; : undefined;
let jobsAwaitingNodeCount = 0;
const tableRows = analyticsConfigs.data_frame_analytics.reduce( const tableRows = analyticsConfigs.data_frame_analytics.reduce(
(reducedtableRows, config) => { (reducedtableRows, config) => {
const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
@ -146,6 +150,10 @@ export const getAnalyticsFactory = (
return reducedtableRows; return reducedtableRows;
} }
if (stats.state === DATA_FRAME_TASK_STATE.STARTING && stats.node === undefined) {
jobsAwaitingNodeCount++;
}
// Table with expandable rows requires `id` on the outer most level // Table with expandable rows requires `id` on the outer most level
reducedtableRows.push({ reducedtableRows.push({
checkpointing: {}, checkpointing: {},
@ -166,6 +174,7 @@ export const getAnalyticsFactory = (
setAnalyticsStats(analyticsStatsResult); setAnalyticsStats(analyticsStatsResult);
setErrorMessage(undefined); setErrorMessage(undefined);
setIsInitialized(true); setIsInitialized(true);
setJobsAwaitingNodeCount(jobsAwaitingNodeCount);
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE); refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE);
} catch (e) { } catch (e) {
// An error is followed immediately by setting the state to idle. // An error is followed immediately by setting the state to idle.

View file

@ -32,6 +32,7 @@ import { MultiJobActions } from '../multi_job_actions';
import { NewJobButton } from '../new_job_button'; import { NewJobButton } from '../new_job_button';
import { JobStatsBar } from '../jobs_stats_bar'; import { JobStatsBar } from '../jobs_stats_bar';
import { NodeAvailableWarning } from '../../../../components/node_available_warning'; import { NodeAvailableWarning } from '../../../../components/node_available_warning';
import { JobsAwaitingNodeWarning } from '../../../../components/jobs_awaiting_node_warning';
import { SavedObjectsWarning } from '../../../../components/saved_objects_warning'; import { SavedObjectsWarning } from '../../../../components/saved_objects_warning';
import { DatePickerWrapper } from '../../../../components/navigation_menu/date_picker_wrapper'; import { DatePickerWrapper } from '../../../../components/navigation_menu/date_picker_wrapper';
import { UpgradeWarning } from '../../../../components/upgrade'; import { UpgradeWarning } from '../../../../components/upgrade';
@ -56,6 +57,7 @@ export class JobsListView extends Component {
itemIdToExpandedRowMap: {}, itemIdToExpandedRowMap: {},
filterClauses: [], filterClauses: [],
deletingJobIds: [], deletingJobIds: [],
jobsAwaitingNodeCount: 0,
}; };
this.spacesEnabled = props.spacesEnabled ?? false; this.spacesEnabled = props.spacesEnabled ?? false;
@ -272,6 +274,7 @@ export class JobsListView extends Component {
spaces = allSpaces['anomaly-detector']; spaces = allSpaces['anomaly-detector'];
} }
let jobsAwaitingNodeCount = 0;
const jobs = await ml.jobs.jobsSummary(expandedJobsIds); const jobs = await ml.jobs.jobsSummary(expandedJobsIds);
const fullJobsList = {}; const fullJobsList = {};
const jobsSummaryList = jobs.map((job) => { const jobsSummaryList = jobs.map((job) => {
@ -287,11 +290,21 @@ export class JobsListView extends Component {
spaces[job.id] !== undefined spaces[job.id] !== undefined
? spaces[job.id] ? spaces[job.id]
: []; : [];
if (job.awaitingNodeAssignment === true) {
jobsAwaitingNodeCount++;
}
return job; return job;
}); });
const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses); const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses);
this.setState( this.setState(
{ jobsSummaryList, filteredJobsSummaryList, fullJobsList, loading: false }, {
jobsSummaryList,
filteredJobsSummaryList,
fullJobsList,
loading: false,
jobsAwaitingNodeCount,
},
() => { () => {
this.refreshSelectedJobs(); this.refreshSelectedJobs();
} }
@ -407,7 +420,7 @@ export class JobsListView extends Component {
} }
renderJobsListComponents() { renderJobsListComponents() {
const { isRefreshing, loading, jobsSummaryList } = this.state; const { isRefreshing, loading, jobsSummaryList, jobsAwaitingNodeCount } = this.state;
const jobIds = jobsSummaryList.map((j) => j.id); const jobIds = jobsSummaryList.map((j) => j.id);
return ( return (
@ -440,6 +453,7 @@ export class JobsListView extends Component {
</EuiPageHeader> </EuiPageHeader>
<NodeAvailableWarning /> <NodeAvailableWarning />
<JobsAwaitingNodeWarning jobCount={jobsAwaitingNodeCount} />
<SavedObjectsWarning jobType="anomaly-detector" /> <SavedObjectsWarning jobType="anomaly-detector" />
<UpgradeWarning /> <UpgradeWarning />

View file

@ -11,12 +11,14 @@ import { JobCreator } from '../job_creator';
import { DatafeedId, JobId } from '../../../../../../common/types/anomaly_detection_jobs'; import { DatafeedId, JobId } from '../../../../../../common/types/anomaly_detection_jobs';
import { DATAFEED_STATE } from '../../../../../../common/constants/states'; import { DATAFEED_STATE } from '../../../../../../common/constants/states';
const REFRESH_INTERVAL_MS = 100; const REFRESH_INTERVAL_MS = 250;
const NODE_ASSIGNMENT_CHECK_REFRESH_INTERVAL_MS = 2000;
const TARGET_PROGRESS_DELTA = 2; const TARGET_PROGRESS_DELTA = 2;
const REFRESH_RATE_ADJUSTMENT_DELAY_MS = 2000; const REFRESH_RATE_ADJUSTMENT_DELAY_MS = 2000;
type Progress = number; type Progress = number;
export type ProgressSubscriber = (progress: number) => void; export type ProgressSubscriber = (progress: number) => void;
export type JobAssignmentSubscriber = (assigned: boolean) => void;
export class JobRunner { export class JobRunner {
private _jobId: JobId; private _jobId: JobId;
@ -35,6 +37,8 @@ export class JobRunner {
private _datafeedStartTime: number = 0; private _datafeedStartTime: number = 0;
private _performRefreshRateAdjustment: boolean = false; private _performRefreshRateAdjustment: boolean = false;
private _jobAssignedToNode: boolean = false;
private _jobAssignedToNode$: BehaviorSubject<boolean>;
constructor(jobCreator: JobCreator) { constructor(jobCreator: JobCreator) {
this._jobId = jobCreator.jobId; this._jobId = jobCreator.jobId;
@ -45,6 +49,7 @@ export class JobRunner {
this._stopRefreshPoll = jobCreator.stopAllRefreshPolls; this._stopRefreshPoll = jobCreator.stopAllRefreshPolls;
this._progress$ = new BehaviorSubject(this._percentageComplete); this._progress$ = new BehaviorSubject(this._percentageComplete);
this._jobAssignedToNode$ = new BehaviorSubject(this._jobAssignedToNode);
this._subscribers = jobCreator.subscribers; this._subscribers = jobCreator.subscribers;
} }
@ -62,7 +67,9 @@ export class JobRunner {
private async openJob(): Promise<void> { private async openJob(): Promise<void> {
try { try {
await mlJobService.openJob(this._jobId); const { node }: { node?: string } = await mlJobService.openJob(this._jobId);
this._jobAssignedToNode = node !== undefined && node.length > 0;
this._jobAssignedToNode$.next(this._jobAssignedToNode);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -94,7 +101,7 @@ export class JobRunner {
this._datafeedState = DATAFEED_STATE.STARTED; this._datafeedState = DATAFEED_STATE.STARTED;
this._percentageComplete = 0; this._percentageComplete = 0;
const check = async () => { const checkProgress = async () => {
const { isRunning, progress: prog, isJobClosed } = await this.getProgress(); const { isRunning, progress: prog, isJobClosed } = await this.getProgress();
// if the progress has reached 100% but the job is still running, // if the progress has reached 100% but the job is still running,
@ -109,7 +116,9 @@ export class JobRunner {
if ((isRunning === true || isJobClosed === false) && this._stopRefreshPoll.stop === false) { if ((isRunning === true || isJobClosed === false) && this._stopRefreshPoll.stop === false) {
setTimeout(async () => { setTimeout(async () => {
await check(); if (this._stopRefreshPoll.stop === false) {
await checkProgress();
}
}, this._refreshInterval); }, this._refreshInterval);
} else { } else {
// job has finished running, set progress to 100% // job has finished running, set progress to 100%
@ -121,11 +130,30 @@ export class JobRunner {
subscriptions.forEach((s) => s.unsubscribe()); subscriptions.forEach((s) => s.unsubscribe());
} }
}; };
const checkJobIsAssigned = async () => {
this._jobAssignedToNode = await this._isJobAssigned();
this._jobAssignedToNode$.next(this._jobAssignedToNode);
if (this._jobAssignedToNode === true) {
await checkProgress();
} else {
setTimeout(async () => {
if (this._stopRefreshPoll.stop === false) {
await checkJobIsAssigned();
}
}, NODE_ASSIGNMENT_CHECK_REFRESH_INTERVAL_MS);
}
};
// wait for the first check to run and then return success. // wait for the first check to run and then return success.
// all subsequent checks will update the observable // all subsequent checks will update the observable
if (pollProgress === true) { if (pollProgress === true) {
await check(); if (this._jobAssignedToNode === true) {
await checkProgress();
} else {
await checkJobIsAssigned();
}
} }
return started; return started;
} catch (error) { } catch (error) {
throw error; throw error;
@ -159,6 +187,11 @@ export class JobRunner {
} }
} }
private async _isJobAssigned(): Promise<boolean> {
const { jobs } = await ml.getJobStats({ jobId: this._jobId });
return jobs.length > 0 && jobs[0].node !== undefined;
}
public async startDatafeed() { public async startDatafeed() {
return await this._startDatafeed(this._start, this._end, true); return await this._startDatafeed(this._start, this._end, true);
} }
@ -185,4 +218,12 @@ export class JobRunner {
const { isRunning } = await this.getProgress(); const { isRunning } = await this.getProgress();
return isRunning; return isRunning;
} }
public isJobAssignedToNode() {
return this._jobAssignedToNode;
}
public subscribeToJobAssignment(func: JobAssignmentSubscriber) {
return this._jobAssignedToNode$.subscribe(func);
}
} }

View file

@ -5,6 +5,7 @@
*/ */
import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; import React, { Fragment, FC, useContext, useState, useEffect } from 'react';
import { Subscription } from 'rxjs';
import { import {
EuiButton, EuiButton,
EuiButtonEmpty, EuiButtonEmpty,
@ -29,6 +30,7 @@ import { DetectorChart } from './components/detector_chart';
import { JobProgress } from './components/job_progress'; import { JobProgress } from './components/job_progress';
import { PostSaveOptions } from './components/post_save_options'; import { PostSaveOptions } from './components/post_save_options';
import { StartDatafeedSwitch } from './components/start_datafeed_switch'; import { StartDatafeedSwitch } from './components/start_datafeed_switch';
import { NewJobAwaitingNodeWarning } from '../../../../../components/jobs_awaiting_node_warning';
import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service'; import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service';
import { import {
convertToAdvancedJob, convertToAdvancedJob,
@ -55,6 +57,7 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
const [isValid, setIsValid] = useState(jobValidator.validationSummary.basic); const [isValid, setIsValid] = useState(jobValidator.validationSummary.basic);
const [jobRunner, setJobRunner] = useState<JobRunner | null>(null); const [jobRunner, setJobRunner] = useState<JobRunner | null>(null);
const [startDatafeed, setStartDatafeed] = useState(true); const [startDatafeed, setStartDatafeed] = useState(true);
const [showJobAssignWarning, setShowJobAssignWarning] = useState(false);
const isAdvanced = isAdvancedJobCreator(jobCreator); const isAdvanced = isAdvancedJobCreator(jobCreator);
const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY; const jsonEditorMode = isAdvanced ? EDITOR_MODE.EDITABLE : EDITOR_MODE.READONLY;
@ -63,6 +66,20 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
jobCreator.subscribeToProgress(setProgress); jobCreator.subscribeToProgress(setProgress);
}, []); }, []);
useEffect(() => {
let s: Subscription | null = null;
if (jobRunner !== null) {
s = jobRunner.subscribeToJobAssignment((assigned: boolean) =>
setShowJobAssignWarning(!assigned)
);
}
return () => {
if (s !== null) {
s?.unsubscribe();
}
};
}, [jobRunner]);
async function start() { async function start() {
setCreatingJob(true); setCreatingJob(true);
if (isAdvanced) { if (isAdvanced) {
@ -155,6 +172,7 @@ export const SummaryStep: FC<StepProps> = ({ setCurrentStep, isCurrentStep }) =>
)} )}
<EuiHorizontalRule /> <EuiHorizontalRule />
{showJobAssignWarning && <NewJobAwaitingNodeWarning jobType="anomaly-detector" />}
<EuiFlexGroup> <EuiFlexGroup>
{progress < 100 && ( {progress < 100 && (
<Fragment> <Fragment>

View file

@ -5,14 +5,16 @@
*/ */
import { ml } from '../services/ml_api_service'; import { ml } from '../services/ml_api_service';
import { MlNodeCount } from '../../../common/types/ml_server_info';
let mlNodeCount: number = 0; let mlNodeCount: number = 0;
let lazyMlNodeCount: number = 0;
let userHasPermissionToViewMlNodeCount: boolean = false; let userHasPermissionToViewMlNodeCount: boolean = false;
export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise<void>) { export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise<void>) {
try { try {
const nodes = await getMlNodeCount(); const { count, lazyNodeCount } = await getMlNodeCount();
if (nodes.count !== undefined && nodes.count > 0) { if (count > 0 || lazyNodeCount > 0) {
Promise.resolve(); Promise.resolve();
} else { } else {
throw Error('Cannot load count of ML nodes'); throw Error('Cannot load count of ML nodes');
@ -25,23 +27,24 @@ export async function checkMlNodesAvailable(redirectToJobsManagementPage: () =>
} }
} }
export async function getMlNodeCount() { export async function getMlNodeCount(): Promise<MlNodeCount> {
try { try {
const nodes = await ml.mlNodeCount(); const nodes = await ml.mlNodeCount();
mlNodeCount = nodes.count; mlNodeCount = nodes.count;
lazyMlNodeCount = nodes.lazyNodeCount;
userHasPermissionToViewMlNodeCount = true; userHasPermissionToViewMlNodeCount = true;
return Promise.resolve(nodes); return nodes;
} catch (error) { } catch (error) {
mlNodeCount = 0; mlNodeCount = 0;
if (error.statusCode === 403) { if (error.statusCode === 403) {
userHasPermissionToViewMlNodeCount = false; userHasPermissionToViewMlNodeCount = false;
} }
return Promise.resolve({ count: 0 }); return { count: 0, lazyNodeCount: 0 };
} }
} }
export function mlNodesAvailable() { export function mlNodesAvailable() {
return mlNodeCount !== 0; return mlNodeCount !== 0 || lazyMlNodeCount !== 0;
} }
export function permissionToViewMlNodeCount() { export function permissionToViewMlNodeCount() {

View file

@ -28,8 +28,9 @@ import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
interface Props { interface Props {
jobCreationDisabled: boolean; jobCreationDisabled: boolean;
setLazyJobCount: React.Dispatch<React.SetStateAction<number>>;
} }
export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => { export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled, setLazyJobCount }) => {
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]); const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>( const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
undefined undefined
@ -52,6 +53,7 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
setAnalyticsStats, setAnalyticsStats,
setErrorMessage, setErrorMessage,
setIsInitialized, setIsInitialized,
setLazyJobCount,
false, false,
false false
); );

View file

@ -51,9 +51,10 @@ function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup {
interface Props { interface Props {
jobCreationDisabled: boolean; jobCreationDisabled: boolean;
setLazyJobCount: React.Dispatch<React.SetStateAction<number>>;
} }
export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => { export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled, setLazyJobCount }) => {
const { const {
services: { notifications }, services: { notifications },
} = useMlKibana(); } = useMlKibana();
@ -84,10 +85,14 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
const loadJobs = async () => { const loadJobs = async () => {
setIsLoading(true); setIsLoading(true);
let lazyJobCount = 0;
try { try {
const jobsResult: MlSummaryJobs = await ml.jobs.jobsSummary([]); const jobsResult: MlSummaryJobs = await ml.jobs.jobsSummary([]);
const jobsSummaryList = jobsResult.map((job: MlSummaryJob) => { const jobsSummaryList = jobsResult.map((job: MlSummaryJob) => {
job.latestTimestampSortValue = job.latestTimestampMs || 0; job.latestTimestampSortValue = job.latestTimestampMs || 0;
if (job.awaitingNodeAssignment) {
lazyJobCount++;
}
return job; return job;
}); });
const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList); const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList);
@ -100,6 +105,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => {
setGroups(jobsGroups); setGroups(jobsGroups);
setJobsList(jobsWithTimerange); setJobsList(jobsWithTimerange);
loadMaxAnomalyScores(jobsGroups); loadMaxAnomalyScores(jobsGroups);
setLazyJobCount(lazyJobCount);
} catch (e) { } catch (e) {
setErrorMessage(e.message !== undefined ? e.message : JSON.stringify(e)); setErrorMessage(e.message !== undefined ? e.message : JSON.stringify(e));
setIsLoading(false); setIsLoading(false);

View file

@ -12,20 +12,30 @@ import { AnalyticsPanel } from './analytics_panel';
interface Props { interface Props {
createAnomalyDetectionJobDisabled: boolean; createAnomalyDetectionJobDisabled: boolean;
createAnalyticsJobDisabled: boolean; createAnalyticsJobDisabled: boolean;
setAdLazyJobCount: React.Dispatch<React.SetStateAction<number>>;
setDfaLazyJobCount: React.Dispatch<React.SetStateAction<number>>;
} }
// Fetch jobs and determine what to show // Fetch jobs and determine what to show
export const OverviewContent: FC<Props> = ({ export const OverviewContent: FC<Props> = ({
createAnomalyDetectionJobDisabled, createAnomalyDetectionJobDisabled,
createAnalyticsJobDisabled, createAnalyticsJobDisabled,
setAdLazyJobCount,
setDfaLazyJobCount,
}) => ( }) => (
<EuiFlexItem grow={3}> <EuiFlexItem grow={3}>
<EuiFlexGroup direction="column"> <EuiFlexGroup direction="column">
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<AnomalyDetectionPanel jobCreationDisabled={createAnomalyDetectionJobDisabled} /> <AnomalyDetectionPanel
jobCreationDisabled={createAnomalyDetectionJobDisabled}
setLazyJobCount={setAdLazyJobCount}
/>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<AnalyticsPanel jobCreationDisabled={createAnalyticsJobDisabled} /> <AnalyticsPanel
jobCreationDisabled={createAnalyticsJobDisabled}
setLazyJobCount={setDfaLazyJobCount}
/>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
</EuiFlexItem> </EuiFlexItem>

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import React, { Fragment, FC } from 'react'; import React, { Fragment, FC, useState } from 'react';
import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui';
import { checkPermission } from '../capabilities/check_capabilities'; import { checkPermission } from '../capabilities/check_capabilities';
import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes';
@ -12,6 +12,7 @@ import { NavigationMenu } from '../components/navigation_menu';
import { OverviewSideBar } from './components/sidebar'; import { OverviewSideBar } from './components/sidebar';
import { OverviewContent } from './components/content'; import { OverviewContent } from './components/content';
import { NodeAvailableWarning } from '../components/node_available_warning'; import { NodeAvailableWarning } from '../components/node_available_warning';
import { JobsAwaitingNodeWarning } from '../components/jobs_awaiting_node_warning';
import { SavedObjectsWarning } from '../components/saved_objects_warning'; import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { UpgradeWarning } from '../components/upgrade'; import { UpgradeWarning } from '../components/upgrade';
import { HelpMenu } from '../components/help_menu'; import { HelpMenu } from '../components/help_menu';
@ -27,12 +28,17 @@ export const OverviewPage: FC = () => {
services: { docLinks }, services: { docLinks },
} = useMlKibana(); } = useMlKibana();
const helpLink = docLinks.links.ml.guide; const helpLink = docLinks.links.ml.guide;
const [adLazyJobCount, setAdLazyJobCount] = useState(0);
const [dfaLazyJobCount, setDfaLazyJobCount] = useState(0);
return ( return (
<Fragment> <Fragment>
<NavigationMenu tabId="overview" /> <NavigationMenu tabId="overview" />
<EuiPage data-test-subj="mlPageOverview"> <EuiPage data-test-subj="mlPageOverview">
<EuiPageBody> <EuiPageBody>
<NodeAvailableWarning /> <NodeAvailableWarning />
<JobsAwaitingNodeWarning jobCount={adLazyJobCount + dfaLazyJobCount} />
<SavedObjectsWarning /> <SavedObjectsWarning />
<UpgradeWarning /> <UpgradeWarning />
@ -41,6 +47,8 @@ export const OverviewPage: FC = () => {
<OverviewContent <OverviewContent
createAnomalyDetectionJobDisabled={disableCreateAnomalyDetectionJob} createAnomalyDetectionJobDisabled={disableCreateAnomalyDetectionJob}
createAnalyticsJobDisabled={disableCreateAnalyticsButton} createAnalyticsJobDisabled={disableCreateAnalyticsButton}
setAdLazyJobCount={setAdLazyJobCount}
setDfaLazyJobCount={setDfaLazyJobCount}
/> />
</EuiFlexGroup> </EuiFlexGroup>
</EuiPageBody> </EuiPageBody>

View file

@ -15,12 +15,17 @@ import { resultsApiProvider } from './results';
import { jobsApiProvider } from './jobs'; import { jobsApiProvider } from './jobs';
import { fileDatavisualizer } from './datavisualizer'; import { fileDatavisualizer } from './datavisualizer';
import { savedObjectsApiProvider } from './saved_objects'; import { savedObjectsApiProvider } from './saved_objects';
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; import {
MlServerDefaults,
MlServerLimits,
MlNodeCount,
} from '../../../../common/types/ml_server_info';
import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { MlCapabilitiesResponse } from '../../../../common/types/capabilities';
import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars';
import { import {
Job, Job,
JobStats,
Datafeed, Datafeed,
CombinedJob, CombinedJob,
Detector, Detector,
@ -116,14 +121,14 @@ export function mlApiServicesProvider(httpService: HttpService) {
return { return {
getJobs(obj?: { jobId?: string }) { getJobs(obj?: { jobId?: string }) {
const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
return httpService.http<any>({ return httpService.http<{ jobs: Job[]; count: number }>({
path: `${basePath()}/anomaly_detectors${jobId}`, path: `${basePath()}/anomaly_detectors${jobId}`,
}); });
}, },
getJobStats(obj: { jobId?: string }) { getJobStats(obj: { jobId?: string }) {
const jobId = obj && obj.jobId ? `/${obj.jobId}` : ''; const jobId = obj && obj.jobId ? `/${obj.jobId}` : '';
return httpService.http<any>({ return httpService.http<{ jobs: JobStats[]; count: number }>({
path: `${basePath()}/anomaly_detectors${jobId}/_stats`, path: `${basePath()}/anomaly_detectors${jobId}/_stats`,
}); });
}, },
@ -614,7 +619,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
}, },
mlNodeCount() { mlNodeCount() {
return httpService.http<{ count: number }>({ return httpService.http<MlNodeCount>({
path: `${basePath()}/ml_node_count`, path: `${basePath()}/ml_node_count`,
method: 'GET', method: 'GET',
}); });

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IScopedClusterClient } from 'kibana/server';
import { MlNodeCount } from '../../common/types/ml_server_info';
export async function getMlNodeCount(client: IScopedClusterClient): Promise<MlNodeCount> {
const { body } = await client.asInternalUser.nodes.info({
filter_path: 'nodes.*.attributes',
});
let count = 0;
if (typeof body.nodes === 'object') {
Object.keys(body.nodes).forEach((k) => {
if (body.nodes[k].attributes !== undefined) {
const maxOpenJobs = body.nodes[k].attributes['ml.max_open_jobs'];
if (maxOpenJobs !== null && maxOpenJobs > 0) {
count++;
}
}
});
}
const lazyNodeCount = await getLazyMlNodeCount(client);
return { count, lazyNodeCount };
}
export async function getLazyMlNodeCount(client: IScopedClusterClient) {
const { body } = await client.asInternalUser.cluster.getSettings({
include_defaults: true,
filter_path: '**.xpack.ml.max_lazy_ml_nodes',
});
const lazyMlNodesString: string | undefined = (
body.defaults ||
body.persistent ||
body.transient ||
{}
).xpack?.ml.max_lazy_ml_nodes;
const count = lazyMlNodesString === undefined ? 0 : +lazyMlNodesString;
if (count === 0 || isNaN(count)) {
return 0;
}
return count;
}
export async function countJobsLazyStarting(
client: IScopedClusterClient,
startingJobsCount: number
) {
const lazyMlNodesCount = await getLazyMlNodeCount(client);
const { count: currentMlNodeCount } = await getMlNodeCount(client);
const availableLazyMlNodes = lazyMlNodesCount ?? lazyMlNodesCount - currentMlNodeCount;
let lazilyStartingJobsCount = startingJobsCount;
if (startingJobsCount > availableLazyMlNodes) {
if (lazyMlNodesCount > currentMlNodeCount) {
lazilyStartingJobsCount = availableLazyMlNodes;
}
}
const response = {
availableLazyMlNodes,
currentMlNodeCount,
lazilyStartingJobsCount,
totalStartingJobs: startingJobsCount,
};
return response;
}

View file

@ -35,12 +35,12 @@ import {
import type { MlClient } from '../../lib/ml_client'; import type { MlClient } from '../../lib/ml_client';
export class AnalyticsManager { export class AnalyticsManager {
private _client: IScopedClusterClient['asInternalUser']; private _client: IScopedClusterClient;
private _mlClient: MlClient; private _mlClient: MlClient;
private _inferenceModels: TrainedModelConfigResponse[]; private _inferenceModels: TrainedModelConfigResponse[];
private _jobStats: DataFrameAnalyticsStats[]; private _jobStats: DataFrameAnalyticsStats[];
constructor(mlClient: MlClient, client: IScopedClusterClient['asInternalUser']) { constructor(mlClient: MlClient, client: IScopedClusterClient) {
this._client = client; this._client = client;
this._mlClient = mlClient; this._mlClient = mlClient;
this._inferenceModels = []; this._inferenceModels = [];
@ -112,7 +112,9 @@ export class AnalyticsManager {
} }
private async getAnalyticsStats() { private async getAnalyticsStats() {
const resp = await this._mlClient.getDataFrameAnalyticsStats({ size: 1000 }); const resp = await this._mlClient.getDataFrameAnalyticsStats<{
data_frame_analytics: DataFrameAnalyticsStats[];
}>({ size: 1000 });
const stats = resp?.body?.data_frame_analytics; const stats = resp?.body?.data_frame_analytics;
return stats; return stats;
} }
@ -142,15 +144,14 @@ export class AnalyticsManager {
} }
private async getIndexData(index: string) { private async getIndexData(index: string) {
const indexData = await this._client.indices.get({ const indexData = await this._client.asInternalUser.indices.get({
index, index,
}); });
return indexData?.body; return indexData?.body;
} }
private async getTransformData(transformId: string) { private async getTransformData(transformId: string) {
const transform = await this._client.transform.getTransform({ const transform = await this._client.asInternalUser.transform.getTransform({
transform_id: transformId, transform_id: transformId,
}); });
const transformData = transform?.body?.transforms[0]; const transformData = transform?.body?.transforms[0];

View file

@ -204,6 +204,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
isNotSingleMetricViewerJobMessage: errorMessage, isNotSingleMetricViewerJobMessage: errorMessage,
nodeName: job.node ? job.node.name : undefined, nodeName: job.node ? job.node.name : undefined,
deleting: job.deleting || undefined, deleting: job.deleting || undefined,
awaitingNodeAssignment: isJobAwaitingNodeAssignment(job),
}; };
if (jobIds.find((j) => j === tempJob.id)) { if (jobIds.find((j) => j === tempJob.id)) {
tempJob.fullJob = job; tempJob.fullJob = job;
@ -519,6 +520,10 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) {
return false; return false;
} }
function isJobAwaitingNodeAssignment(job: CombinedJobWithStats) {
return job.node === undefined && job.state === JOB_STATE.OPENING;
}
return { return {
forceDeleteJob, forceDeleteJob,
deleteJobs, deleteJobs,

View file

@ -44,7 +44,7 @@ function getAnalyticsMap(
client: IScopedClusterClient, client: IScopedClusterClient,
idOptions: GetAnalyticsMapArgs idOptions: GetAnalyticsMapArgs
) { ) {
const analytics = new AnalyticsManager(mlClient, client.asInternalUser); const analytics = new AnalyticsManager(mlClient, client);
return analytics.getAnalyticsMap(idOptions); return analytics.getAnalyticsMap(idOptions);
} }
@ -53,7 +53,7 @@ function getExtendedMap(
client: IScopedClusterClient, client: IScopedClusterClient,
idOptions: ExtendAnalyticsMapArgs idOptions: ExtendAnalyticsMapArgs
) { ) {
const analytics = new AnalyticsManager(mlClient, client.asInternalUser); const analytics = new AnalyticsManager(mlClient, client);
return analytics.extendAnalyticsMapForAnalyticsJob(idOptions); return analytics.extendAnalyticsMapForAnalyticsJob(idOptions);
} }

View file

@ -5,12 +5,13 @@
*/ */
import { schema } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema';
import { IScopedClusterClient } from 'kibana/server';
import { wrapError } from '../client/error_wrapper'; import { wrapError } from '../client/error_wrapper';
import { mlLog } from '../lib/log'; import { mlLog } from '../lib/log';
import { capabilitiesProvider } from '../lib/capabilities'; import { capabilitiesProvider } from '../lib/capabilities';
import { spacesUtilsProvider } from '../lib/spaces_utils'; import { spacesUtilsProvider } from '../lib/spaces_utils';
import { RouteInitialization, SystemRouteDeps } from '../types'; import { RouteInitialization, SystemRouteDeps } from '../types';
import { getMlNodeCount } from '../lib/node_utils';
/** /**
* System routes * System routes
@ -19,25 +20,6 @@ export function systemRoutes(
{ router, mlLicense, routeGuard }: RouteInitialization, { router, mlLicense, routeGuard }: RouteInitialization,
{ getSpaces, cloud, resolveMlCapabilities }: SystemRouteDeps { getSpaces, cloud, resolveMlCapabilities }: SystemRouteDeps
) { ) {
async function getNodeCount(client: IScopedClusterClient) {
const { body } = await client.asInternalUser.nodes.info({
filter_path: 'nodes.*.attributes',
});
let count = 0;
if (typeof body.nodes === 'object') {
Object.keys(body.nodes).forEach((k) => {
if (body.nodes[k].attributes !== undefined) {
const maxOpenJobs = body.nodes[k].attributes['ml.max_open_jobs'];
if (maxOpenJobs !== null && maxOpenJobs > 0) {
count++;
}
}
});
}
return { count };
}
/** /**
* @apiGroup SystemRoutes * @apiGroup SystemRoutes
* *
@ -156,7 +138,7 @@ export function systemRoutes(
routeGuard.basicLicenseAPIGuard(async ({ client, response }) => { routeGuard.basicLicenseAPIGuard(async ({ client, response }) => {
try { try {
return response.ok({ return response.ok({
body: await getNodeCount(client), body: await getMlNodeCount(client),
}); });
} catch (e) { } catch (e) {
return response.customError(wrapError(e)); return response.customError(wrapError(e));

View file

@ -43,6 +43,7 @@ describe('useInstalledSecurityJobs', () => {
expect(result.current.jobs).toEqual( expect(result.current.jobs).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
awaitingNodeAssignment: false,
datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', datafeedId: 'datafeed-siem-api-rare_process_linux_ecs',
datafeedIndices: ['auditbeat-*'], datafeedIndices: ['auditbeat-*'],
datafeedState: 'stopped', datafeedState: 'stopped',

View file

@ -46,6 +46,7 @@ export const mockOpenedJob: MlSummaryJob = {
memory_status: 'hard_limit', memory_status: 'hard_limit',
nodeName: 'siem-es', nodeName: 'siem-es',
processed_record_count: 3425264, processed_record_count: 3425264,
awaitingNodeAssignment: false,
}; };
export const mockJobsSummaryResponse: MlSummaryJob[] = [ export const mockJobsSummaryResponse: MlSummaryJob[] = [
@ -64,6 +65,7 @@ export const mockJobsSummaryResponse: MlSummaryJob[] = [
latestTimestampMs: 1561402325194, latestTimestampMs: 1561402325194,
earliestTimestampMs: 1554327458406, earliestTimestampMs: 1554327458406,
isSingleMetricViewerJob: true, isSingleMetricViewerJob: true,
awaitingNodeAssignment: false,
}, },
{ {
id: 'siem-api-rare_process_linux_ecs', id: 'siem-api-rare_process_linux_ecs',
@ -79,6 +81,7 @@ export const mockJobsSummaryResponse: MlSummaryJob[] = [
latestTimestampMs: 1557434782207, latestTimestampMs: 1557434782207,
earliestTimestampMs: 1557353420495, earliestTimestampMs: 1557353420495,
isSingleMetricViewerJob: true, isSingleMetricViewerJob: true,
awaitingNodeAssignment: false,
}, },
{ {
id: 'siem-api-rare_process_windows_ecs', id: 'siem-api-rare_process_windows_ecs',
@ -92,6 +95,7 @@ export const mockJobsSummaryResponse: MlSummaryJob[] = [
datafeedIndices: ['winlogbeat-*'], datafeedIndices: ['winlogbeat-*'],
datafeedState: 'stopped', datafeedState: 'stopped',
isSingleMetricViewerJob: true, isSingleMetricViewerJob: true,
awaitingNodeAssignment: false,
}, },
{ {
id: 'siem-api-suspicious_login_activity_ecs', id: 'siem-api-suspicious_login_activity_ecs',
@ -105,6 +109,7 @@ export const mockJobsSummaryResponse: MlSummaryJob[] = [
datafeedIndices: ['auditbeat-*'], datafeedIndices: ['auditbeat-*'],
datafeedState: 'stopped', datafeedState: 'stopped',
isSingleMetricViewerJob: true, isSingleMetricViewerJob: true,
awaitingNodeAssignment: false,
}, },
]; ];
@ -513,6 +518,7 @@ export const mockSecurityJobs: SecurityJob[] = [
isCompatible: true, isCompatible: true,
isInstalled: true, isInstalled: true,
isElasticJob: true, isElasticJob: true,
awaitingNodeAssignment: false,
}, },
{ {
id: 'rare_process_by_host_linux_ecs', id: 'rare_process_by_host_linux_ecs',
@ -531,6 +537,7 @@ export const mockSecurityJobs: SecurityJob[] = [
isCompatible: true, isCompatible: true,
isInstalled: true, isInstalled: true,
isElasticJob: true, isElasticJob: true,
awaitingNodeAssignment: false,
}, },
{ {
datafeedId: '', datafeedId: '',
@ -549,5 +556,6 @@ export const mockSecurityJobs: SecurityJob[] = [
isCompatible: false, isCompatible: false,
isInstalled: false, isInstalled: false,
isElasticJob: true, isElasticJob: true,
awaitingNodeAssignment: false,
}, },
]; ];

View file

@ -65,6 +65,7 @@ describe('useSecurityJobs', () => {
memory_status: 'hard_limit', memory_status: 'hard_limit',
moduleId: '', moduleId: '',
processed_record_count: 582251, processed_record_count: 582251,
awaitingNodeAssignment: false,
}; };
const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false));

View file

@ -29,6 +29,7 @@ describe('useSecurityJobsHelpers', () => {
false false
); );
expect(securityJob).toEqual({ expect(securityJob).toEqual({
awaitingNodeAssignment: false,
datafeedId: '', datafeedId: '',
datafeedIndices: [], datafeedIndices: [],
datafeedState: '', datafeedState: '',

View file

@ -42,6 +42,7 @@ export const moduleToSecurityJob = (
isCompatible, isCompatible,
isInstalled: false, isInstalled: false,
isElasticJob: true, isElasticJob: true,
awaitingNodeAssignment: false,
}; };
}; };

View file

@ -25,6 +25,7 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = `
items={ items={
Array [ Array [
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "datafeed-linux_anomalous_network_activity_ecs", "datafeedId": "datafeed-linux_anomalous_network_activity_ecs",
"datafeedIndices": Array [ "datafeedIndices": Array [
"auditbeat-*", "auditbeat-*",
@ -52,6 +53,7 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = `
"processed_record_count": 32010, "processed_record_count": 32010,
}, },
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "datafeed-rare_process_by_host_linux_ecs", "datafeedId": "datafeed-rare_process_by_host_linux_ecs",
"datafeedIndices": Array [ "datafeedIndices": Array [
"auditbeat-*", "auditbeat-*",
@ -76,6 +78,7 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = `
"processed_record_count": 0, "processed_record_count": 0,
}, },
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "", "datafeedId": "",
"datafeedIndices": Array [], "datafeedIndices": Array [],
"datafeedState": "", "datafeedState": "",

View file

@ -28,6 +28,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = `
securityJobs={ securityJobs={
Array [ Array [
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "datafeed-linux_anomalous_network_activity_ecs", "datafeedId": "datafeed-linux_anomalous_network_activity_ecs",
"datafeedIndices": Array [ "datafeedIndices": Array [
"auditbeat-*", "auditbeat-*",
@ -55,6 +56,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = `
"processed_record_count": 32010, "processed_record_count": 32010,
}, },
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "datafeed-rare_process_by_host_linux_ecs", "datafeedId": "datafeed-rare_process_by_host_linux_ecs",
"datafeedIndices": Array [ "datafeedIndices": Array [
"auditbeat-*", "auditbeat-*",
@ -79,6 +81,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = `
"processed_record_count": 0, "processed_record_count": 0,
}, },
Object { Object {
"awaitingNodeAssignment": false,
"datafeedId": "", "datafeedId": "",
"datafeedIndices": Array [], "datafeedIndices": Array [],
"datafeedState": "", "datafeedState": "",