Commit 3ae93c72 authored by Kushal Pandya's avatar Kushal Pandya

Make Toggle, Repair and Remove actions async

parent 30227d7d
<script>
import { s__ } from '~/locale';
import Flash from '~/flash';
import statusCodes from '~/lib/utils/http_status';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import SmartInterval from '~/smart_interval';
import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import geoNodesList from './geo_nodes_list.vue';
export default {
components: {
loadingIcon,
modal,
geoNodesList,
},
props: {
......@@ -33,6 +40,12 @@
return {
isLoading: true,
hasError: false,
showModal: false,
targetNode: null,
targetNodeActionType: '',
modalKind: 'warning',
modalMessage: '',
modalActionLabel: '',
errorMessage: '',
};
},
......@@ -43,17 +56,34 @@
},
created() {
eventHub.$on('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$on('showNodeActionModal', this.showNodeActionModal);
eventHub.$on('repairNode', this.repairNode);
},
mounted() {
this.fetchGeoNodes();
},
beforeDestroy() {
eventHub.$off('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$off('showNodeActionModal', this.showNodeActionModal);
eventHub.$off('repairNode', this.repairNode);
if (this.nodePollingInterval) {
this.nodePollingInterval.stopTimer();
}
},
methods: {
setNodeActionStatus(node, status) {
Object.assign(node, { nodeActionActive: status });
},
initNodeDetailsPolling(node) {
this.nodePollingInterval = new SmartInterval({
callback: this.fetchNodeDetails.bind(this, node),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
},
fetchGeoNodes() {
this.hasError = false;
this.service.getGeoNodes()
......@@ -67,8 +97,9 @@
this.errorMessage = err;
});
},
fetchNodeDetails(nodeId) {
return this.service.getGeoNodeDetails(nodeId)
fetchNodeDetails(node) {
const nodeId = node.id;
return this.service.getGeoNodeDetails(node)
.then(res => res.data)
.then((nodeDetails) => {
const primaryNodeVersion = this.store.getPrimaryNodeVersion();
......@@ -80,19 +111,82 @@
eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId));
})
.catch((err) => {
if (err.response && err.response.status === statusCodes.NOT_FOUND) {
this.store.setNodeDetails(nodeId, {
geo_node_id: nodeId,
health: err.message,
health_status: 'Unknown',
missing_oauth_application: false,
sync_status_unavailable: true,
storage_shards_match: null,
});
eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId));
} else {
eventHub.$emit('nodeDetailsLoadFailed', nodeId, err);
}
});
},
initNodeDetailsPolling(nodeId) {
this.nodePollingInterval = new SmartInterval({
callback: this.fetchNodeDetails.bind(this, nodeId),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
repairNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.repairNode(targetNode)
.then(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Node Authentication was successfully repaired.'), 'notice');
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while repairing node'));
});
},
toggleNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.toggleNode(targetNode)
.then(res => res.data)
.then((node) => {
Object.assign(targetNode, { enabled: node.enabled, nodeActionActive: false });
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while changing node status'));
});
},
removeNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.removeNode(targetNode)
.then(() => {
this.store.removeNode(targetNode);
Flash(s__('GeoNodes|Node was successfully removed.'), 'notice');
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while removing node'));
});
},
handleNodeAction() {
this.showModal = false;
if (this.targetNodeActionType === NODE_ACTIONS.TOGGLE) {
this.toggleNode(this.targetNode);
} else if (this.targetNodeActionType === NODE_ACTIONS.REMOVE) {
this.removeNode(this.targetNode);
}
},
showNodeActionModal({ actionType, node, modalKind = 'warning', modalMessage, modalActionLabel }) {
this.targetNode = node;
this.targetNodeActionType = actionType;
this.modalKind = modalKind;
this.modalMessage = modalMessage;
this.modalActionLabel = modalActionLabel;
if (actionType === NODE_ACTIONS.TOGGLE && !node.enabled) {
this.toggleNode(this.targetNode);
} else {
this.showModal = true;
}
},
hideNodeActionModal() {
this.showModal = false;
},
},
};
</script>
......@@ -120,5 +214,14 @@
>
{{ errorMessage }}
</p>
<modal
v-show="showModal"
:title="__('Are you sure?')"
:kind="modalKind"
:text="modalMessage"
:primary-button-label="modalActionLabel"
@cancel="hideNodeActionModal"
@submit="handleNodeAction"
/>
</div>
</template>
......@@ -7,7 +7,8 @@ import eventHub from 'ee/geo_nodes/event_hub';
import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store';
import GeoNodesService from 'ee/geo_nodes/service/geo_nodes_service';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { PRIMARY_VERSION, NODE_DETAILS_PATH, mockNodes, rawMockNodeDetails } from '../mock_data';
import { NODE_ACTIONS } from 'ee/geo_nodes/constants';
import { PRIMARY_VERSION, NODE_DETAILS_PATH, mockNodes, mockNode, rawMockNodeDetails } from '../mock_data';
const createComponent = () => {
const Component = Vue.extend(appComponent);
......@@ -34,19 +35,27 @@ describe('AppComponent', () => {
mock = new MockAdapter(axios);
document.body.innerHTML += '<div class="flash-container"></div>';
mock.onGet(/(.*)\/geo_nodes$/).reply(() => [statusCode, response]);
vm = createComponent();
});
afterEach(() => {
document.querySelector('.flash-container').remove();
vm.$destroy();
mock.restore();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.isLoading).toBeTruthy();
expect(vm.hasError).toBeFalsy();
expect(vm.isLoading).toBe(true);
expect(vm.hasError).toBe(false);
expect(vm.showModal).toBe(false);
expect(vm.targetNode).toBeNull();
expect(vm.targetNodeActionType).toBe('');
expect(vm.modalKind).toBe('warning');
expect(vm.modalMessage).toBe('');
expect(vm.modalActionLabel).toBe('');
expect(vm.errorMessage).toBe('');
});
});
......@@ -60,6 +69,23 @@ describe('AppComponent', () => {
});
describe('methods', () => {
describe('setNodeActionStatus', () => {
it('sets `nodeActionActive` property with value of `status` parameter for provided `node` parameter', () => {
const node = {
nodeActionActive: false,
};
vm.setNodeActionStatus(node, true);
expect(node.nodeActionActive).toBe(true);
});
});
describe('initNodeDetailsPolling', () => {
it('initializes SmartInterval and sets it to component', () => {
vm.initNodeDetailsPolling(2);
expect(vm.nodePollingInterval).toBeDefined();
});
});
describe('fetchGeoNodes', () => {
it('calls service.getGeoNodes and sets response to the store on success', (done) => {
spyOn(vm.store, 'setNodes');
......@@ -89,35 +115,244 @@ describe('AppComponent', () => {
describe('fetchNodeDetails', () => {
it('calls service.getGeoNodeDetails and sets response to the store on success', (done) => {
mock.onGet(`${vm.service.geoNodeDetailsBasePath}/2/status.json`).reply(200, rawMockNodeDetails);
mock.onGet(mockNode.statusPath).reply(200, rawMockNodeDetails);
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(2);
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(vm.service.getGeoNodeDetails).toHaveBeenCalled();
expect(Object.keys(vm.store.state.nodeDetails).length).toBe(1);
expect(vm.store.state.nodeDetails['2']).toBeDefined();
expect(Object.keys(vm.store.state.nodeDetails).length).not.toBe(0);
expect(vm.store.state.nodeDetails['1']).toBeDefined();
done();
}, 0);
});
it('emits `nodeDetailsLoadFailed` event on failure', (done) => {
const err = 'Something went wrong';
spyOn(eventHub, '$emit');
mock.onGet(`${vm.service.geoNodeDetailsBasePath}/2/status.json`).reply(500, err);
mock.onGet(mockNode.statusPath).reply(500, {});
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(2);
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoadFailed', 2, jasmine.any(Object));
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoadFailed', mockNode.id, jasmine.any(Object));
done();
}, 0);
});
it('emits `nodeDetailsLoaded` event with fake nodeDetails object on 404 failure', (done) => {
spyOn(eventHub, '$emit');
mock.onGet(mockNode.statusPath).reply(404, {});
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoaded', jasmine.any(Object));
const nodeDetails = vm.store.state.nodeDetails['1'];
expect(nodeDetails).toBeDefined();
expect(nodeDetails.syncStatusUnavailable).toBe(true);
expect(nodeDetails.health).toBe('Request failed with status code 404');
done();
}, 0);
});
});
describe('initNodeDetailsPolling', () => {
it('initializes SmartInterval and sets it to component', () => {
vm.initNodeDetailsPolling(2);
expect(vm.nodePollingInterval).toBeDefined();
describe('repairNode', () => {
it('calls service.repairNode and shows success Flash message on request success', (done) => {
const node = { ...mockNode };
mock.onPost(node.repairPath).reply(200);
spyOn(vm.service, 'repairNode').and.callThrough();
vm.repairNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.repairNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Node Authentication was successfully repaired.');
expect(node.nodeActionActive).toBe(false);
done();
});
});
it('calls service.repairNode and shows failure Flash message on request failure', (done) => {
const node = { ...mockNode };
mock.onPost(node.repairPath).reply(500);
spyOn(vm.service, 'repairNode').and.callThrough();
vm.repairNode(node);
setTimeout(() => {
expect(vm.service.repairNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while repairing node');
expect(node.nodeActionActive).toBe(false);
done();
});
});
});
describe('toggleNode', () => {
it('calls service.toggleNode for enabling node and updates toggle button on request success', (done) => {
const node = { ...mockNode };
mock.onPut(node.basePath).reply(200, {
enabled: true,
});
spyOn(vm.service, 'toggleNode').and.callThrough();
node.enabled = false;
vm.toggleNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
expect(node.enabled).toBe(true);
expect(node.nodeActionActive).toBe(false);
done();
});
});
it('calls service.toggleNode and shows Flash error on request failure', (done) => {
const node = { ...mockNode };
mock.onPut(node.basePath).reply(500);
spyOn(vm.service, 'toggleNode').and.callThrough();
node.enabled = false;
vm.toggleNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while changing node status');
expect(node.nodeActionActive).toBe(false);
done();
});
});
});
describe('removeNode', () => {
it('calls service.removeNode for removing node and shows Flash message on request success', (done) => {
const node = { ...mockNode };
mock.onDelete(node.basePath).reply(200);
spyOn(vm.service, 'removeNode').and.callThrough();
spyOn(vm.store, 'removeNode').and.stub();
vm.removeNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.removeNode).toHaveBeenCalledWith(node);
expect(vm.store.removeNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Node was successfully removed.');
done();
});
});
it('calls service.removeNode and shows Flash message on request failure', (done) => {
const node = { ...mockNode };
mock.onDelete(node.basePath).reply(500);
spyOn(vm.service, 'removeNode').and.callThrough();
spyOn(vm.store, 'removeNode').and.stub();
vm.removeNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.removeNode).toHaveBeenCalledWith(node);
expect(vm.store.removeNode).not.toHaveBeenCalled();
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while removing node');
done();
});
});
});
describe('handleNodeAction', () => {
it('sets `showModal` to false and calls `toggleNode` when `targetNodeActionType` is `toggle`', () => {
vm.targetNode = { ...mockNode };
vm.targetNodeActionType = NODE_ACTIONS.TOGGLE;
vm.showModal = true;
spyOn(vm, 'toggleNode').and.stub();
vm.handleNodeAction();
expect(vm.showModal).toBe(false);
expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
});
it('sets `showModal` to false and calls `removeNode` when `targetNodeActionType` is `remove`', () => {
vm.targetNode = { ...mockNode };
vm.targetNodeActionType = NODE_ACTIONS.REMOVE;
vm.showModal = true;
spyOn(vm, 'removeNode').and.stub();
vm.handleNodeAction();
expect(vm.showModal).toBe(false);
expect(vm.removeNode).toHaveBeenCalledWith(vm.targetNode);
});
});
describe('showNodeActionModal', () => {
let node;
let modalKind;
let modalMessage;
let modalActionLabel;
beforeEach(() => {
node = { ...mockNode };
modalKind = 'warning';
modalMessage = 'Foobar message';
modalActionLabel = 'Disable';
});
it('sets target node and modal config props on component', () => {
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.targetNode).toBe(node);
expect(vm.targetNodeActionType).toBe(NODE_ACTIONS.TOGGLE);
expect(vm.modalKind).toBe(modalKind);
expect(vm.modalMessage).toBe(modalMessage);
expect(vm.modalActionLabel).toBe(modalActionLabel);
});
it('sets showModal to `true` when actionType is `toggle` and node is enabled', () => {
node.enabled = true;
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.showModal).toBe(true);
});
it('calls toggleNode when actionType is `toggle` and node.enabled is `false`', () => {
node.enabled = false;
spyOn(vm, 'toggleNode').and.stub();
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
});
it('sets showModal to `true` when actionType is not `toggle`', () => {
node.enabled = true;
vm.showNodeActionModal({
actionType: NODE_ACTIONS.REMOVE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.showModal).toBe(true);
});
});
describe('hideNodeActionModal', () => {
it('sets `showModal` to `false`', () => {
vm.showModal = true;
vm.hideNodeActionModal();
expect(vm.showModal).toBe(false);
});
});
});
......@@ -127,6 +362,8 @@ describe('AppComponent', () => {
spyOn(eventHub, '$on');
const vmX = createComponent();
expect(eventHub.$on).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
vmX.$destroy();
});
});
......@@ -137,6 +374,8 @@ describe('AppComponent', () => {
const vmX = createComponent();
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
});
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment