[ML] Add action to view datafeed counts chart to jobs list rows (#136274)

* [ML] Add action to view datafeed counts chart to jobs list rows

* [ML] Update functional tests for permissions and job actions

* [ML] Edit to test in job_table service message

* [ML] Update translations

* [ML] type change in datafeed_chart_flyout
This commit is contained in:
Pete Harverson 2022-07-14 11:33:58 +01:00 committed by GitHub
parent d35a687c29
commit ac434b3433
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 32 deletions

View file

@ -51,6 +51,7 @@ import { ML_APP_LOCATOR, ML_PAGES } from '../../../../../common/constants/locato
import { timeFormatter } from '../../../../../common/util/date_utils';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
import { DatafeedChartFlyout } from '../../../jobs/jobs_list/components/datafeed_chart_flyout';
import { RevertModelSnapshotFlyout } from '../../model_snapshots/revert_model_snapshot_flyout';
const editAnnotationsText = (
<FormattedMessage
@ -61,7 +62,7 @@ const editAnnotationsText = (
const viewDataFeedText = (
<FormattedMessage
id="xpack.ml.annotationsTable.datafeedChartTooltip"
defaultMessage="Datafeed chart"
defaultMessage="View datafeed counts"
/>
);
@ -72,9 +73,14 @@ const CURRENT_SERIES = 'current_series';
class AnnotationsTableUI extends Component {
static propTypes = {
annotations: PropTypes.array,
annotationUpdatesService: PropTypes.object.isRequired,
jobs: PropTypes.array,
detectors: PropTypes.array,
isSingleMetricViewerLinkVisible: PropTypes.bool,
isNumberBadgeVisible: PropTypes.bool,
refreshJobList: PropTypes.func,
chartDetails: PropTypes.object,
kibana: PropTypes.object,
};
constructor(props) {
@ -91,6 +97,8 @@ class AnnotationsTableUI extends Component {
? this.props.jobs[0].job_id
: undefined,
datafeedFlyoutVisible: false,
modelSnapshot: null,
revertSnapshotFlyoutVisible: false,
datafeedEnd: null,
};
this.sorting = {
@ -727,10 +735,30 @@ class AnnotationsTableUI extends Component {
datafeedFlyoutVisible: false,
});
}}
onModelSnapshotAnnotationClick={(modelSnapshot) => {
this.setState({
modelSnapshot,
revertSnapshotFlyoutVisible: true,
datafeedFlyoutVisible: false,
});
}}
end={this.state.datafeedEnd}
jobId={this.state.jobId}
/>
) : null}
{this.state.revertSnapshotFlyoutVisible === true && this.state.modelSnapshot !== null ? (
<RevertModelSnapshotFlyout
snapshot={this.state.modelSnapshot}
snapshots={[this.state.modelSnapshot]}
job={this.getJob(this.state.jobId)}
closeFlyout={() => {
this.setState({
revertSnapshotFlyoutVisible: false,
});
}}
refresh={this.props.refreshJobList ?? (() => {})}
/>
) : null}
</Fragment>
);
}

View file

