mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* 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:
parent
be0f898c58
commit
35cea77c81
13 changed files with 210 additions and 29 deletions
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,6 +63,10 @@ export {
|
|||
deserializeJobs,
|
||||
} from './jobs';
|
||||
|
||||
export {
|
||||
createNoticeableDelay,
|
||||
} from './noticeable_delay';
|
||||
|
||||
export {
|
||||
extractQueryParams,
|
||||
} from './query_params';
|
||||
|
|
|
@ -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]);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue