[Monitoring] Refactor the enter setup mode button (#51103) (#51374)

* Refactor this button to react for more control

* Reset this

* await this

* PR feedback

* Fix tests

* Use class

* Fix tests
This commit is contained in:
Chris Roberson 2019-11-22 12:04:36 -05:00 committed by GitHub
parent 2c52d156bb
commit 3c1695a200
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 230 additions and 114 deletions

View file

@ -78,6 +78,8 @@ exports[`SetupModeRenderer should render the flyout open 1`] = `
<EuiButton
color="danger"
fill={true}
iconSide="right"
iconType="flag"
onClick={[Function]}
size="s"
>
@ -173,6 +175,8 @@ exports[`SetupModeRenderer should render with setup mode enabled 1`] = `
<EuiButton
color="danger"
fill={true}
iconSide="right"
iconType="flag"
onClick={[Function]}
size="s"
>

View file

@ -10,7 +10,7 @@ import {
updateSetupModeData,
disableElasticsearchInternalCollection,
toggleSetupMode,
setSetupModeMenuItem
setSetupModeMenuItem,
} from '../../lib/setup_mode';
import { Flyout } from '../metricbeat_migration/flyout';
import {
@ -20,7 +20,7 @@ import {
EuiFlexItem,
EuiTextColor,
EuiIcon,
EuiSpacer
EuiSpacer,
} from '@elastic/eui';
import { findNewUuid } from './lib/find_new_uuid';
import { i18n } from '@kbn/i18n';
@ -33,11 +33,11 @@ export class SetupModeRenderer extends React.Component {
instance: null,
newProduct: null,
isSettingUpNew: false,
}
};
componentWillMount() {
const { scope, injector } = this.props;
initSetupModeState(scope, injector, (_oldData) => {
initSetupModeState(scope, injector, _oldData => {
const newState = { renderState: true };
const { productName } = this.props;
if (!productName) {
@ -95,10 +95,9 @@ export class SetupModeRenderer extends React.Component {
const uuids = Object.values(data.byUuid);
if (uuids.length && !isSettingUpNew) {
product = uuids[0];
}
else {
} else {
product = {
isNetNewUser: true
isNetNewUser: true,
};
}
}
@ -123,7 +122,7 @@ export class SetupModeRenderer extends React.Component {
return (
<Fragment>
<EuiSpacer size="xxl"/>
<EuiSpacer size="xxl" />
<EuiBottomBar>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
@ -134,9 +133,7 @@ export class SetupModeRenderer extends React.Component {
id="xpack.monitoring.setupMode.description"
defaultMessage="You are in setup mode. The ({flagIcon}) icon indicates configuration options."
values={{
flagIcon: (
<EuiIcon type="flag"/>
)
flagIcon: <EuiIcon type="flag" />,
}}
/>
</EuiTextColor>
@ -146,9 +143,16 @@ export class SetupModeRenderer extends React.Component {
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton color="danger" fill size="s" onClick={() => toggleSetupMode(false)}>
<EuiButton
color="danger"
fill
iconType="flag"
iconSide="right"
size="s"
onClick={() => toggleSetupMode(false)}
>
{i18n.translate('xpack.monitoring.setupMode.exit', {
defaultMessage: `Exit setup mode`
defaultMessage: `Exit setup mode`,
})}
</EuiButton>
</EuiFlexItem>
@ -173,8 +177,7 @@ export class SetupModeRenderer extends React.Component {
if (setupModeState.data) {
if (productName) {
data = setupModeState.data[productName];
}
else {
} else {
data = setupModeState.data;
}
}
@ -189,11 +192,12 @@ export class SetupModeRenderer extends React.Component {
productName,
updateSetupModeData,
shortcutToFinishMigration: () => this.shortcutToFinishMigration(),
openFlyout: (instance, isSettingUpNew) => this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }),
openFlyout: (instance, isSettingUpNew) =>
this.setState({ isFlyoutOpen: true, instance, isSettingUpNew }),
closeFlyout: () => this.setState({ isFlyoutOpen: false }),
},
flyoutComponent: this.getFlyout(data, meta),
bottomBarComponent: this.getBottomBar(setupModeState)
bottomBarComponent: this.getBottomBar(setupModeState),
});
}
}

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnterButton should render properly 1`] = `
<div
className="monSetupModeEnterButton__buttonWrapper"
>
<EuiButton
iconSide="right"
iconType="flag"
isLoading={false}
onClick={[Function]}
size="s"
>
Enter setup mode
</EuiButton>
</div>
`;

View file

@ -0,0 +1,6 @@
.monSetupModeEnterButton__buttonWrapper {
position: absolute;
top: $euiSize;
left: $euiSizeM;
z-index: 1;
}

View file

@ -0,0 +1 @@
@import 'enter_button';

View file

@ -0,0 +1,42 @@
/*
* 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 React from 'react';
import { shallow } from 'enzyme';
import { SetupModeEnterButton } from './enter_button';
describe('EnterButton', () => {
it('should render properly', () => {
const component = shallow(<SetupModeEnterButton enabled={true} toggleSetupMode={jest.fn()} />);
expect(component).toMatchSnapshot();
});
it('should show a loading state', () => {
const component = shallow(<SetupModeEnterButton enabled={true} toggleSetupMode={jest.fn()} />);
component.find('EuiButton').simulate('click');
expect(component.find('EuiButton').prop('isLoading')).toBe(true);
});
it('should call toggleSetupMode', () => {
const toggleSetupMode = jest.fn();
const component = shallow(
<SetupModeEnterButton enabled={true} toggleSetupMode={toggleSetupMode} />
);
component.find('EuiButton').simulate('click');
expect(toggleSetupMode).toHaveBeenCalledWith(true);
});
it('should not render if not enabled', () => {
const toggleSetupMode = jest.fn();
const component = shallow(
<SetupModeEnterButton enabled={false} toggleSetupMode={toggleSetupMode} />
);
expect(component.html()).toBe(null);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 React from 'react';
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export interface SetupModeEnterButtonProps {
enabled: boolean;
toggleSetupMode: (state: boolean) => void;
}
export const SetupModeEnterButton: React.FC<SetupModeEnterButtonProps> = (
props: SetupModeEnterButtonProps
) => {
const [isLoading, setIsLoading] = React.useState(false);
if (!props.enabled) {
return null;
}
async function enterSetupMode() {
setIsLoading(true);
await props.toggleSetupMode(true);
setIsLoading(false);
}
return (
<div className="monSetupModeEnterButton__buttonWrapper">
<EuiButton
onClick={enterSetupMode}
iconType="flag"
size="s"
iconSide="right"
isLoading={isLoading}
>
{i18n.translate('xpack.monitoring.setupMode.enter', {
defaultMessage: 'Enter setup mode',
})}
</EuiButton>
</div>
);
};

View file

@ -1,7 +1,7 @@
<div class="app-container">
<div id="setupModeNav"></div>
<kbn-top-nav
name="monitoringMain.navName"
config="topNavMenu"
app-name="'monitoring'"
show-search-bar="true"
show-date-picker="true"
@ -77,7 +77,11 @@
class="euiTab"
ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}"
>
<span ng-if="monitoringMain.tabIconClass" class="fa {{ monitoringMain.tabIconClass }}" title="{{ monitoringMain.tabIconLabel }}"></span>
<span
ng-if="monitoringMain.tabIconClass"
class="fa {{ monitoringMain.tabIconClass }}"
title="{{ monitoringMain.tabIconLabel }}"
></span>
<span
i18n-id="xpack.monitoring.esNavigation.instance.overviewLinkText"
i18n-default-message="Overview"
@ -92,10 +96,12 @@
i18n-default-message="Advanced"
>
</a>
<!-- ML Instance (for use later) -->
<!-- ML Instance (for use later) -->
<a
ng-if="monitoringMain.instance && monitoringMain.name !== 'nodes' && monitoringMain.name !== 'indices'"
class="euiTab">{{ monitoringMain.instance }}</a>
class="euiTab"
>{{ monitoringMain.instance }}</a
>
</div>
<div ng-if="monitoringMain.inKibana" class="euiTabs" role="navigation">
@ -281,8 +287,8 @@
class="euiTab"
ng-if="monitoringMain.pipelineVersions.length"
id="dropdown-elm"
ng-init="monitoringMain.dropdownLoadedHandler()">
</div>
ng-init="monitoringMain.dropdownLoadedHandler()"
></div>
</div>
<div ng-if="monitoringMain.inOverview" class="euiTabs" role="navigation">
@ -300,10 +306,10 @@
<div ng-if="monitoringMain.inListing" class="euiTabs" role="navigation">
<a
class="euiTab"
i18n-id="xpack.monitoring.clustersNavigation.clustersLinkText"
i18n-default-message="Clusters"
></a>
class="euiTab"
i18n-id="xpack.monitoring.clustersNavigation.clustersLinkText"
i18n-default-message="Clusters"
></a>
</div>
</div>
<div ng-transclude></div>

View file

@ -20,3 +20,4 @@
@import 'components/table/index';
@import 'components/logstash/pipeline_viewer/views/index';
@import 'components/elasticsearch/shard_allocation/index';
@import 'components/setup_mode/index';

View file

@ -4,11 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render } from 'react-dom';
import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { get, contains } from 'lodash';
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import { i18n } from '@kbn/i18n';
import { SetupModeEnterButton } from '../components/setup_mode/enter_button';
function isOnPage(hash) {
return contains(window.location.hash, hash);
@ -21,15 +24,15 @@ const angularState = {
const checkAngularState = () => {
if (!angularState.injector || !angularState.scope) {
throw 'Unable to interact with setup mode because the angular injector was not previously set.'
+ ' This needs to be set by calling `initSetupModeState`.';
throw 'Unable to interact with setup mode because the angular injector was not previously set.' +
' This needs to be set by calling `initSetupModeState`.';
}
};
const setupModeState = {
enabled: false,
data: null,
callbacks: []
callbacks: [],
};
export const getSetupModeState = () => setupModeState;
@ -55,26 +58,23 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false)
let url = '../api/monitoring/v1/setup/collection';
if (uuid) {
url += `/node/${uuid}`;
}
else if (!fetchWithoutClusterUuid && clusterUuid) {
} else if (!fetchWithoutClusterUuid && clusterUuid) {
url += `/cluster/${clusterUuid}`;
}
else {
} else {
url += '/cluster';
}
try {
const response = await http.post(url, { ccs });
return response.data;
}
catch (err) {
} catch (err) {
const Private = angularState.injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
}
};
const notifySetupModeDataChange = (oldData) => {
const notifySetupModeDataChange = oldData => {
setupModeState.callbacks.forEach(cb => cb(oldData));
};
@ -86,18 +86,21 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false)
const isCloud = chrome.getInjected('isOnCloud');
const hasPermissions = get(data, '_meta.hasPermissions', false);
if (isCloud || !hasPermissions) {
const text = !hasPermissions
? i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', {
defaultMessage: 'You do not have the necessary permissions to do this.'
})
: i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', {
defaultMessage: 'This feature is not available on cloud.'
let text = null;
if (!hasPermissions) {
text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', {
defaultMessage: 'You do not have the necessary permissions to do this.',
});
} else {
text = i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', {
defaultMessage: 'This feature is not available on cloud.',
});
}
angularState.scope.$evalAsync(() => {
toastNotifications.addDanger({
title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', {
defaultMessage: 'Setup mode is not available'
defaultMessage: 'Setup mode is not available',
}),
text,
});
@ -110,8 +113,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false)
const clusterUuid = globalState.cluster_uuid;
if (!clusterUuid) {
const liveClusterUuid = get(data, '_meta.liveClusterUuid');
const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {}))
.filter(node => node.isPartiallyMigrated || node.isFullyMigrated);
const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter(
node => node.isPartiallyMigrated || node.isFullyMigrated
);
if (liveClusterUuid && migratedEsNodes.length > 0) {
setNewlyDiscoveredClusterUuid(liveClusterUuid);
}
@ -128,8 +132,7 @@ export const disableElasticsearchInternalCollection = async () => {
try {
const response = await http.post(url);
return response.data;
}
catch (err) {
} catch (err) {
const Private = angularState.injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
@ -160,23 +163,12 @@ export const setSetupModeMenuItem = () => {
}
const globalState = angularState.injector.get('globalState');
const navItems = [];
if (!globalState.inSetupMode && !chrome.getInjected('isOnCloud')) {
navItems.push({
id: 'enter',
label: i18n.translate('xpack.monitoring.setupMode.enter', {
defaultMessage: 'Enter Setup Mode'
}),
run: () => toggleSetupMode(true),
testId: 'enterSetupMode'
});
}
const enabled = !globalState.inSetupMode && !chrome.getInjected('isOnCloud');
angularState.scope.topNavMenu = [...navItems];
// LOL angular
if (!angularState.scope.$$phase) {
angularState.scope.$apply();
}
render(
<SetupModeEnterButton enabled={enabled} toggleSetupMode={toggleSetupMode} />,
document.getElementById('setupModeNav')
);
};
export const initSetupModeState = async ($scope, $injector, callback) => {
@ -195,7 +187,7 @@ export const isInSetupMode = async () => {
return true;
}
const $injector = angularState.injector || await chrome.dangerouslyGetActiveInjector();
const $injector = angularState.injector || (await chrome.dangerouslyGetActiveInjector());
const globalState = $injector.get('globalState');
return globalState.inSetupMode;
};

View file

@ -13,36 +13,40 @@ let setSetupModeMenuItem;
jest.mock('./ajax_error_handler', () => ({
ajaxErrorHandlersProvider: err => {
throw err;
}
},
}));
jest.mock('react-dom', () => ({
render: jest.fn(),
}));
let data = {};
const injectorModulesMock = {
globalState: {
save: jest.fn()
save: jest.fn(),
},
Private: module => module,
$http: {
post: jest.fn().mockImplementation(() => {
return { data };
})
}),
},
$executor: {
run: jest.fn()
}
run: jest.fn(),
},
};
const angularStateMock = {
injector: {
get: module => {
return injectorModulesMock[module] || {};
}
},
},
scope: {
$apply: fn => fn && fn(),
$evalAsync: fn => fn && fn()
}
$evalAsync: fn => fn && fn(),
},
};
// We are no longer waiting for setup mode data to be fetched when enabling
@ -66,11 +70,11 @@ function setModules() {
describe('setup_mode', () => {
beforeEach(async () => {
jest.doMock('ui/chrome', () => ({
getInjected: (key) => {
getInjected: key => {
if (key === 'isOnCloud') {
return false;
}
}
},
}));
setModules();
});
@ -80,13 +84,14 @@ describe('setup_mode', () => {
let error;
try {
toggleSetupMode(true);
}
catch (err) {
} catch (err) {
error = err;
}
expect(error).toEqual('Unable to interact with setup '
+ 'mode because the angular injector was not previously set. This needs to be '
+ 'set by calling `initSetupModeState`.');
expect(error).toEqual(
'Unable to interact with setup ' +
'mode because the angular injector was not previously set. This needs to be ' +
'set by calling `initSetupModeState`.'
);
});
it('should enable toggle mode', async () => {
@ -102,11 +107,11 @@ describe('setup_mode', () => {
});
it('should set top nav config', async () => {
const render = require('react-dom').render;
initSetupModeState(angularStateMock.scope, angularStateMock.injector);
setSetupModeMenuItem();
expect(angularStateMock.scope.topNavMenu.length).toBe(1);
await toggleSetupMode(true);
expect(angularStateMock.scope.topNavMenu.length).toBe(0);
expect(render.mock.calls.length).toBe(2);
});
});
@ -115,32 +120,24 @@ describe('setup_mode', () => {
data = {};
});
it('should enable it through clicking top nav item', async () => {
initSetupModeState(angularStateMock.scope, angularStateMock.injector);
setSetupModeMenuItem();
expect(injectorModulesMock.globalState.inSetupMode).toBe(false);
await angularStateMock.scope.topNavMenu[0].run();
expect(injectorModulesMock.globalState.inSetupMode).toBe(true);
});
it('should not fetch data if on cloud', async (done) => {
it('should not fetch data if on cloud', async done => {
const addDanger = jest.fn();
jest.doMock('ui/chrome', () => ({
getInjected: (key) => {
getInjected: key => {
if (key === 'isOnCloud') {
return true;
}
}
},
}));
data = {
_meta: {
hasPermissions: true
}
hasPermissions: true,
},
};
jest.doMock('ui/notify', () => ({
toastNotifications: {
addDanger,
}
},
}));
setModules();
initSetupModeState(angularStateMock.scope, angularStateMock.injector);
@ -150,23 +147,23 @@ describe('setup_mode', () => {
expect(state.enabled).toBe(false);
expect(addDanger).toHaveBeenCalledWith({
title: 'Setup mode is not available',
text: 'This feature is not available on cloud.'
text: 'This feature is not available on cloud.',
});
done();
});
});
it('should not fetch data if the user does not have sufficient permissions', async (done) => {
it('should not fetch data if the user does not have sufficient permissions', async done => {
const addDanger = jest.fn();
jest.doMock('ui/notify', () => ({
toastNotifications: {
addDanger,
}
},
}));
data = {
_meta: {
hasPermissions: false
}
hasPermissions: false,
},
};
setModules();
initSetupModeState(angularStateMock.scope, angularStateMock.injector);
@ -176,26 +173,26 @@ describe('setup_mode', () => {
expect(state.enabled).toBe(false);
expect(addDanger).toHaveBeenCalledWith({
title: 'Setup mode is not available',
text: 'You do not have the necessary permissions to do this.'
text: 'You do not have the necessary permissions to do this.',
});
done();
});
});
it('should set the newly discovered cluster uuid', async (done) => {
it('should set the newly discovered cluster uuid', async done => {
const clusterUuid = '1ajy';
data = {
_meta: {
liveClusterUuid: clusterUuid,
hasPermissions: true
hasPermissions: true,
},
elasticsearch: {
byUuid: {
123: {
isPartiallyMigrated: true
}
}
}
isPartiallyMigrated: true,
},
},
},
};
initSetupModeState(angularStateMock.scope, angularStateMock.injector);
await toggleSetupMode(true);
@ -205,20 +202,20 @@ describe('setup_mode', () => {
});
});
it('should fetch data for a given cluster', async (done) => {
it('should fetch data for a given cluster', async done => {
const clusterUuid = '1ajy';
data = {
_meta: {
liveClusterUuid: clusterUuid,
hasPermissions: true
hasPermissions: true,
},
elasticsearch: {
byUuid: {
123: {
isPartiallyMigrated: true
}
}
}
isPartiallyMigrated: true,
},
},
},
};
initSetupModeState(angularStateMock.scope, angularStateMock.injector);