Skip to content

Commit 29ff4bc

Browse files
authored
[CCR] Surface license errors in-app and refine permissions error UI. (#29228)
* Fix camelcasing bug in XPackInfo. * Silently swallow API error when checking for index name availability. * Fix typo in followerIndexForm fatal error. * Add permissions check before allowing user to access the app.
1 parent 5d1e1be commit 29ff4bc

File tree

14 files changed

+375
-159
lines changed

14 files changed

+375
-159
lines changed

x-pack/plugins/cross_cluster_replication/public/app/app.js

Lines changed: 188 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,25 @@
77
import React, { Component } from 'react';
88
import PropTypes from 'prop-types';
99
import { Route, Switch, Redirect } from 'react-router-dom';
10+
import chrome from 'ui/chrome';
11+
import { fatalError } from 'ui/notify';
12+
import { i18n } from '@kbn/i18n';
13+
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
14+
15+
import {
16+
EuiEmptyPrompt,
17+
EuiFlexGroup,
18+
EuiFlexItem,
19+
EuiLoadingSpinner,
20+
EuiPageContent,
21+
EuiTitle,
22+
} from '@elastic/eui';
1023

11-
import routing from './services/routing';
1224
import { BASE_PATH } from '../../common/constants';
25+
import { SectionUnauthorized, SectionError } from './components';
26+
import routing from './services/routing';
27+
import { isAvailable, isActive, getReason } from './services/license';
28+
import { loadPermissions } from './services/api';
1329

1430
import {
1531
CrossClusterReplicationHome,
@@ -19,47 +35,181 @@ import {
1935
FollowerIndexEdit,
2036
} from './sections';
2137

22-
export class App extends Component {
23-
static contextTypes = {
24-
router: PropTypes.shape({
25-
history: PropTypes.shape({
26-
push: PropTypes.func.isRequired,
27-
createHref: PropTypes.func.isRequired
38+
export const App = injectI18n(
39+
class extends Component {
40+
static contextTypes = {
41+
router: PropTypes.shape({
42+
history: PropTypes.shape({
43+
push: PropTypes.func.isRequired,
44+
createHref: PropTypes.func.isRequired
45+
}).isRequired
2846
}).isRequired
29-
}).isRequired
30-
}
47+
}
3148

32-
constructor(...args) {
33-
super(...args);
34-
this.registerRouter();
35-
}
49+
constructor(...args) {
50+
super(...args);
51+
this.registerRouter();
3652

37-
componentWillMount() {
38-
routing.userHasLeftApp = false;
39-
}
53+
this.state = {
54+
isFetchingPermissions: false,
55+
fetchPermissionError: undefined,
56+
hasPermission: false,
57+
missingPermissions: [],
58+
};
59+
}
4060

41-
componentWillUnmount() {
42-
routing.userHasLeftApp = true;
43-
}
61+
componentWillMount() {
62+
routing.userHasLeftApp = false;
63+
}
4464

45-
registerRouter() {
46-
const { router } = this.context;
47-
routing.reactRouter = router;
48-
}
65+
componentDidMount() {
66+
this.checkPermissions();
67+
}
4968

50-
render() {
51-
return (
52-
<div>
53-
<Switch>
54-
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}/follower_indices`} />
55-
<Route exact path={`${BASE_PATH}/auto_follow_patterns/add`} component={AutoFollowPatternAdd} />
56-
<Route exact path={`${BASE_PATH}/auto_follow_patterns/edit/:id`} component={AutoFollowPatternEdit} />
57-
<Route exact path={`${BASE_PATH}/follower_indices/add`} component={FollowerIndexAdd} />
58-
<Route exact path={`${BASE_PATH}/follower_indices/edit/:id`} component={FollowerIndexEdit} />
59-
<Route exact path={`${BASE_PATH}/:section`} component={CrossClusterReplicationHome} />
60-
</Switch>
61-
</div>
62-
);
63-
}
64-
}
69+
componentWillUnmount() {
70+
routing.userHasLeftApp = true;
71+
}
72+
73+
async checkPermissions() {
74+
this.setState({
75+
isFetchingPermissions: true,
76+
});
6577

78+
try {
79+
const { hasPermission, missingPermissions } = await loadPermissions();
80+
81+
this.setState({
82+
isFetchingPermissions: false,
83+
hasPermission,
84+
missingPermissions,
85+
});
86+
} catch (error) {
87+
// Expect an error in the shape provided by Angular's $http service.
88+
if (error && error.data) {
89+
return this.setState({
90+
isFetchingPermissions: false,
91+
fetchPermissionError: error,
92+
});
93+
}
94+
95+
// This error isn't an HTTP error, so let the fatal error screen tell the user something
96+
// unexpected happened.
97+
fatalError(error, i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', {
98+
defaultMessage: 'Cross Cluster Replication app',
99+
}));
100+
}
101+
}
102+
103+
registerRouter() {
104+
const { router } = this.context;
105+
routing.reactRouter = router;
106+
}
107+
108+
render() {
109+
const {
110+
isFetchingPermissions,
111+
fetchPermissionError,
112+
hasPermission,
113+
missingPermissions,
114+
} = this.state;
115+
116+
if (!isAvailable() || !isActive()) {
117+
return (
118+
<SectionUnauthorized
119+
title={(
120+
<FormattedMessage
121+
id="xpack.crossClusterReplication.app.licenseErrorTitle"
122+
defaultMessage="License error"
123+
/>
124+
)}
125+
>
126+
{getReason()}
127+
{' '}
128+
<a href={chrome.addBasePath('/app/kibana#/management/elasticsearch/license_management/home')}>
129+
<FormattedMessage
130+
id="xpack.crossClusterReplication.app.licenseErrorLinkText"
131+
defaultMessage="Manage your license."
132+
/>
133+
</a>
134+
</SectionUnauthorized>
135+
);
136+
}
137+
138+
if (isFetchingPermissions) {
139+
return (
140+
<EuiPageContent horizontalPosition="center">
141+
<EuiFlexGroup>
142+
<EuiFlexItem>
143+
<EuiLoadingSpinner size="xl"/>
144+
</EuiFlexItem>
145+
146+
<EuiFlexItem>
147+
<EuiTitle>
148+
<h2>
149+
<FormattedMessage
150+
id="xpack.crossClusterReplication.app.permissionCheckTitle"
151+
defaultMessage="Checking permissions..."
152+
/>
153+
</h2>
154+
</EuiTitle>
155+
</EuiFlexItem>
156+
</EuiFlexGroup>
157+
</EuiPageContent>
158+
);
159+
}
160+
161+
if (fetchPermissionError) {
162+
return (
163+
<SectionError
164+
title={(
165+
<FormattedMessage
166+
id="xpack.crossClusterReplication.app.permissionCheckErrorTitle"
167+
defaultMessage="Error checking permissions"
168+
/>
169+
)}
170+
error={fetchPermissionError}
171+
/>
172+
);
173+
}
174+
175+
if (!hasPermission) {
176+
return (
177+
<EuiPageContent horizontalPosition="center">
178+
<EuiEmptyPrompt
179+
iconType="securityApp"
180+
iconColor={null}
181+
title={
182+
<h2>
183+
<FormattedMessage
184+
id="xpack.crossClusterReplication.app.deniedPermissionTitle"
185+
defaultMessage="Permission denied"
186+
/>
187+
</h2>}
188+
body={
189+
<p>
190+
<FormattedMessage
191+
id="xpack.crossClusterReplication.app.deniedPermissionDescription"
192+
defaultMessage="You do not have required permissions ({permissions}) for Cross Cluster Replication."
193+
values={{ permissions: missingPermissions.join(', ') }}
194+
/>
195+
</p>}
196+
/>
197+
</EuiPageContent>
198+
);
199+
}
200+
201+
return (
202+
<div>
203+
<Switch>
204+
<Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}/follower_indices`} />
205+
<Route exact path={`${BASE_PATH}/auto_follow_patterns/add`} component={AutoFollowPatternAdd} />
206+
<Route exact path={`${BASE_PATH}/auto_follow_patterns/edit/:id`} component={AutoFollowPatternEdit} />
207+
<Route exact path={`${BASE_PATH}/follower_indices/add`} component={FollowerIndexAdd} />
208+
<Route exact path={`${BASE_PATH}/follower_indices/edit/:id`} component={FollowerIndexEdit} />
209+
<Route exact path={`${BASE_PATH}/:section`} component={CrossClusterReplicationHome} />
210+
</Switch>
211+
</div>
212+
);
213+
}
214+
}
215+
);

x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,15 @@ export const FollowerIndexForm = injectI18n(
191191
if (error && error.data) {
192192
// All validation does is check for a name collision, so we can just let the user attempt
193193
// to save the follower index and get an error back from the API.
194-
this.setState({
194+
return this.setState({
195195
isValidatingIndexName: false,
196196
});
197197
}
198198

199199
// This error isn't an HTTP error, so let the fatal error screen tell the user something
200200
// unexpected happened.
201201
fatalError(error, i18n.translate('xpack.crossClusterReplication.followerIndexForm.indexNameValidationFatalErrorTitle', {
202-
defaultMessage: 'Follower Index Forn index name validation',
202+
defaultMessage: 'Follower Index Form index name validation',
203203
}));
204204
}
205205
};

x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@
55
*/
66

77
import React, { Fragment } from 'react';
8-
import { injectI18n } from '@kbn/i18n/react';
98

109
import { EuiCallOut } from '@elastic/eui';
1110

12-
export function SectionUnauthorizedUI({ intl, children }) {
13-
const title = intl.formatMessage({
14-
id: 'xpack.crossClusterReplication.remoteClusterList.noPermissionTitle',
15-
defaultMessage: 'Permission error',
16-
});
11+
export function SectionUnauthorized({ title, children }) {
1712
return (
1813
<Fragment>
1914
<EuiCallOut
@@ -26,5 +21,3 @@ export function SectionUnauthorizedUI({ intl, children }) {
2621
</Fragment>
2722
);
2823
}
29-
30-
export const SectionUnauthorized = injectI18n(SectionUnauthorizedUI);

0 commit comments

Comments
 (0)