@ -50,12 +50,14 @@ import { DATAFEED_STATE } from '../../../../../../common/constants/states';
import {
CombinedJobWithStats,
ModelSnapshot,
MlSummaryJob,
} from '../../../../../../common/types/anomaly_detection_jobs';
import { JobMessage } from '../../../../../../common/types/audit_message';
import { LineAnnotationDatumWithModelSnapshot } from '../../../../../../common/types/results';
import { useToastNotificationService } from '../../../../services/toast_notification_service';
import { useMlApiContext } from '../../../../contexts/kibana';
import { useCurrentEuiTheme } from '../../../../components/color_range_legend';
import { RevertModelSnapshotFlyout } from '../../../../components/model_snapshots/revert_model_snapshot_flyout';
import { JobMessagesPane } from '../job_details/job_messages_pane';
import { EditQueryDelay } from './edit_query_delay';
import { CHART_DIRECTION, ChartDirectionType, CHART_SIZE } from './constants';
@ -595,3 +597,87 @@ export const DatafeedChartFlyout: FC<DatafeedChartFlyoutProps> = ({
</EuiPortal>
);
};
type ShowFunc = (jobUpdate: MlSummaryJob) => void;
interface JobListDatafeedChartFlyoutProps {
setShowFunction: (showFunc: ShowFunc) => void;
unsetShowFunction: () => void;
refreshJobs(): void;
}
/**
* Component to wire the datafeed chart flyout with the Job list view.
* @param setShowFunction function to show the flyout
* @param unsetShowFunction function called when flyout is closed
* @param refreshJobs function to refresh the jobs list
* @constructor
*/
export const JobListDatafeedChartFlyout: FC<JobListDatafeedChartFlyoutProps> = ({
setShowFunction,
unsetShowFunction,
refreshJobs,
}) => {
const [isVisible, setIsVisible] = useState(false);
const [job, setJob] = useState<MlSummaryJob | undefined>();
const [jobWithStats, setJobWithStats] = useState<CombinedJobWithStats | undefined>();
const [isRevertModelSnapshotFlyoutVisible, setIsRevertModelSnapshotFlyoutVisible] =
useState(false);
const [snapshot, setSnapshot] = useState<ModelSnapshot | null>(null);
const showFlyoutCallback = useCallback((jobUpdate: MlSummaryJob) => {
setJob(jobUpdate);
setIsVisible(true);
}, []);
const showRevertModelSnapshot = useCallback(async () => {
// Need to load the full job with stats, as the model snapshot
// flyout needs the timestamp of the last result.
const fullJob: CombinedJobWithStats = await loadFullJob(job!.id);
setJobWithStats(fullJob);
setIsRevertModelSnapshotFlyoutVisible(true);
}, [job]);
useEffect(() => {
setShowFunction(showFlyoutCallback);
return () => {
unsetShowFunction();
};
}, []);
if (isVisible === true && job !== undefined) {
return (
<DatafeedChartFlyout
onClose={() => setIsVisible(false)}
onModelSnapshotAnnotationClick={(modelSnapshot) => {
setIsVisible(false);
setSnapshot(modelSnapshot);
showRevertModelSnapshot();
}}
end={job.latestResultsTimestampMs || Date.now()}
jobId={job.id}
/>
);
}
if (
isRevertModelSnapshotFlyoutVisible === true &&
jobWithStats !== undefined &&
snapshot !== null
) {
return (
<RevertModelSnapshotFlyout
snapshot={snapshot}
snapshots={[snapshot]}
job={jobWithStats}
closeFlyout={() => {
setIsRevertModelSnapshotFlyoutVisible(false);
}}
refresh={refreshJobs}
/>
);
}
return null;
};

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { DatafeedChartFlyout } from './datafeed_chart_flyout';
export { DatafeedChartFlyout, JobListDatafeedChartFlyout } from './datafeed_chart_flyout';

View file

