[Rollups] Disable deletion of started rollup jobs (#24878) (#24995)

* Disable deletion of started rollup jobs.
* Update empty prompt icon.
* Add isUpdating selector and display a spinner instead of the action button when jobs are being updated.
* Localize Navigation component.
* Add noticeable delay of 300ms show spinner displays and doesn't flicker.
This commit is contained in:
CJ Cenizal 2018-11-01 12:09:15 -07:00 committed by GitHub
parent be0f898c58
commit 35cea77c81
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 210 additions and 29 deletions

View file

@ -5,13 +5,23 @@
*/
import { connect } from 'react-redux';
import { JobActionMenu as JobActionMenuComponent } from './job_action_menu';
import { isUpdating } from '../../../store/selectors';
import {
startJobs,
stopJobs,
deleteJobs,
} from '../../../store/actions';
import { JobActionMenu as JobActionMenuComponent } from './job_action_menu';
const mapStateToProps = (state) => {
return {
isUpdating: isUpdating(state),
};
};
const mapDispatchToProps = (dispatch, { jobs }) => {
const jobIds = jobs.map(job => job.id);
return {
@ -27,4 +37,4 @@ const mapDispatchToProps = (dispatch, { jobs }) => {
};
};
export const JobActionMenu = connect(undefined, mapDispatchToProps)(JobActionMenuComponent);
export const JobActionMenu = connect(mapStateToProps, mapDispatchToProps)(JobActionMenuComponent);

View file

@ -6,13 +6,17 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { injectI18n } from '@kbn/i18n/react';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiContextMenu,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLoadingSpinner,
EuiPopover,
EuiText,
} from '@elastic/eui';
import { ConfirmDeleteModal } from './confirm_delete_modal';
@ -23,6 +27,7 @@ class JobActionMenuUi extends Component {
startJobs: PropTypes.func.isRequired,
stopJobs: PropTypes.func.isRequired,
deleteJobs: PropTypes.func.isRequired,
isUpdating: PropTypes.bool.isRequired,
iconSide: PropTypes.string,
anchorPosition: PropTypes.string,
label: PropTypes.node,
@ -89,19 +94,21 @@ class JobActionMenuUi extends Component {
});
}
items.push({
name: intl.formatMessage({
id: 'xpack.rollupJobs.jobActionMenu.deleteJobLabel',
defaultMessage: 'Delete {isSingleSelection, plural, one {job} other {jobs}}',
}, {
isSingleSelection,
}),
icon: <EuiIcon type="trash" />,
onClick: () => {
this.closePopover();
this.openDeleteConfirmationModal();
},
});
if (this.canDeleteJobs()) {
items.push({
name: intl.formatMessage({
id: 'xpack.rollupJobs.jobActionMenu.deleteJobLabel',
defaultMessage: 'Delete {isSingleSelection, plural, one {job} other {jobs}}',
}, {
isSingleSelection,
}),
icon: <EuiIcon type="trash" />,
onClick: () => {
this.closePopover();
this.openDeleteConfirmationModal();
},
});
}
const panelTree = {
id: 0,
@ -145,6 +152,12 @@ class JobActionMenuUi extends Component {
return jobs.some(job => job.status === 'started');
}
canDeleteJobs() {
const { jobs } = this.props;
const areAllJobsStopped = jobs.findIndex(job => job.status === 'started') === -1;
return areAllJobsStopped;
}
confirmDeleteModal = () => {
const { showDeleteConfirmation } = this.state;
@ -179,7 +192,27 @@ class JobActionMenuUi extends Component {
};
render() {
const { intl } = this.props;
const { intl, isUpdating } = this.props;
if (isUpdating) {
return (
<EuiFlexGroup justifyContent="flexStart" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l"/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<FormattedMessage
id="xpack.rollupJobs.jobActionMenu.updatingText"
defaultMessage="Updating"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}
const jobCount = this.props.jobs.length;
const {

View file

@ -6,6 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
@ -16,7 +17,7 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
export const Navigation = ({
const NavigationUi = ({
isSaving,
hasNextStep,
hasPreviousStep,
@ -33,7 +34,12 @@ export const Navigation = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>Saving</EuiText>
<EuiText>
<FormattedMessage
id="xpack.rollupJobs.create.navigation.savingText"
defaultMessage="Saving"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -47,7 +53,10 @@ export const Navigation = ({
iconType="arrowLeft"
onClick={goToPreviousStep}
>
Back
<FormattedMessage
id="xpack.rollupJobs.create.backButton.label"
defaultMessage="Back"
/>
</EuiButtonEmpty>
</EuiFlexItem>
);
@ -64,7 +73,10 @@ export const Navigation = ({
isDisabled={!canGoToNextStep}
fill
>
Next
<FormattedMessage
id="xpack.rollupJobs.create.nextButton.label"
defaultMessage="Next"
/>
</EuiButton>
</EuiFlexItem>
);
@ -77,7 +89,10 @@ export const Navigation = ({
onClick={save}
fill
>
Save
<FormattedMessage
id="xpack.rollupJobs.create.saveButton.label"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
);
@ -91,7 +106,7 @@ export const Navigation = ({
);
};
Navigation.propTypes = {
NavigationUi.propTypes = {
hasNextStep: PropTypes.bool.isRequired,
hasPreviousStep: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
@ -100,3 +115,5 @@ Navigation.propTypes = {
save: PropTypes.func.isRequired,
canGoToNextStep: PropTypes.bool.isRequired,
};
export const Navigation = injectI18n(NavigationUi);

View file

@ -5,7 +5,6 @@
*/
import { connect } from 'react-redux';
import { JobList as JobListView } from './job_list';
import {
getPageOfJobs,
@ -20,6 +19,8 @@ import {
closeDetailPanel,
} from '../../store/actions';
import { JobList as JobListView } from './job_list';
const mapStateToProps = (state) => {
return {
jobs: getPageOfJobs(state),

View file

@ -87,6 +87,7 @@ export class JobListUi extends Component {
// this page.
this.props.closeDetailPanel();
}
getHeaderSection() {
return (
<EuiPageContentHeaderSection>
@ -101,6 +102,7 @@ export class JobListUi extends Component {
</EuiPageContentHeaderSection>
);
}
renderNoPermission() {
const { intl } = this.props;
const title = intl.formatMessage({
@ -124,10 +126,11 @@ export class JobListUi extends Component {
</Fragment>
);
}
renderEmpty() {
return (
<EuiEmptyPrompt
iconType="managementApp"
iconType="indexRollupApp"
title={(
<h1>
<FormattedMessage

View file

@ -63,6 +63,10 @@ export {
deserializeJobs,
} from './jobs';
export {
createNoticeableDelay,
} from './noticeable_delay';
export {
extractQueryParams,
} from './query_params';

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
// Ensure an API request resolves after a brief yet noticeable delay, giving users time to recognize
// a spinner or other feedback without it flickering.
export function createNoticeableDelay(promise) {
const noticeableDelay = new Promise(resolve => setTimeout(() => {
resolve();
}, 300));
return Promise.all([promise, noticeableDelay]);
}

View file

@ -22,6 +22,11 @@ export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS';
export const CREATE_JOB_FAILURE = 'CREATE_JOB_FAILURE';
export const CLEAR_CREATE_JOB_ERRORS = 'CLEAR_CREATE_JOB_ERRORS';
// Update job (start, stop, delete)
export const UPDATE_JOB_START = 'UPDATE_JOB_START';
export const UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS';
export const UPDATE_JOB_FAILURE = 'UPDATE_JOB_FAILURE';
// Table state
export const FILTER_CHANGED = 'FILTER_CHANGED';
export const PAGE_CHANGED = 'PAGE_CHANGED';

View file

@ -8,25 +8,57 @@ import { toastNotifications } from 'ui/notify';
import {
startJobs as sendStartJobsRequest,
stopJobs as sendStopJobsRequest,
createNoticeableDelay,
} from '../../services';
import {
UPDATE_JOB_START,
UPDATE_JOB_SUCCESS,
UPDATE_JOB_FAILURE,
} from '../action_types';
import { refreshJobs } from './refresh_jobs';
export const startJobs = (jobIds) => async (dispatch) => {
dispatch({
type: UPDATE_JOB_START,
});
try {
await sendStartJobsRequest(jobIds);
await createNoticeableDelay(sendStartJobsRequest(jobIds));
} catch (error) {
dispatch({
type: UPDATE_JOB_FAILURE,
});
return toastNotifications.addDanger(error.data.message);
}
dispatch({
type: UPDATE_JOB_SUCCESS,
});
dispatch(refreshJobs());
};
export const stopJobs = (jobIds) => async (dispatch) => {
dispatch({
type: UPDATE_JOB_START,
});
try {
await sendStopJobsRequest(jobIds);
await createNoticeableDelay(sendStopJobsRequest(jobIds));
} catch (error) {
dispatch({
type: UPDATE_JOB_FAILURE,
});
return toastNotifications.addDanger(error.data.message);
}
dispatch({
type: UPDATE_JOB_SUCCESS,
});
dispatch(refreshJobs());
};

View file

@ -6,16 +6,30 @@
import { toastNotifications } from 'ui/notify';
import { deleteJobs as sendDeleteJobsRequest } from '../../services';
import { deleteJobs as sendDeleteJobsRequest, createNoticeableDelay } from '../../services';
import { getDetailPanelJob } from '../selectors';
import {
UPDATE_JOB_START,
UPDATE_JOB_SUCCESS,
UPDATE_JOB_FAILURE,
} from '../action_types';
import { refreshJobs } from './refresh_jobs';
import { closeDetailPanel } from './detail_panel';
export const deleteJobs = (jobIds) => async (dispatch, getState) => {
dispatch({
type: UPDATE_JOB_START,
});
try {
await sendDeleteJobsRequest(jobIds);
await createNoticeableDelay(sendDeleteJobsRequest(jobIds));
} catch (error) {
dispatch({
type: UPDATE_JOB_FAILURE,
});
return toastNotifications.addDanger(error.data.message);
}
@ -31,5 +45,9 @@ export const deleteJobs = (jobIds) => async (dispatch, getState) => {
dispatch(closeDetailPanel());
}
dispatch({
type: UPDATE_JOB_SUCCESS,
});
dispatch(refreshJobs());
};

View file

@ -9,10 +9,12 @@ import { jobs } from './jobs';
import { tableState } from './table_state';
import { detailPanel } from './detail_panel';
import { createJob } from './create_job';
import { updateJob } from './update_job';
export const rollupJobs = combineReducers({
jobs,
tableState,
detailPanel,
createJob,
updateJob,
});

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 {
UPDATE_JOB_START,
UPDATE_JOB_SUCCESS,
UPDATE_JOB_FAILURE,
} from '../action_types';
const initialState = {
isUpdating: false,
error: undefined,
};
export function updateJob(state = initialState, action) {
const { type } = action;
switch (type) {
case UPDATE_JOB_START:
return {
isUpdating: true,
};
case UPDATE_JOB_SUCCESS:
return {
isUpdating: false,
};
case UPDATE_JOB_FAILURE:
return {
isUpdating: false,
};
default:
return state;
}
}

View file

@ -24,6 +24,7 @@ export const isLoading = (state) => state.jobs.isLoading;
export const jobLoadError = (state) => state.jobs.jobLoadError;
export const isSaving = (state) => state.createJob.isSaving;
export const getCreateJobError = (state) => state.createJob.error;
export const isUpdating = (state) => state.updateJob.isUpdating;
export const getJobStatusByJobName = (state, jobName) => {
const jobs = getJobs(state);