[ML] Stats bar for data frame analytics (#49464)

* [ML] stats for analytics jobs

* [ML] alight stats position

* [ML] refactor getAnalyticFactory, remove analytics stats bar component

* [ML] align layout for anomaly detection

* [ML] align layout

* [ML] show failed jobs count

* [ML] Anomaly detection jobs header

* [ML] test

* [ML] fix action columns

* [ML] add type for createAnalyticsForm

* [ML] move page title, prettier formatting
This commit is contained in:
Dima Arnautov 2019-11-12 13:42:23 +01:00 committed by GitHub
parent d00be3438c
commit b9a43a1788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 420 additions and 326 deletions

View file

@ -8,7 +8,7 @@ import React, { FC } from 'react';
export interface StatsBarStat {
label: string;
value: string | number;
value: number;
show?: boolean;
}
interface StatProps {

View file

@ -23,7 +23,7 @@ export interface AnalyticStatsBarStats extends Stats {
stopped: StatsBarStat;
}
type StatsBarStats = JobStatsBarStats | AnalyticStatsBarStats;
export type StatsBarStats = JobStatsBarStats | AnalyticStatsBarStats;
type StatsKey = keyof StatsBarStats;
interface StatsBarProps {

View file

@ -8,7 +8,14 @@ import React, { Fragment, FC, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt } from '@elastic/eui';
import {
EuiButtonEmpty,
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common';
import { checkPermission } from '../../../../../privilege/check_privilege';
@ -22,7 +29,6 @@ import {
Query,
Clause,
} from './common';
import { ActionDispatchers } from '../../hooks/use_create_analytics_form/actions';
import { getAnalyticsFactory } from '../../services/analytics_service';
import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';
@ -33,6 +39,10 @@ import {
SortDirection,
SORT_DIRECTION,
} from '../../../../../components/ml_in_memory_table';
import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar';
import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button';
import { CreateAnalyticsButton } from '../create_analytics_button';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
function getItemIdToExpandedRowMap(
itemIds: DataFrameAnalyticsId[],
@ -62,20 +72,22 @@ interface Props {
isManagementTable?: boolean;
isMlEnabledInSpace?: boolean;
blockRefresh?: boolean;
openCreateJobModal?: ActionDispatchers['openModal'];
createAnalyticsForm?: CreateAnalyticsFormProps;
}
// isManagementTable - for use in Kibana managagement ML section
export const DataFrameAnalyticsList: FC<Props> = ({
isManagementTable = false,
isMlEnabledInSpace = true,
blockRefresh = false,
openCreateJobModal,
createAnalyticsForm,
}) => {
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [filterActive, setFilterActive] = useState(false);
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
undefined
);
const [filteredAnalytics, setFilteredAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [expandedRowItemIds, setExpandedRowItemIds] = useState<DataFrameAnalyticsId[]>([]);
@ -94,10 +106,12 @@ export const DataFrameAnalyticsList: FC<Props> = ({
const getAnalytics = getAnalyticsFactory(
setAnalytics,
setAnalyticsStats,
setErrorMessage,
setIsInitialized,
blockRefresh
);
// Subscribe to the refresh observable to trigger reloading the analytics list.
useRefreshAnalyticsList({
isLoading: setIsLoading,
@ -213,9 +227,12 @@ export const DataFrameAnalyticsList: FC<Props> = ({
</h2>
}
actions={
!isManagementTable && openCreateJobModal !== undefined
!isManagementTable && createAnalyticsForm
? [
<EuiButtonEmpty onClick={openCreateJobModal} isDisabled={disabled}>
<EuiButtonEmpty
onClick={createAnalyticsForm.actions.openModal}
isDisabled={disabled}
>
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
defaultMessage: 'Create your first data frame analytics job',
})}
@ -310,7 +327,28 @@ export const DataFrameAnalyticsList: FC<Props> = ({
return (
<Fragment>
<ProgressBar isLoading={isLoading} />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{analyticsStats && (
<EuiFlexItem grow={false}>
<StatsBar stats={analyticsStats} dataTestSub={'mlAnalyticsStatsBar'} />
</EuiFlexItem>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<RefreshAnalyticsListButton />
</EuiFlexItem>
{!isManagementTable && createAnalyticsForm && (
<EuiFlexItem grow={false}>
<CreateAnalyticsButton {...createAnalyticsForm} />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<MlInMemoryTable
allowNeutralSort={false}
className="mlAnalyticsTable"

View file

@ -43,7 +43,7 @@ export function getErrorMessage(error: any) {
return JSON.stringify(error);
}
export const useCreateAnalyticsForm = () => {
export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
const kibanaContext = useKibanaContext();
const [state, dispatch] = useReducer(reducer, getInitialState());
const { refresh } = useRefreshAnalyticsList();

View file

@ -4,29 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, FC, useState } from 'react';
import React, { FC, Fragment, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
EuiBetaBadge,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPanel,
EuiSpacer,
EuiTitle,
EuiPageHeader,
EuiPageHeaderSection,
} from '@elastic/eui';
import { NavigationMenu } from '../../../components/navigation_menu';
import { CreateAnalyticsButton } from './components/create_analytics_button';
import { DataFrameAnalyticsList } from './components/analytics_list';
import { RefreshAnalyticsListButton } from './components/refresh_analytics_list_button';
import { useRefreshInterval } from './components/analytics_list/use_refresh_interval';
import { useCreateAnalyticsForm } from './hooks/use_create_analytics_form';
@ -42,8 +35,8 @@ export const Page: FC = () => {
<NavigationMenu tabId="data_frame_analytics" />
<EuiPage data-test-subj="mlPageDataFrameAnalytics">
<EuiPageBody>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle>
<h1>
<FormattedMessage
@ -67,29 +60,12 @@ export const Page: FC = () => {
/>
</h1>
</EuiTitle>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiFlexGroup alignItems="center">
{/* grow={false} fixes IE11 issue with nested flex */}
<EuiFlexItem grow={false}>
<RefreshAnalyticsListButton />
</EuiFlexItem>
{/* grow={false} fixes IE11 issue with nested flex */}
<EuiFlexItem grow={false}>
<CreateAnalyticsButton {...createAnalyticsForm} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiSpacer size="l" />
<EuiPanel>
<DataFrameAnalyticsList
blockRefresh={blockRefresh}
openCreateJobModal={createAnalyticsForm.actions.openModal}
/>
</EuiPanel>
</EuiPageContentBody>
</EuiPageHeaderSection>
</EuiPageHeader>
<DataFrameAnalyticsList
blockRefresh={blockRefresh}
createAnalyticsForm={createAnalyticsForm}
/>
</EuiPageBody>
</EuiPage>
</Fragment>

View file

@ -0,0 +1,92 @@
/*
* 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 { GetDataFrameAnalyticsStatsResponseOk } from '../../../../../services/ml_api_service';
import { getAnalyticsJobsStats } from './get_analytics';
import { DATA_FRAME_TASK_STATE } from '../../components/analytics_list/common';
jest.mock('ui/index_patterns', () => ({
validateIndexPattern: () => true,
}));
describe('get_analytics', () => {
test('should get analytics jobs stats', () => {
// arrange
const mockResponse: GetDataFrameAnalyticsStatsResponseOk = {
count: 2,
data_frame_analytics: [
{
id: 'outlier-cloudwatch',
state: DATA_FRAME_TASK_STATE.STOPPED,
progress: [
{
phase: 'reindexing',
progress_percent: 0,
},
{
phase: 'loading_data',
progress_percent: 0,
},
{
phase: 'analyzing',
progress_percent: 0,
},
{
phase: 'writing_results',
progress_percent: 0,
},
],
},
{
id: 'reg-gallery',
state: DATA_FRAME_TASK_STATE.FAILED,
progress: [
{
phase: 'reindexing',
progress_percent: 0,
},
{
phase: 'loading_data',
progress_percent: 0,
},
{
phase: 'analyzing',
progress_percent: 0,
},
{
phase: 'writing_results',
progress_percent: 0,
},
],
},
],
};
// act and assert
expect(getAnalyticsJobsStats(mockResponse)).toEqual({
total: {
label: 'Total analytics jobs',
value: 2,
show: true,
},
started: {
label: 'Running',
value: 0,
show: true,
},
stopped: {
label: 'Stopped',
value: 1,
show: true,
},
failed: {
label: 'Failed',
value: 1,
show: true,
},
});
});
});

View file

@ -4,32 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ml } from '../../../../../services/ml_api_service';
import { i18n } from '@kbn/i18n';
import {
GetDataFrameAnalyticsStatsResponse,
GetDataFrameAnalyticsStatsResponseError,
GetDataFrameAnalyticsStatsResponseOk,
ml,
} from '../../../../../services/ml_api_service';
import {
DataFrameAnalyticsConfig,
refreshAnalyticsList$,
REFRESH_ANALYTICS_LIST_STATE,
refreshAnalyticsList$,
} from '../../../../common';
import {
DataFrameAnalyticsListRow,
DataFrameAnalyticsStats,
DATA_FRAME_MODE,
DataFrameAnalyticsListRow,
isDataFrameAnalyticsFailed,
isDataFrameAnalyticsRunning,
isDataFrameAnalyticsStats,
isDataFrameAnalyticsStopped,
} from '../../components/analytics_list/common';
import { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
interface GetDataFrameAnalyticsResponse {
count: number;
data_frame_analytics: DataFrameAnalyticsConfig[];
}
interface GetDataFrameAnalyticsStatsResponseOk {
node_failures?: object;
count: number;
data_frame_analytics: DataFrameAnalyticsStats[];
}
const isGetDataFrameAnalyticsStatsResponseOk = (
export const isGetDataFrameAnalyticsStatsResponseOk = (
arg: any
): arg is GetDataFrameAnalyticsStatsResponseOk => {
return (
@ -39,20 +42,71 @@ const isGetDataFrameAnalyticsStatsResponseOk = (
);
};
interface GetDataFrameAnalyticsStatsResponseError {
statusCode: number;
error: string;
message: string;
export type GetAnalytics = (forceRefresh?: boolean) => void;
/**
* Gets initial object for analytics stats.
*/
export function getInitialAnalyticsStats(): AnalyticStatsBarStats {
return {
total: {
label: i18n.translate('xpack.ml.overview.statsBar.totalAnalyticsLabel', {
defaultMessage: 'Total analytics jobs',
}),
value: 0,
show: true,
},
started: {
label: i18n.translate('xpack.ml.overview.statsBar.runningAnalyticsLabel', {
defaultMessage: 'Running',
}),
value: 0,
show: true,
},
stopped: {
label: i18n.translate('xpack.ml.overview.statsBar.stoppedAnalyticsLabel', {
defaultMessage: 'Stopped',
}),
value: 0,
show: true,
},
failed: {
label: i18n.translate('xpack.ml.overview.statsBar.failedAnalyticsLabel', {
defaultMessage: 'Failed',
}),
value: 0,
show: false,
},
};
}
type GetDataFrameAnalyticsStatsResponse =
| GetDataFrameAnalyticsStatsResponseOk
| GetDataFrameAnalyticsStatsResponseError;
export type GetAnalytics = (forceRefresh?: boolean) => void;
/**
* Gets analytics jobs stats formatted for the stats bar.
*/
export function getAnalyticsJobsStats(
analyticsStats: GetDataFrameAnalyticsStatsResponseOk
): AnalyticStatsBarStats {
const resultStats: AnalyticStatsBarStats = analyticsStats.data_frame_analytics.reduce(
(acc, { state }) => {
if (isDataFrameAnalyticsFailed(state)) {
acc.failed.value = ++acc.failed.value;
} else if (isDataFrameAnalyticsRunning(state)) {
acc.started.value = ++acc.started.value;
} else if (isDataFrameAnalyticsStopped(state)) {
acc.stopped.value = ++acc.stopped.value;
}
return acc;
},
getInitialAnalyticsStats()
);
resultStats.failed.show = resultStats.failed.value > 0;
resultStats.total.value = analyticsStats.count;
return resultStats;
}
export const getAnalyticsFactory = (
setAnalytics: React.Dispatch<React.SetStateAction<DataFrameAnalyticsListRow[]>>,
setAnalyticsStats: React.Dispatch<React.SetStateAction<AnalyticStatsBarStats | undefined>>,
setErrorMessage: React.Dispatch<
React.SetStateAction<GetDataFrameAnalyticsStatsResponseError | undefined>
>,
@ -74,6 +128,10 @@ export const getAnalyticsFactory = (
const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics();
const analyticsStats: GetDataFrameAnalyticsStatsResponse = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats();
const analyticsStatsResult = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
? getAnalyticsJobsStats(analyticsStats)
: undefined;
const tableRows = analyticsConfigs.data_frame_analytics.reduce(
(reducedtableRows, config) => {
const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats)
@ -100,6 +158,7 @@ export const getAnalyticsFactory = (
);
setAnalytics(tableRows);
setAnalyticsStats(analyticsStatsResult);
setErrorMessage(undefined);
setIsInitialized(true);
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE);
@ -109,6 +168,7 @@ export const getAnalyticsFactory = (
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.ERROR);
refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.IDLE);
setAnalytics([]);
setAnalyticsStats(undefined);
setErrorMessage(e);
setIsInitialized(true);
}

View file

@ -1,7 +1,3 @@
.job-management {
padding: $euiSizeL;
}
.new-job-button-container {
float: right;
}
}

View file

@ -8,11 +8,3 @@
.job-management {
padding: 20px;
}
.job-buttons-container {
float: right;
}
.clear {
clear: both;
}

View file

@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component } from 'react';
import { timefilter } from 'ui/timefilter';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { ml } from 'plugins/ml/services/ml_api_service';
import { loadFullJob, filterJobs, checkForAutoStartDatafeed } from '../utils';
import { checkForAutoStartDatafeed, filterJobs, loadFullJob } from '../utils';
import { JobsList } from '../jobs_list';
import { JobDetails } from '../job_details';
import { JobFilterBar } from '../job_filter_bar';
@ -26,22 +28,11 @@ import { isEqual } from 'lodash';
import {
DEFAULT_REFRESH_INTERVAL_MS,
MINIMUM_REFRESH_INTERVAL_MS,
DELETING_JOBS_REFRESH_INTERVAL_MS,
MINIMUM_REFRESH_INTERVAL_MS,
} from '../../../../../common/constants/jobs_list';
import React, {
Component
} from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
} from '@elastic/eui';
let jobsRefreshInterval = null;
let jobsRefreshInterval = null;
let deletingJobsRefreshTimeout = null;
// 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page
@ -76,8 +67,8 @@ export class JobsListView extends Component {
if (this.props.isManagementTable === true) {
this.refreshJobSummaryList(true);
} else {
// The advanced job wizard is still angularjs based and triggers
// broadcast events which it expects the jobs list to be subscribed to.
// The advanced job wizard is still angularjs based and triggers
// broadcast events which it expects the jobs list to be subscribed to.
this.props.angularWrapperScope.$on('jobsUpdated', () => {
this.refreshJobSummaryList(true);
});
@ -114,7 +105,7 @@ export class JobsListView extends Component {
// so switch it on and set the interval to 30s
timefilter.setRefreshInterval({
pause: false,
value: DEFAULT_REFRESH_INTERVAL_MS
value: DEFAULT_REFRESH_INTERVAL_MS,
});
}
@ -124,7 +115,7 @@ export class JobsListView extends Component {
initAutoRefreshUpdate() {
// update the interval if it changes
this.refreshIntervalSubscription = timefilter.getRefreshIntervalUpdate$().subscribe({
next: () => this.setAutoRefresh()
next: () => this.setAutoRefresh(),
});
}
@ -143,7 +134,7 @@ export class JobsListView extends Component {
this.clearRefreshInterval();
if (interval >= MINIMUM_REFRESH_INTERVAL_MS) {
this.blockRefresh = false;
jobsRefreshInterval = setInterval(() => (this.refreshJobSummaryList()), interval);
jobsRefreshInterval = setInterval(() => this.refreshJobSummaryList(), interval);
}
}
@ -159,13 +150,12 @@ export class JobsListView extends Component {
}
}
toggleRow = (jobId) => {
toggleRow = jobId => {
if (this.state.itemIdToExpandedRowMap[jobId]) {
const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
delete itemIdToExpandedRowMap[jobId];
this.setState({ itemIdToExpandedRowMap });
} else {
let itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap };
if (this.state.fullJobsList[jobId] !== undefined) {
@ -191,7 +181,7 @@ export class JobsListView extends Component {
this.setState({ itemIdToExpandedRowMap }, () => {
loadFullJob(jobId)
.then((job) => {
.then(job => {
const fullJobsList = { ...this.state.fullJobsList };
fullJobsList[jobId] = job;
this.setState({ fullJobsList }, () => {
@ -213,54 +203,54 @@ export class JobsListView extends Component {
this.setState({ itemIdToExpandedRowMap });
});
})
.catch((error) => {
.catch(error => {
console.error(error);
});
});
}
}
};
addUpdateFunction = (id, f) => {
this.updateFunctions[id] = f;
}
removeUpdateFunction = (id) => {
};
removeUpdateFunction = id => {
delete this.updateFunctions[id];
}
};
setShowEditJobFlyoutFunction = (func) => {
setShowEditJobFlyoutFunction = func => {
this.showEditJobFlyout = func;
}
};
unsetShowEditJobFlyoutFunction = () => {
this.showEditJobFlyout = () => {};
}
};
setShowDeleteJobModalFunction = (func) => {
setShowDeleteJobModalFunction = func => {
this.showDeleteJobModal = func;
}
};
unsetShowDeleteJobModalFunction = () => {
this.showDeleteJobModal = () => {};
}
};
setShowStartDatafeedModalFunction = (func) => {
setShowStartDatafeedModalFunction = func => {
this.showStartDatafeedModal = func;
}
};
unsetShowStartDatafeedModalFunction = () => {
this.showStartDatafeedModal = () => {};
}
};
setShowCreateWatchFlyoutFunction = (func) => {
setShowCreateWatchFlyoutFunction = func => {
this.showCreateWatchFlyout = func;
}
};
unsetShowCreateWatchFlyoutFunction = () => {
this.showCreateWatchFlyout = () => {};
}
};
getShowCreateWatchFlyoutFunction = () => {
return this.showCreateWatchFlyout;
}
};
selectJobChange = (selectedJobs) => {
selectJobChange = selectedJobs => {
this.setState({ selectedJobs });
}
};
refreshSelectedJobs() {
const selectedJobsIds = this.state.selectedJobs.map(j => j.id);
@ -275,24 +265,23 @@ export class JobsListView extends Component {
this.setState({ selectedJobs });
}
setFilters = (filterClauses) => {
setFilters = filterClauses => {
const filteredJobsSummaryList = filterJobs(this.state.jobsSummaryList, filterClauses);
this.setState({ filteredJobsSummaryList, filterClauses }, () => {
this.refreshSelectedJobs();
});
}
};
onRefreshClick = () => {
this.setState({ isRefreshing: true });
this.refreshJobSummaryList(true);
}
};
isDoneRefreshing = () => {
this.setState({ isRefreshing: false });
}
};
async refreshJobSummaryList(forceRefresh = false) {
if (forceRefresh === true || this.blockRefresh === false) {
// Set loading to true for jobs_list table for initial job loading
if (this.state.loading === null) {
this.setState({ loading: true });
@ -302,24 +291,27 @@ export class JobsListView extends Component {
try {
const jobs = await ml.jobs.jobsSummary(expandedJobsIds);
const fullJobsList = {};
const jobsSummaryList = jobs.map((job) => {
const jobsSummaryList = jobs.map(job => {
if (job.fullJob !== undefined) {
fullJobsList[job.id] = job.fullJob;
delete job.fullJob;
}
job.latestTimestampSortValue = (job.latestTimestampMs || 0);
job.latestTimestampSortValue = job.latestTimestampMs || 0;
return job;
});
const filteredJobsSummaryList = filterJobs(jobsSummaryList, this.state.filterClauses);
this.setState({ jobsSummaryList, filteredJobsSummaryList, fullJobsList, loading: false }, () => {
this.refreshSelectedJobs();
});
this.setState(
{ jobsSummaryList, filteredJobsSummaryList, fullJobsList, loading: false },
() => {
this.refreshSelectedJobs();
}
);
Object.keys(this.updateFunctions).forEach((j) => {
Object.keys(this.updateFunctions).forEach(j => {
this.updateFunctions[j].setState({ job: fullJobsList[j] });
});
jobs.forEach((job) => {
jobs.forEach(job => {
if (job.deleting && this.state.itemIdToExpandedRowMap[job.id]) {
this.toggleRow(job.id);
}
@ -342,7 +334,8 @@ export class JobsListView extends Component {
async checkDeletingJobTasks(forceRefresh = false) {
const { jobIds: taskJobIds } = await ml.jobs.deletingJobTasks();
const taskListHasChanged = (isEqual(taskJobIds.sort(), this.state.deletingJobIds.sort()) === false);
const taskListHasChanged =
isEqual(taskJobIds.sort(), this.state.deletingJobIds.sort()) === false;
this.setState({
deletingJobIds: taskJobIds,
@ -363,7 +356,13 @@ export class JobsListView extends Component {
}
renderManagementJobsListComponents() {
const { loading, itemIdToExpandedRowMap, filteredJobsSummaryList, fullJobsList, selectedJobs } = this.state;
const {
loading,
itemIdToExpandedRowMap,
filteredJobsSummaryList,
fullJobsList,
selectedJobs,
} = this.state;
return (
<div className="managementJobsList">
<div>
@ -442,38 +441,51 @@ export class JobsListView extends Component {
const { isManagementTable } = this.props;
return (
<React.Fragment>
<JobStatsBar
jobsSummaryList={jobsSummaryList}
/>
<div className="job-management" data-test-subj="ml-jobs-list">
<NodeAvailableWarning />
<UpgradeWarning />
<header>
<div className="job-buttons-container">
<EuiFlexGroup alignItems="center">
<div className="job-management" data-test-subj="ml-jobs-list">
{!isManagementTable && (
<>
<EuiTitle>
<h1>
<FormattedMessage
id="xpack.ml.jobsList.title"
defaultMessage="Anomaly detection jobs"
/>
</h1>
</EuiTitle>
<EuiSpacer size="m" />
</>
)}
<NodeAvailableWarning />
<UpgradeWarning />
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<JobStatsBar jobsSummaryList={jobsSummaryList} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<RefreshJobsListButton
onRefreshClick={this.onRefreshClick}
isRefreshing={isRefreshing}
/>
</EuiFlexItem>
{!isManagementTable && (
<EuiFlexItem grow={false}>
<RefreshJobsListButton
onRefreshClick={this.onRefreshClick}
isRefreshing={isRefreshing}
/>
<NewJobButton />
</EuiFlexItem>
{isManagementTable === undefined &&
<EuiFlexItem grow={false}>
<NewJobButton />
</EuiFlexItem>}
</EuiFlexGroup>
</div>
</header>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<div className="clear" />
<EuiSpacer size="s" />
<EuiSpacer size="s" />
{ !isManagementTable && this.renderJobsListComponents() }
{ isManagementTable && this.renderManagementJobsListComponents() }
</ div>
</React.Fragment>
{!isManagementTable && this.renderJobsListComponents()}
{isManagementTable && this.renderManagementJobsListComponents()}
</div>
);
}
}

View file

@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React from 'react';
import { NavigationMenu } from '../../components/navigation_menu';
import { JobsListView } from './components/jobs_list_view';
export const JobsPage = (props) => (
<Fragment>
<>
<NavigationMenu tabId="jobs" />
<JobsListView {...props} />
</Fragment>
</>
);

View file

@ -1,10 +1,6 @@
// Refresh button style
.job-buttons-container {
float: right;
}
.managementJobsList{
clear: both;
}

View file

@ -23,7 +23,6 @@ import { metadata } from 'ui/metadata';
// @ts-ignore undeclared module
import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view';
import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';
import { RefreshAnalyticsListButton } from '../../../../data_frame_analytics/pages/analytics_management/components/refresh_analytics_list_button';
interface Props {
isMlEnabledInSpace: boolean;
@ -56,10 +55,6 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] {
content: (
<Fragment>
<EuiSpacer size="m" />
<span className="mlKibanaManagement__analyticsRefreshButton">
<RefreshAnalyticsListButton />
</span>
<EuiSpacer size="s" className="mlKibanaManagement__analyticsSpacer" />
<DataFrameAnalyticsList
isManagementTable={true}
isMlEnabledInSpace={isMlEnabledInSpace}

View file

@ -4,30 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useState, useEffect } from 'react';
import React, { FC, useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AnalyticsTable } from './table';
import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar';
interface Props {
jobCreationDisabled: boolean;
}
export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
const [analytics, setAnalytics] = useState<DataFrameAnalyticsListRow[]>([]);
const [analyticsStats, setAnalyticsStats] = useState<AnalyticStatsBarStats | undefined>(
undefined
);
const [errorMessage, setErrorMessage] = useState<any>(undefined);
const [isInitialized, setIsInitialized] = useState(false);
const getAnalytics = getAnalyticsFactory(setAnalytics, setErrorMessage, setIsInitialized, false);
const getAnalytics = getAnalyticsFactory(
setAnalytics,
setAnalyticsStats,
setErrorMessage,
setIsInitialized,
false
);
useEffect(() => {
getAnalytics(true);
@ -38,21 +52,19 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
};
const errorDisplay = (
<Fragment>
<EuiCallOut
title={i18n.translate('xpack.ml.overview.analyticsList.errorPromptTitle', {
defaultMessage: 'An error occurred getting the data frame analytics list.',
})}
color="danger"
iconType="alert"
>
<pre>
{errorMessage && errorMessage.message !== undefined
? errorMessage.message
: JSON.stringify(errorMessage)}
</pre>
</EuiCallOut>
</Fragment>
<EuiCallOut
title={i18n.translate('xpack.ml.overview.analyticsList.errorPromptTitle', {
defaultMessage: 'An error occurred getting the data frame analytics list.',
})}
color="danger"
iconType="alert"
>
<pre>
{errorMessage && errorMessage.message !== undefined
? errorMessage.message
: JSON.stringify(errorMessage)}
</pre>
</EuiCallOut>
);
const panelClass = isInitialized === false ? 'mlOverviewPanel__isLoading' : 'mlOverviewPanel';
@ -75,13 +87,11 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
</h2>
}
body={
<Fragment>
<p>
{i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', {
defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`,
})}
</p>
</Fragment>
<p>
{i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', {
defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`,
})}
</p>
}
actions={
<EuiButton
@ -99,7 +109,24 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
/>
)}
{isInitialized === true && analytics.length > 0 && (
<Fragment>
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
{i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', {
defaultMessage: 'Analytics',
})}
</h3>
</EuiText>
</EuiFlexItem>
{analyticsStats !== undefined && (
<EuiFlexItem grow={false} className="mlOverviewPanel__statsBar">
<StatsBar stats={analyticsStats} dataTestSub={'mlOverviewAnalyticsStatsBar'} />
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
<AnalyticsTable items={analytics} />
<EuiSpacer size="m" />
<div className="mlOverviewPanel__buttons">
@ -114,7 +141,7 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => {
})}
</EuiButton>
</div>
</Fragment>
</>
)}
</EuiPanel>
);

View file

@ -1,89 +0,0 @@
/*
* 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, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { StatsBar, AnalyticStatsBarStats } from '../../../components/stats_bar';
import {
isDataFrameAnalyticsFailed,
isDataFrameAnalyticsRunning,
isDataFrameAnalyticsStopped,
DataFrameAnalyticsListRow,
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
function getAnalyticsStats(analyticsList: any[]) {
const analyticsStats = {
total: {
label: i18n.translate('xpack.ml.overview.statsBar.totalAnalyticsLabel', {
defaultMessage: 'Total analytics jobs',
}),
value: 0,
show: true,
},
started: {
label: i18n.translate('xpack.ml.overview.statsBar.runningAnalyticsLabel', {
defaultMessage: 'Running',
}),
value: 0,
show: true,
},
stopped: {
label: i18n.translate('xpack.ml.overview.statsBar.stoppedAnalyticsLabel', {
defaultMessage: 'Stopped',
}),
value: 0,
show: true,
},
failed: {
label: i18n.translate('xpack.ml.overview.statsBar.failedAnalyticsLabel', {
defaultMessage: 'Failed',
}),
value: 0,
show: false,
},
};
if (analyticsList === undefined) {
return analyticsStats;
}
let failedJobs = 0;
let startedJobs = 0;
let stoppedJobs = 0;
analyticsList.forEach(job => {
if (isDataFrameAnalyticsFailed(job.stats.state)) {
failedJobs++;
} else if (isDataFrameAnalyticsRunning(job.stats.state)) {
startedJobs++;
} else if (isDataFrameAnalyticsStopped(job.stats.state)) {
stoppedJobs++;
}
});
analyticsStats.total.value = analyticsList.length;
analyticsStats.started.value = startedJobs;
analyticsStats.stopped.value = stoppedJobs;
if (failedJobs !== 0) {
analyticsStats.failed.value = failedJobs;
analyticsStats.failed.show = true;
} else {
analyticsStats.failed.show = false;
}
return analyticsStats;
}
interface Props {
analyticsList: DataFrameAnalyticsListRow[];
}
export const AnalyticsStatsBar: FC<Props> = ({ analyticsList }) => {
const analyticsStats: AnalyticStatsBarStats = getAnalyticsStats(analyticsList);
return <StatsBar stats={analyticsStats} dataTestSub={'mlOverviewAnalyticsStatsBar'} />;
};

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, Fragment, useState } from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import React, { FC, useState } from 'react';
import { EuiBadge } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
MlInMemoryTable,
@ -25,7 +25,6 @@ import {
} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/columns';
import { AnalyticsViewAction } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/actions';
import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';
import { AnalyticsStatsBar } from './analytics_stats_bar';
interface Props {
items: any[];
@ -114,36 +113,19 @@ export const AnalyticsTable: FC<Props> = ({ items }) => {
};
return (
<Fragment>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>
{i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', {
defaultMessage: 'Analytics',
})}
</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} className="mlOverviewPanel__statsBar">
<AnalyticsStatsBar analyticsList={items} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<MlInMemoryTable
allowNeutralSort={false}
className="mlAnalyticsTable"
columns={columns}
hasActions={false}
isExpandable={false}
isSelectable={false}
items={items}
itemId={DataFrameAnalyticsListColumn.id}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlOverviewTableAnalytics"
/>
</Fragment>
<MlInMemoryTable
allowNeutralSort={false}
className="mlAnalyticsTable"
columns={columns}
hasActions={false}
isExpandable={false}
isSelectable={false}
items={items}
itemId={DataFrameAnalyticsListColumn.id}
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="mlOverviewTableAnalytics"
/>
);
};

View file

@ -11,6 +11,7 @@ import { PrivilegesResponse } from '../../../common/types/privileges';
import { MlSummaryJobs } from '../../../common/types/jobs';
import { MlServerDefaults, MlServerLimits } from '../../jobs/new_job_new/utils/new_job_defaults';
import { ES_AGGREGATION } from '../../../common/constants/aggregation_types';
import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
// TODO This is not a complete representation of all methods of `ml.*`.
// It just satisfies needs for other parts of the code area which use
@ -65,7 +66,7 @@ declare interface Ml {
dataFrameAnalytics: {
getDataFrameAnalytics(analyticsId?: string): Promise<any>;
getDataFrameAnalyticsStats(analyticsId?: string): Promise<any>;
getDataFrameAnalyticsStats(analyticsId?: string): Promise<GetDataFrameAnalyticsStatsResponse>;
createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise<any>;
evaluateDataFrameAnalytics(evaluateConfig: any): Promise<any>;
deleteDataFrameAnalytics(analyticsId: string): Promise<any>;
@ -155,3 +156,19 @@ declare interface Ml {
}
declare const ml: Ml;
export interface GetDataFrameAnalyticsStatsResponseOk {
node_failures?: object;
count: number;
data_frame_analytics: DataFrameAnalyticsStats[];
}
export interface GetDataFrameAnalyticsStatsResponseError {
statusCode: number;
error: string;
message: string;
}
export type GetDataFrameAnalyticsStatsResponse =
| GetDataFrameAnalyticsStatsResponseOk
| GetDataFrameAnalyticsStatsResponseError;