From 1584808526035b44e53ae97fb7c419b2ba9362ae Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 12 Nov 2019 13:42:23 +0100 Subject: [PATCH] [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 --- .../ml/public/components/stats_bar/stat.tsx | 2 +- .../public/components/stats_bar/stats_bar.tsx | 2 +- .../analytics_list/analytics_list.tsx | 54 ++++- .../use_create_analytics_form.ts | 2 +- .../pages/analytics_management/page.tsx | 46 ++--- .../analytics_service/get_analytics.test.ts | 92 +++++++++ .../analytics_service/get_analytics.ts | 100 ++++++++-- .../ml/public/jobs/jobs_list/_jobs_list.scss | 6 +- .../jobs_list_view/_jobs_list_view.scss | 8 - .../jobs_list_view/jobs_list_view.js | 186 ++++++++++-------- .../plugins/ml/public/jobs/jobs_list/jobs.js | 6 +- .../components/jobs_list_page/_buttons.scss | 4 - .../jobs_list_page/jobs_list_page.tsx | 5 - .../analytics_panel/analytics_panel.tsx | 79 +++++--- .../analytics_panel/analytics_stats_bar.tsx | 89 --------- .../components/analytics_panel/table.tsx | 50 ++--- .../public/services/ml_api_service/index.d.ts | 19 +- 17 files changed, 422 insertions(+), 328 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts delete mode 100644 x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx b/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx index 9de287d54a72095..45000a2252ce679 100644 --- a/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx +++ b/x-pack/legacy/plugins/ml/public/components/stats_bar/stat.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; export interface StatsBarStat { label: string; - value: string | number; + value: number; show?: boolean; } interface StatProps { diff --git a/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx b/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx index df87fb0b05c3720..4ad1139bc9b52f3 100644 --- a/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx +++ b/x-pack/legacy/plugins/ml/public/components/stats_bar/stats_bar.tsx @@ -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 { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 1f66ea40b565ab5..8e044327610df86 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -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 = ({ 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([]); + const [analyticsStats, setAnalyticsStats] = useState( + undefined + ); const [filteredAnalytics, setFilteredAnalytics] = useState([]); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); @@ -94,10 +106,12 @@ export const DataFrameAnalyticsList: FC = ({ 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 = ({ } actions={ - !isManagementTable && openCreateJobModal !== undefined + !isManagementTable && createAnalyticsForm ? [ - + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { defaultMessage: 'Create your first data frame analytics job', })} @@ -310,7 +327,28 @@ export const DataFrameAnalyticsList: FC = ({ return ( - + + + {analyticsStats && ( + + + + )} + + + + + + + {!isManagementTable && createAnalyticsForm && ( + + + + )} + + + + { +export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const kibanaContext = useKibanaContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); const { refresh } = useRefreshAnalyticsList(); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx index fcff4aa06b6bbbf..9d5502569687cce 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx @@ -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 = () => { - - + +

{ />

-
- - - {/* grow={false} fixes IE11 issue with nested flex */} - - - - {/* grow={false} fixes IE11 issue with nested flex */} - - - - - -
- - - - - - + + +
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts new file mode 100644 index 000000000000000..33a073d7a686e72 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts @@ -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, + }, + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 36fd283cbea70df..1875216408c6288 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -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; -type GetDataFrameAnalyticsStatsResponse = - | GetDataFrameAnalyticsStatsResponseOk - | GetDataFrameAnalyticsStatsResponseError; +/** + * 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, + }, + }; +} -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>, + setAnalyticsStats: React.Dispatch>, setErrorMessage: React.Dispatch< React.SetStateAction >, @@ -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); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss index d94bb5d67827998..824f764de390206 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/_jobs_list.scss @@ -1,7 +1,3 @@ -.job-management { - padding: $euiSizeL; -} - .new-job-button-container { float: right; -} \ No newline at end of file +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss index a7d562a9494cd74..ef0fbc358193e4b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/_jobs_list_view.scss @@ -8,11 +8,3 @@ .job-management { padding: 20px; } - -.job-buttons-container { - float: right; -} - -.clear { - clear: both; -} diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 786321c7be6c119..2b60eed5fd248e9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -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 (
@@ -442,38 +441,51 @@ export class JobsListView extends Component { const { isManagementTable } = this.props; return ( - - -
- - -
-
- +
+ {!isManagementTable && ( + <> + +

+ +

+
+ + + )} + + + + + + + + + + + + + + + {!isManagementTable && ( - + - {isManagementTable === undefined && - - - } - -
-
- -
- - - - { !isManagementTable && this.renderJobsListComponents() } - { isManagementTable && this.renderManagementJobsListComponents() } - - + )} + + + + + + + {!isManagementTable && this.renderJobsListComponents()} + {isManagementTable && this.renderManagementJobsListComponents()} +
); } } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js index 188048d2d2f05fa..21c184cdcd298ea 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js @@ -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) => ( - + <> - + ); diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss index 80b3ad5a390d2e6..d235c832ffaf1ed 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_buttons.scss @@ -1,10 +1,6 @@ // Refresh button style -.job-buttons-container { - float: right; -} - .managementJobsList{ clear: both; } diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 99e0a240d32d149..e3188c0892580ed 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -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: ( - - - - = ({ jobCreationDisabled }) => { const [analytics, setAnalytics] = useState([]); + const [analyticsStats, setAnalyticsStats] = useState( + undefined + ); const [errorMessage, setErrorMessage] = useState(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 = ({ jobCreationDisabled }) => { }; const errorDisplay = ( - - -
-          {errorMessage && errorMessage.message !== undefined
-            ? errorMessage.message
-            : JSON.stringify(errorMessage)}
-        
-
-
+ +
+        {errorMessage && errorMessage.message !== undefined
+          ? errorMessage.message
+          : JSON.stringify(errorMessage)}
+      
+
); const panelClass = isInitialized === false ? 'mlOverviewPanel__isLoading' : 'mlOverviewPanel'; @@ -75,13 +87,11 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { } body={ - -

- {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.`, - })} -

-
+

+ {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.`, + })} +

} actions={ = ({ jobCreationDisabled }) => { /> )} {isInitialized === true && analytics.length > 0 && ( - + <> + + + +

+ {i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', { + defaultMessage: 'Analytics', + })} +

+
+
+ {analyticsStats !== undefined && ( + + + + )} +
+
@@ -114,7 +141,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { })}
-
+ )} ); diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx deleted file mode 100644 index 19a907ff8e899c8..000000000000000 --- a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx +++ /dev/null @@ -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 = ({ analyticsList }) => { - const analyticsStats: AnalyticStatsBarStats = getAnalyticsStats(analyticsList); - - return ; -}; diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx index 1ac767ab977008b..787f0a467f44de9 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/table.tsx @@ -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 = ({ items }) => { }; return ( - - - - -

- {i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', { - defaultMessage: 'Analytics', - })} -

-
-
- - - -
- - -
+ ); }; diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 22062331bb380f1..4f042c638471d19 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -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; - getDataFrameAnalyticsStats(analyticsId?: string): Promise; + getDataFrameAnalyticsStats(analyticsId?: string): Promise; createDataFrameAnalytics(analyticsId: string, analyticsConfig: any): Promise; evaluateDataFrameAnalytics(evaluateConfig: any): Promise; deleteDataFrameAnalytics(analyticsId: string): Promise; @@ -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;