@ -23,6 +23,7 @@ import { isManagedJob } from '../../../jobs_utils';
export function actionsMenuContent(
showEditJobFlyout,
showDatafeedChartFlyout,
showDeleteJobModal,
showResetJobModal,
showStartDatafeedModal,
@ -34,6 +35,7 @@ export function actionsMenuContent(
const canCreateJob = checkPermission('canCreateJob') && mlNodesAvailable();
const canUpdateJob = checkPermission('canUpdateJob');
const canDeleteJob = checkPermission('canDeleteJob');
const canGetDatafeeds = checkPermission('canGetDatafeeds');
const canUpdateDatafeed = checkPermission('canUpdateDatafeed');
const canStartStopDatafeed = checkPermission('canStartStopDatafeed') && mlNodesAvailable();
const canCloseJob = checkPermission('canCloseJob') && mlNodesAvailable();
@ -152,6 +154,25 @@ export function actionsMenuContent(
},
'data-test-subj': 'mlActionButtonCloneJob',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.viewDatafeedCountsLabel', {
defaultMessage: 'View datafeed counts',
}),
description: i18n.translate(
'xpack.ml.jobsList.managementActions.viewDatafeedCountsDescription',
{
defaultMessage: 'View datafeed counts',
}
),
icon: 'visAreaStacked',
enabled: () => canGetDatafeeds,
available: () => canGetDatafeeds,
onClick: (item) => {
showDatafeedChartFlyout(item);
closeMenu();
},
'data-test-subj': 'mlActionButtonViewDatafeedChart',
},
{
name: i18n.translate('xpack.ml.jobsList.managementActions.editJobLabel', {
defaultMessage: 'Edit job',

View file

@ -9,7 +9,7 @@ import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiTabbedContent, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { EuiButtonEmpty, EuiTabbedContent, EuiLoadingSpinner } from '@elastic/eui';
import { extractJobDetails } from './extract_job_details';
import { JsonPane } from './json_tab';
@ -84,27 +84,20 @@ export class JobDetailsUI extends Component {
} = extractJobDetails(job, basePath, refreshJobList);
datafeed.titleAction = (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.jobDetails.datafeedChartTooltipText"
defaultMessage="Datafeed chart"
/>
<EuiButtonEmpty
onClick={() =>
this.setState({
datafeedChartFlyoutVisible: true,
})
}
iconType="visAreaStacked"
size="s"
>
<EuiButtonIcon
size="xs"
aria-label={i18n.translate('xpack.ml.jobDetails.datafeedChartAriaLabel', {
defaultMessage: 'Datafeed chart',
})}
iconType="visAreaStacked"
onClick={() =>
this.setState({
datafeedChartFlyoutVisible: true,
})
}
<FormattedMessage
id="xpack.ml.jobDetails.tabs.datafeed.viewCountsButtonText"
defaultMessage="View datafeed counts"
/>
</EuiToolTip>
</EuiButtonEmpty>
);
const tabs = [
@ -248,7 +241,7 @@ export class JobDetailsUI extends Component {
}),
content: (
<Fragment>
<AnnotationsTable jobs={[job]} drillDown={true} />
<AnnotationsTable jobs={[job]} refreshJobList={refreshJobList} />
<AnnotationFlyout />
</Fragment>
),

View file

@ -44,7 +44,7 @@ function Section({ section }) {
return (
<React.Fragment>
<EuiFlexGroup gutterSize="xs">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h4>{section.title}</h4>

View file

@ -330,6 +330,7 @@ export class JobsList extends Component {
),
actions: actionsMenuContent(
this.props.showEditJobFlyout,
this.props.showDatafeedChartFlyout,
this.props.showDeleteJobModal,
this.props.showResetJobModal,
this.props.showStartDatafeedModal,
@ -406,6 +407,7 @@ JobsList.propTypes = {
toggleRow: PropTypes.func.isRequired,
selectJobChange: PropTypes.func.isRequired,
showEditJobFlyout: PropTypes.func,
showDatafeedChartFlyout: PropTypes.func,
showDeleteJobModal: PropTypes.func,
showStartDatafeedModal: PropTypes.func,
showCloseJobsConfirmModal: PropTypes.func,

View file

@ -14,6 +14,7 @@ import { JobsList } from '../jobs_list';
import { JobDetails } from '../job_details';
import { JobFilterBar } from '../job_filter_bar';
import { EditJobFlyout } from '../edit_job_flyout';
import { JobListDatafeedChartFlyout } from '../datafeed_chart_flyout';
import { DeleteJobModal } from '../delete_job_modal';
import { ResetJobModal } from '../reset_job_modal';
import { StartDatafeedModal } from '../start_datafeed_modal';
@ -53,6 +54,7 @@ export class JobsListView extends Component {
this.updateFunctions = {};
this.showEditJobFlyout = () => {};
this.showDatafeedChartFlyout = () => {};
this.showStopDatafeedsConfirmModal = () => {};
this.showCloseJobsConfirmModal = () => {};
this.showDeleteJobModal = () => {};
@ -178,6 +180,13 @@ export class JobsListView extends Component {
this.showEditJobFlyout = () => {};
};
setShowDatafeedChartFlyoutFunction = (func) => {
this.showDatafeedChartFlyout = func;
};
unsetShowDatafeedChartFlyoutFunction = () => {
this.showDatafeedChartFlyout = () => {};
};
setShowStopDatafeedsConfirmModalFunction = (func) => {
this.showStopDatafeedsConfirmModal = func;
};
@ -437,6 +446,7 @@ export class JobsListView extends Component {
toggleRow={this.toggleRow}
selectJobChange={this.selectJobChange}
showEditJobFlyout={this.showEditJobFlyout}
showDatafeedChartFlyout={this.showDatafeedChartFlyout}
showDeleteJobModal={this.showDeleteJobModal}
showResetJobModal={this.showResetJobModal}
showCloseJobsConfirmModal={this.showCloseJobsConfirmModal}
@ -459,13 +469,17 @@ export class JobsListView extends Component {
refreshJobs={() => this.refreshJobSummaryList(true)}
allJobIds={jobIds}
/>
<JobListDatafeedChartFlyout
setShowFunction={this.setShowDatafeedChartFlyoutFunction}
unsetShowFunction={this.unsetShowDatafeedChartFlyoutFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
/>
<StopDatafeedsConfirmModal
setShowFunction={this.setShowStopDatafeedsConfirmModalFunction}
unsetShowFunction={this.unsetShowStopDatafeedsConfirmModalFunction}
refreshJobs={() => this.refreshJobSummaryList(true)}
allJobIds={jobIds}
/>
<CloseJobsConfirmModal
setShowFunction={this.setShowCloseJobsConfirmModalFunction}
unsetShowFunction={this.unsetShowCloseJobsConfirmModalFunction}

View file

@ -19164,8 +19164,6 @@
"xpack.ml.itemsGrid.itemsPerPageButtonLabel": "Éléments par page : {itemsPerPage}",
"xpack.ml.itemsGrid.noItemsAddedTitle": "Aucun élément n'a été ajouté",
"xpack.ml.itemsGrid.noMatchingItemsTitle": "Aucun élément correspondant",
"xpack.ml.jobDetails.datafeedChartAriaLabel": "Graphique de flux de données",
"xpack.ml.jobDetails.datafeedChartTooltipText": "Graphique de flux de données",
"xpack.ml.jobMessages.actionsLabel": "Actions",
"xpack.ml.jobMessages.clearJobAuditMessagesDisabledTooltip": "Effacement des notifications non pris en charge.",
"xpack.ml.jobMessages.clearJobAuditMessagesErrorTitle": "Erreur lors de l'effacement des avertissements et erreurs du message lié à la tâche",

View file

@ -19156,8 +19156,6 @@
"xpack.ml.itemsGrid.itemsPerPageButtonLabel": "ページごとの項目数:{itemsPerPage}",
"xpack.ml.itemsGrid.noItemsAddedTitle": "項目が追加されていません",
"xpack.ml.itemsGrid.noMatchingItemsTitle": "一致する項目が見つかりません。",
"xpack.ml.jobDetails.datafeedChartAriaLabel": "データフィードグラフ",
"xpack.ml.jobDetails.datafeedChartTooltipText": "データフィードグラフ",
"xpack.ml.jobMessages.actionsLabel": "アクション",
"xpack.ml.jobMessages.clearJobAuditMessagesDisabledTooltip": "通知の消去はサポートされていません。",
"xpack.ml.jobMessages.clearJobAuditMessagesErrorTitle": "ジョブメッセージ警告とエラーの消去エラー",

View file

@ -19177,8 +19177,6 @@
"xpack.ml.itemsGrid.itemsPerPageButtonLabel": "每页中的项:{itemsPerPage}",
"xpack.ml.itemsGrid.noItemsAddedTitle": "没有添加任何项",
"xpack.ml.itemsGrid.noMatchingItemsTitle": "没有匹配的项",
"xpack.ml.jobDetails.datafeedChartAriaLabel": "数据馈送图表",
"xpack.ml.jobDetails.datafeedChartTooltipText": "数据馈送图表",
"xpack.ml.jobMessages.actionsLabel": "操作",
"xpack.ml.jobMessages.clearJobAuditMessagesDisabledTooltip": "不支持清除通知。",
"xpack.ml.jobMessages.clearJobAuditMessagesErrorTitle": "清除作业消息警告和错误时出错",

View file

@ -226,7 +226,9 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('should display enabled AD job row action buttons');
await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionStartDatafeedButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionResetJobButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionCloneJobButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionViewDatafeedCountsButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionEditJobButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionDeleteJobButtonEnabled(adJobId, true);

View file

@ -208,8 +208,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true);
await ml.testExecution.logTestStep('should display disabled AD job row action button');
await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, false);
await ml.testExecution.logTestStep(
'should display enabled AD job row view datafeed counts action'
);
await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, true);
await ml.jobTable.assertJobActionViewDatafeedCountsButtonEnabled(adJobId, true);
await ml.testExecution.logTestStep(
'should display expected disabled AD job row actions'
);
await ml.jobTable.assertJobActionStartDatafeedButtonEnabled(adJobId, false);
await ml.jobTable.assertJobActionResetJobButtonEnabled(adJobId, false);
await ml.jobTable.assertJobActionCloneJobButtonEnabled(adJobId, false);
await ml.jobTable.assertJobActionEditJobButtonEnabled(adJobId, false);
await ml.jobTable.assertJobActionDeleteJobButtonEnabled(adJobId, false);
await ml.testExecution.logTestStep('should select the job');
await ml.jobTable.selectJobRow(adJobId);

View file

@ -366,6 +366,17 @@ export function MachineLearningJobTableProvider(
);
}
public async assertJobActionResetJobButtonEnabled(jobId: string, expectedValue: boolean) {
await this.ensureJobActionsMenuOpen(jobId);
const isEnabled = await testSubjects.isEnabled('mlActionButtonResetJob');
expect(isEnabled).to.eql(
expectedValue,
`Expected "reset job" action button for AD job '${jobId}' to be '${
expectedValue ? 'enabled' : 'disabled'
}' (got '${isEnabled ? 'enabled' : 'disabled'}')`
);
}
public async assertJobActionCloneJobButtonEnabled(jobId: string, expectedValue: boolean) {
await this.ensureJobActionsMenuOpen(jobId);
const isEnabled = await testSubjects.isEnabled('mlActionButtonCloneJob');
@ -377,6 +388,20 @@ export function MachineLearningJobTableProvider(
);
}
public async assertJobActionViewDatafeedCountsButtonEnabled(
jobId: string,
expectedValue: boolean
) {
await this.ensureJobActionsMenuOpen(jobId);
const isEnabled = await testSubjects.isEnabled('mlActionButtonViewDatafeedChart');
expect(isEnabled).to.eql(
expectedValue,
`Expected "view datafeed counts" action button for AD job '${jobId}' to be '${
expectedValue ? 'enabled' : 'disabled'
}' (got '${isEnabled ? 'enabled' : 'disabled'}')`
);
}
public async assertJobActionEditJobButtonEnabled(jobId: string, expectedValue: boolean) {
await this.ensureJobActionsMenuOpen(jobId);
const isEnabled = await testSubjects.isEnabled('mlActionButtonEditJob');