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

Make Toggle, Repair and Remove actions async

parent 30227d7d
<script> <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 loadingIcon from '~/vue_shared/components/loading_icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import geoNodesList from './geo_nodes_list.vue'; import geoNodesList from './geo_nodes_list.vue';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
modal,
geoNodesList, geoNodesList,
}, },
props: { props: {
...@@ -33,6 +40,12 @@ ...@@ -33,6 +40,12 @@
return { return {
isLoading: true, isLoading: true,
hasError: false, hasError: false,
showModal: false,
targetNode: null,
targetNodeActionType: '',
modalKind: 'warning',
modalMessage: '',
modalActionLabel: '',
errorMessage: '', errorMessage: '',
}; };
}, },
...@@ -43,17 +56,34 @@ ...@@ -43,17 +56,34 @@
}, },
created() { created() {
eventHub.$on('pollNodeDetails', this.initNodeDetailsPolling); eventHub.$on('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$on('showNodeActionModal', this.showNodeActionModal);
eventHub.$on('repairNode', this.repairNode);
}, },
mounted() { mounted() {
this.fetchGeoNodes(); this.fetchGeoNodes();
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('pollNodeDetails', this.initNodeDetailsPolling); eventHub.$off('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$off('showNodeActionModal', this.showNodeActionModal);
eventHub.$off('repairNode', this.repairNode);
if (this.nodePollingInterval) { if (this.nodePollingInterval) {
this.nodePollingInterval.stopTimer(); this.nodePollingInterval.stopTimer();
} }
}, },
methods: { 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() { fetchGeoNodes() {
this.hasError = false; this.hasError = false;
this.service.getGeoNodes() this.service.getGeoNodes()
...@@ -67,8 +97,9 @@ ...@@ -67,8 +97,9 @@
this.errorMessage = err; this.errorMessage = err;
}); });
}, },
fetchNodeDetails(nodeId) { fetchNodeDetails(node) {
return this.service.getGeoNodeDetails(nodeId) const nodeId = node.id;
return this.service.getGeoNodeDetails(node)
.then(res => res.data) .then(res => res.data)
.then((nodeDetails) => { .then((nodeDetails) => {
const primaryNodeVersion = this.store.getPrimaryNodeVersion(); const primaryNodeVersion = this.store.getPrimaryNodeVersion();
...@@ -80,18 +111,81 @@ ...@@ -80,18 +111,81 @@
eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId)); eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId));
}) })
.catch((err) => { .catch((err) => {
eventHub.$emit('nodeDetailsLoadFailed', nodeId, 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) { repairNode(targetNode) {
this.nodePollingInterval = new SmartInterval({ this.setNodeActionStatus(targetNode, true);
callback: this.fetchNodeDetails.bind(this, nodeId), this.service.repairNode(targetNode)
startingInterval: 30000, .then(() => {
maxInterval: 120000, this.setNodeActionStatus(targetNode, false);
hiddenInterval: 240000, Flash(s__('GeoNodes|Node Authentication was successfully repaired.'), 'notice');
incrementByFactorOf: 15000, })
immediateExecution: true, .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;
}, },
}, },
}; };
...@@ -120,5 +214,14 @@ ...@@ -120,5 +214,14 @@
> >
{{ errorMessage }} {{ errorMessage }}
</p> </p>
<modal
v-show="showModal"
:title="__('Are you sure?')"
:kind="modalKind"
:text="modalMessage"
:primary-button-label="modalActionLabel"
@cancel="hideNodeActionModal"
@submit="handleNodeAction"
/>
</div> </div>
</template> </template>
...@@ -7,7 +7,8 @@ import eventHub from 'ee/geo_nodes/event_hub'; ...@@ -7,7 +7,8 @@ import eventHub from 'ee/geo_nodes/event_hub';
import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store'; import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store';
import GeoNodesService from 'ee/geo_nodes/service/geo_nodes_service'; import GeoNodesService from 'ee/geo_nodes/service/geo_nodes_service';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; 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 createComponent = () => {
const Component = Vue.extend(appComponent); const Component = Vue.extend(appComponent);
...@@ -34,19 +35,27 @@ describe('AppComponent', () => { ...@@ -34,19 +35,27 @@ describe('AppComponent', () => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
document.body.innerHTML += '<div class="flash-container"></div>';
mock.onGet(/(.*)\/geo_nodes$/).reply(() => [statusCode, response]); mock.onGet(/(.*)\/geo_nodes$/).reply(() => [statusCode, response]);
vm = createComponent(); vm = createComponent();
}); });
afterEach(() => { afterEach(() => {
document.querySelector('.flash-container').remove();
vm.$destroy(); vm.$destroy();
mock.restore(); mock.restore();
}); });
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
expect(vm.isLoading).toBeTruthy(); expect(vm.isLoading).toBe(true);
expect(vm.hasError).toBeFalsy(); 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(''); expect(vm.errorMessage).toBe('');
}); });
}); });
...@@ -60,6 +69,23 @@ describe('AppComponent', () => { ...@@ -60,6 +69,23 @@ describe('AppComponent', () => {
}); });
describe('methods', () => { 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', () => { describe('fetchGeoNodes', () => {
it('calls service.getGeoNodes and sets response to the store on success', (done) => { it('calls service.getGeoNodes and sets response to the store on success', (done) => {
spyOn(vm.store, 'setNodes'); spyOn(vm.store, 'setNodes');
...@@ -89,35 +115,244 @@ describe('AppComponent', () => { ...@@ -89,35 +115,244 @@ describe('AppComponent', () => {
describe('fetchNodeDetails', () => { describe('fetchNodeDetails', () => {
it('calls service.getGeoNodeDetails and sets response to the store on success', (done) => { 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(); spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(2); vm.fetchNodeDetails(mockNode);
setTimeout(() => { setTimeout(() => {
expect(vm.service.getGeoNodeDetails).toHaveBeenCalled(); expect(vm.service.getGeoNodeDetails).toHaveBeenCalled();
expect(Object.keys(vm.store.state.nodeDetails).length).toBe(1); expect(Object.keys(vm.store.state.nodeDetails).length).not.toBe(0);
expect(vm.store.state.nodeDetails['2']).toBeDefined(); expect(vm.store.state.nodeDetails['1']).toBeDefined();
done(); done();
}, 0); }, 0);
}); });
it('emits `nodeDetailsLoadFailed` event on failure', (done) => { it('emits `nodeDetailsLoadFailed` event on failure', (done) => {
const err = 'Something went wrong';
spyOn(eventHub, '$emit'); 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(mockNode);
setTimeout(() => {
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(2); vm.fetchNodeDetails(mockNode);
setTimeout(() => { setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoadFailed', 2, jasmine.any(Object)); 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(); done();
}, 0); }, 0);
}); });
}); });
describe('initNodeDetailsPolling', () => { describe('repairNode', () => {
it('initializes SmartInterval and sets it to component', () => { it('calls service.repairNode and shows success Flash message on request success', (done) => {
vm.initNodeDetailsPolling(2); const node = { ...mockNode };
expect(vm.nodePollingInterval).toBeDefined(); 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', () => { ...@@ -127,6 +362,8 @@ describe('AppComponent', () => {
spyOn(eventHub, '$on'); spyOn(eventHub, '$on');
const vmX = createComponent(); const vmX = createComponent();
expect(eventHub.$on).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function)); 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(); vmX.$destroy();
}); });
}); });
...@@ -137,6 +374,8 @@ describe('AppComponent', () => { ...@@ -137,6 +374,8 @@ describe('AppComponent', () => {
const vmX = createComponent(); const vmX = createComponent();
vmX.$destroy(); vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function)); 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