Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
export const SECTIONS = {
AUTO_FOLLOW_PATTERN: 'autoFollowPattern',
INDEX_FOLLOWER: 'indexFollower',
REMOTE_CLUSTER: 'remoteCluster'
REMOTE_CLUSTER: 'remoteCluster',
CCR_STATS: 'ccrStats',
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
loadAutoFollowPatterns,
openAutoFollowPatternDetailPanel as openDetailPanel,
closeAutoFollowPatternDetailPanel as closeDetailPanel,
loadAutoFollowStats,
} from '../../../store/actions';
import { AutoFollowPatternList as AutoFollowPatternListView } from './auto_follow_pattern_list';

Expand All @@ -39,6 +40,7 @@ const mapDispatchToProps = dispatch => ({
closeDetailPanel: () => {
dispatch(closeDetailPanel());
},
loadAutoFollowStats: () => dispatch(loadAutoFollowStats())
});

export const AutoFollowPatternList = connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AutoFollowPatternList = injectI18n(
class extends PureComponent {
static propTypes = {
loadAutoFollowPatterns: PropTypes.func,
loadAutoFollowStats: PropTypes.func,
autoFollowPatterns: PropTypes.array,
apiStatus: PropTypes.string,
apiError: PropTypes.object,
Expand All @@ -31,6 +32,7 @@ export const AutoFollowPatternList = injectI18n(

componentDidMount() {
this.props.loadAutoFollowPatterns();
this.props.loadAutoFollowStats();

// Interval to load auto-follow patterns in the background passing "true" to the fetch method
this.interval = setInterval(() => this.props.loadAutoFollowPatterns(true), REFRESH_RATE_MS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,54 @@ export class DetailPanelUi extends Component {
/>
</EuiLink>
</EuiDescriptionList>
<EuiSpacer size="l" />
{this.renderAutoFollowPatternErrors()}
</EuiFlyoutBody>
</Fragment>
);
}

renderAutoFollowPatternErrors() {
const { autoFollowPattern } = this.props;

if (!autoFollowPattern.errors.length) {
return null;
}

return (
<Fragment>
<EuiFlexGroup
justifyContent="flexStart"
alignItems="center"
gutterSize="s"
>
<EuiFlexItem grow={false}>
<EuiIcon type="alert" color="danger" />
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.recentErrorsTitle"
defaultMessage="Recent errors"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText>
<ul>
{autoFollowPattern.errors.map((error, i) => (
<li key={i}>{error.autoFollowException.reason}</li>
))}
</ul>
</EuiText>
</Fragment>
);
}

renderContent() {
const {
apiStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ export const deleteAutoFollowPattern = (id) => {

return httpClient.delete(`${apiPrefix}/auto_follow_patterns/${ids}`).then(extractData);
};

export const loadAutoFollowStats = () => (
httpClient.get(`${apiPrefix}/stats/auto-follow`).then(extractData)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.
*/


export const parseAutoFollowError = (error) => {
if (!error.leaderIndex) {
return null;
}

const { leaderIndex, autoFollowException } = error;
const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':'));

return {
id,
leaderIndex,
autoFollowException
};
};

/**
* Parse an array of auto-follow pattern errors and returns
* an object where each key is an auto-follow pattern id
*/
export const parseAutoFollowErrors = (recentAutoFollowErrors, maxErrorsToShow = 5) => (
recentAutoFollowErrors
.map(parseAutoFollowError)
.filter(error => error !== null)
.reduce((byId, error) => {
if (!byId[error.id]) {
byId[error.id] = [];
}

if (byId[error.id].length === maxErrorsToShow) {
return byId;
}

byId[error.id].push(error);
return byId;
}, {})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { parseAutoFollowErrors } from './auto_follow_errors';

describe('Auto-follow pattern errors service', () => {
it('should convert an array of error to an object where each key is an auto-follow pattern id', () => {
const esErrors = [{
leaderIndex: 'some:id::kibana_sample_4',
autoFollowException: { type: 'exception', reason: 'Error 1' }
},
{
leaderIndex: 'another-id:mock:kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 2' }
}, {
leaderIndex: 'some:id::kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 3' }
}];

const expected = {
'another-id:mock': [{
id: 'another-id:mock',
leaderIndex: 'another-id:mock:kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 2' }
}],
'some:id:': [{
id: 'some:id:',
leaderIndex: 'some:id::kibana_sample_4',
autoFollowException: { type: 'exception', reason: 'Error 1' }
}, {
id: 'some:id:',
leaderIndex: 'some:id::kibana_sample_5',
autoFollowException: { type: 'exception', reason: 'Error 3' }
}],
};

expect(parseAutoFollowErrors(esErrors)).toEqual(expected);
});

it('should limit the number of error to show for each pattern', () => {
const esErrors = [
{ leaderIndex: 'my-id:kibana-1' },
{ leaderIndex: 'my-id:kibana-2' },
{ leaderIndex: 'my-id:kibana-3' },
{ leaderIndex: 'my-id:kibana-4' },
{ leaderIndex: 'my-id:kibana-5' },
{ leaderIndex: 'my-id:kibana-6' },
{ leaderIndex: 'my-id:kibana-7' },
];

expect(parseAutoFollowErrors(esErrors)['my-id'].length).toEqual(5);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ export const AUTO_FOLLOW_PATTERN_CREATE = 'AUTO_FOLLOW_PATTERN_CREATE';
export const AUTO_FOLLOW_PATTERN_UPDATE = 'AUTO_FOLLOW_PATTERN_UPDATE';
export const AUTO_FOLLOW_PATTERN_DELETE = 'AUTO_FOLLOW_PATTERN_DELETE';
export const AUTO_FOLLOW_PATTERN_DETAIL_PANEL = 'AUTO_FOLLOW_PATTERN_DETAIL_PANEL';

// Stats
export const AUTO_FOLLOW_STATS_LOAD = 'AUTO_FOLLOW_STATS_LOAD';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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 { SECTIONS } from '../../constants';
import { loadAutoFollowStats as loadAutoFollowStatsRequest } from '../../services/api';
import * as t from '../action_types';
import { sendApiRequest } from './api';

const { CCR_STATS: scope } = SECTIONS;

export const loadAutoFollowStats = () =>
sendApiRequest({
label: t.AUTO_FOLLOW_STATS_LOAD,
scope,
handler: async () => (
await loadAutoFollowStatsRequest()
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
export * from './auto_follow_pattern';

export * from './api';
export * from './ccr';
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
import routing from '../../services/routing';
import * as t from '../action_types';
import { extractQueryParams } from '../../services/query_params';
import { loadAutoFollowStats } from '../actions';

export const autoFollowPatternMiddleware = () => next => action => {
export const autoFollowPatternMiddleware = ({ dispatch }) => next => action => {
const { type, payload: name } = action;
const { history } = routing.reactRouter;
const search = history.location.search;
Expand All @@ -28,9 +29,10 @@ export const autoFollowPatternMiddleware = () => next => action => {
history.replace({
search: `?pattern=${encodeURIComponent(name)}`,
});

dispatch(loadAutoFollowStats());
}
}

break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import { combineReducers } from 'redux';
import { reducer as api } from './api';
import { reducer as autoFollowPattern } from './auto_follow_pattern';
import { reducer as stats } from './stats';

export const ccr = combineReducers({
autoFollowPattern,
api,
stats,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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 * as t from '../action_types';
import { parseAutoFollowErrors } from '../../services/auto_follow_errors';

const initialState = {
autoFollow: null,
};

const success = action => `${action}_SUCCESS`;

export const reducer = (state = initialState, action) => {
switch (action.type) {
case success(t.AUTO_FOLLOW_STATS_LOAD): {
const { recentAutoFollowErrors, ...rest } = action.payload;
return { ...state, autoFollow: {
...rest,
recentAutoFollowErrors: parseAutoFollowErrors(recentAutoFollowErrors)
} };
}
default:
return state;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const isApiAuthorized = (scope) => createSelector(getApiError(scope), (er
return error.status !== 403;
});

// Stats
export const getStatsState = (state) => state.stats;
export const getAutoFollowStats = createSelector(getStatsState, (statsState) => statsState.autoFollow);

// Auto-follow pattern
export const getAutoFollowPatternState = (state) => state.autoFollowPattern;
export const getAutoFollowPatterns = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => autoFollowPatternsState.byId);
Expand All @@ -32,10 +36,15 @@ export const getSelectedAutoFollowPattern = createSelector(getAutoFollowPatternS
export const isAutoFollowPatternDetailPanelOpen = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
return !!autoFollowPatternsState.detailPanelId;
});
export const getDetailPanelAutoFollowPattern = createSelector(getAutoFollowPatternState, (autoFollowPatternsState) => {
if(!autoFollowPatternsState.detailPanelId) {
return null;
}
return autoFollowPatternsState.byId[autoFollowPatternsState.detailPanelId];
});
export const getDetailPanelAutoFollowPattern = createSelector(
getAutoFollowPatternState, getAutoFollowStats, (autoFollowPatternsState, autoFollowStatsState) => {
if(!autoFollowPatternsState.detailPanelId) {
return null;
}
const { detailPanelId } = autoFollowPatternsState;
const autoFollowPattern = autoFollowPatternsState.byId[detailPanelId];
const errors = autoFollowStatsState && autoFollowStatsState.recentAutoFollowErrors[detailPanelId] || [];
return autoFollowPattern ? { ...autoFollowPattern, errors } : null;
});
export const getListAutoFollowPatterns = createSelector(getAutoFollowPatterns, (autoFollowPatterns) => objectToArray(autoFollowPatterns));

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if (chrome.getInjected('ccrUiEnabled')) {
controller: class CrossClusterReplicationController {
constructor($scope, $route, $http) {
/**
* React-router's <Redirect> does not play wall with the angular router. It will cause this controller
* React-router's <Redirect> does not play well with the angular router. It will cause this controller
* to re-execute without the $destroy handler being called. This means that the app will be mounted twice
* creating a memory leak when leaving (only 1 app will be unmounted).
* To avoid this, we unmount the React app each time we enter the controller.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
needBody: true,
method: 'DELETE'
});

ccr.stats = ca({
urls: [
{
fmt: '/_ccr/stats',
}
],
method: 'GET'
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = `
Object {
"autoFollowedClusters": Array [
Object {
"clusterName": "new-york",
"lastSeenMetadataVersion": 15,
"timeSinceLastCheckMillis": 2426,
},
],
"numberOfFailedFollowIndices": 0,
"numberOfFailedRemoteClusterStateRequests": 0,
"numberOfSuccessfulFollowIndices": 0,
"recentAutoFollowErrors": Array [
Object {
"autoFollowException": Object {
"reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]",
"type": "exception",
},
"leaderIndex": "pattern-1:kibana_sample_1",
},
Object {
"autoFollowException": Object {
"reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]",
"type": "exception",
},
"leaderIndex": "pattern-2:kibana_sample_1",
},
],
}
`;
Loading