Repository: jhipster/jhipster-control-center
Pull request #57 (2020-05-11 13:00:36)

Kill instances
My attempt at #55 using endpoint actuators.
![Screenshot_20200511_145252](https://user-images.githubusercontent.com/43046742/81564616-5181e200-9398-11ea-8d58-bd3cbc2a8c4b.png)



Patch:
diff --git a/src/main/webapp/app/applications/instance/instance.component.ts b/src/main/webapp/app/applications/instance/instance.component.ts
index f6593e9..46f0326 100644
--- a/src/main/webapp/app/applications/instance/instance.component.ts
+++ b/src/main/webapp/app/applications/instance/instance.component.ts
@@ -65,6 +65,51 @@ export default class JhiInstance extends Vue {
     this.instanceModal.show();
   }
 
+  /* Modal dialog */
+  public confirmShutdown(instance: Instance): void {
+    const config = {
+      title: 'Please Confirm',
+      size: 'sm',
+      buttonSize: 'sm',
+      okVariant: 'danger',
+      okTitle: 'YES',
+      cancelTitle: 'NO',
+      footerClass: 'p-2',
+      hideHeaderClose: false,
+      centered: true
+    };
+    this.$bvModal
+      .msgBoxConfirm('Are you sure you want to shutdown the instance ?', config)
+      .then(res => {
+        if (res) {
+          this.shutdownInstance(instance);
+        }
+      })
+      .catch(err => console.warn(err));
+  }
+
+  /* shutdown an instance */
+  public shutdownInstance(instance: Instance): void {
+    this.instanceService()
+      .shutdownInstance(instance)
+      .then(() => {
+        return this.$bvToast.toast('Instance shutdown successful', {
+          title: 'Success',
+          variant: 'success',
+          solid: true,
+          autoHideDelay: 5000
+        });
+      })
+      .catch(err => {
+        return this.$bvToast.toast(`${err}`, {
+          title: `Error`,
+          variant: 'danger',
+          solid: true,
+          autoHideDelay: 5000
+        });
+      });
+  }
+
   /* istanbul ignore next */
   beforeDestroy(): any {
     // prevent memory leak when component destroyed
diff --git a/src/main/webapp/app/applications/instance/instance.service.ts b/src/main/webapp/app/applications/instance/instance.service.ts
index cd2f91e..2a554b6 100644
--- a/src/main/webapp/app/applications/instance/instance.service.ts
+++ b/src/main/webapp/app/applications/instance/instance.service.ts
@@ -43,7 +43,12 @@ export default class InstanceService extends Vue {
 
   /** get active profiles of an instance */
   public findActiveProfiles(gatewayRoute: string): AxiosPromise<any> {
-    return axios.get('gateway/' + String(gatewayRoute) + '/management/info');
+    return axios.get(`gateway/${String(gatewayRoute)}/management/info`);
+  }
+
+  /* shutdown an instance */
+  public shutdownInstance({ serviceId, instanceId }): AxiosPromise<any> {
+    return axios.post(`/gateway/${serviceId}/${instanceId}/management/shutdown`);
   }
 
   /** convert json array of instances to array of instances */
diff --git a/src/main/webapp/app/applications/instance/instance.vue b/src/main/webapp/app/applications/instance/instance.vue
index 653bbda..9408945 100644
--- a/src/main/webapp/app/applications/instance/instance.vue
+++ b/src/main/webapp/app/applications/instance/instance.vue
@@ -13,6 +13,7 @@
                     <th class="w-30">Profile</th>
                     <th class="w-30">Git</th>
                     <th class="text-center">Detail</th>
+                    <th class="text-center">Kill</th>
                 </tr>
                 </thead>
                 <tbody>
@@ -44,6 +45,13 @@
                             </a>
                         </div>
                     </td>
+                    <td class="table-hover">
+                        <div class="text-center">
+                            <a id="shutdownInstance" v-on:click="confirmShutdown(instance)" v-if="instance.serviceId !== 'consul'">
+                                <font-awesome-icon icon="power-off"></font-awesome-icon>
+                            </a>
+                        </div>
+                    </td>
                 </tr>
                 </tbody>
             </table>
diff --git a/src/main/webapp/app/main.ts b/src/main/webapp/app/main.ts
index 66302aa..9a304b4 100644
--- a/src/main/webapp/app/main.ts
+++ b/src/main/webapp/app/main.ts
@@ -23,6 +23,7 @@ import AlertService from '@/shared/alert/alert.service';
 import ConfigurationService from '@/admin/configuration/configuration.service';
 import { RefreshService } from '@/shared/refresh/refresh.service';
 import InstanceService from '@/applications/instance/instance.service';
+import { ToastPlugin, ModalPlugin } from 'bootstrap-vue';
 
 /* tslint:disable */
 
@@ -34,6 +35,8 @@ config.initVueApp(Vue);
 config.initFortAwesome(Vue);
 bootstrapVueConfig.initBootstrapVue(Vue);
 Vue.use(Vue2Filters);
+Vue.use(ToastPlugin);
+Vue.use(ModalPlugin);
 Vue.component('font-awesome-icon', FontAwesomeIcon);
 Vue.component('jhi-item-count', JhiItemCountComponent);
 Vue.component('jhi-sort-indicator', JhiSortIndicatorComponent);
diff --git a/src/main/webapp/app/shared/config/config-bootstrap-vue.ts b/src/main/webapp/app/shared/config/config-bootstrap-vue.ts
index 10e1b8c..6b1ea1d 100644
--- a/src/main/webapp/app/shared/config/config-bootstrap-vue.ts
+++ b/src/main/webapp/app/shared/config/config-bootstrap-vue.ts
@@ -55,6 +55,7 @@ export function initBootstrapVue(vue) {
   vue.component('b-nav-item', BNavItem);
   vue.component('b-nav-item-dropdown', BNavItemDropdown);
   vue.component('b-modal', BModal);
+  vue.component('b-button', BButton);
   vue.directive('b-modal', VBModal);
   vue.directive('bToggle', VBToggle);
   vue.component('b-sidebar', BSidebar);
diff --git a/src/main/webapp/app/shared/config/config.ts b/src/main/webapp/app/shared/config/config.ts
index 563639e..71f783c 100644
--- a/src/main/webapp/app/shared/config/config.ts
+++ b/src/main/webapp/app/shared/config/config.ts
@@ -42,6 +42,7 @@ import { faChartLine } from '@fortawesome/free-solid-svg-icons/faChartLine';
 import { faAngleDoubleLeft } from '@fortawesome/free-solid-svg-icons/faAngleDoubleLeft';
 import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
 import { faEllipsisV } from '@fortawesome/free-solid-svg-icons/faEllipsisV';
+import { faPowerOff } from '@fortawesome/free-solid-svg-icons/faPowerOff';
 
 import VueCookie from 'vue-cookie';
 import Vuelidate from 'vuelidate';
@@ -101,7 +102,8 @@ export function initFortAwesome(vue) {
     faSitemap,
     faCube,
     faCogs,
-    faEllipsisV
+    faEllipsisV,
+    faPowerOff
   );
 }
 


Test patch:
diff --git a/src/test/javascript/spec/app/applications/instance/instance.component.spec.ts b/src/test/javascript/spec/app/applications/instance/instance.component.spec.ts
index f51ac73..a5c544c 100644
--- a/src/test/javascript/spec/app/applications/instance/instance.component.spec.ts
+++ b/src/test/javascript/spec/app/applications/instance/instance.component.spec.ts
@@ -21,7 +21,8 @@ localVue.use(BootstrapVue);
 const store = config.initVueXStore(localVue);
 
 jest.mock('axios', () => ({
-  get: jest.fn()
+  get: jest.fn(),
+  post: jest.fn()
 }));
 
 const stubbedModal = {
@@ -143,4 +144,68 @@ describe('Instance Component', () => {
       spy.mockRestore();
     });
   });
+
+  describe('should kill the selected instance', () => {
+    it('should show confirm dialog', async () => {
+      // GIVEN
+      const spy = jest.spyOn(instance, 'confirmShutdown');
+      const inst: Instance = new class implements Instance {
+        serviceId: string;
+        instanceId: string;
+        uri: string;
+        host: string;
+        port: number;
+        secure: boolean;
+        metadata: Metadata;
+      }();
+      inst.serviceId = 'app1';
+      inst.instanceId = 'app1-id';
+      inst.uri = 'http://127.0.0.01:8080';
+      inst.host = '127.0.0.1';
+      inst.port = 8080;
+      inst.secure = false;
+      inst.metadata = {};
+      const uri = 'http://127.0.0.01:8080';
+      instance.instancesRoute = [
+        { uri: 'http://127.0.0.01:8081', route_id: 'test2/test2-id' },
+        { uri: 'http://127.0.0.01:8080', route_id: 'test/test-id' }
+      ];
+      // WHEN
+      instance.confirmShutdown(inst);
+      await instance.$nextTick();
+      // THEN
+      expect(spy).toBeCalledWith(inst);
+      spy.mockRestore();
+    });
+
+    it('should fire a post request ', async () => {
+      mockedAxios.post.mockReturnValue(Promise.resolve({}));
+      const inst: Instance = new class implements Instance {
+        serviceId: string;
+        instanceId: string;
+        uri: string;
+        host: string;
+        port: number;
+        secure: boolean;
+        metadata: Metadata;
+      }();
+      inst.serviceId = 'app1';
+      inst.instanceId = 'app1-id';
+      inst.uri = 'http://127.0.0.01:8080';
+      inst.host = '127.0.0.1';
+      inst.port = 8080;
+      inst.secure = false;
+      inst.metadata = {};
+      const uri = 'http://127.0.0.01:8080';
+      instance.instancesRoute = [
+        { uri: 'http://127.0.0.01:8081', route_id: 'test2/test2-id' },
+        { uri: 'http://127.0.0.01:8080', route_id: 'test/test-id' }
+      ];
+
+      instance.shutdownInstance(inst);
+      await instance.$nextTick();
+
+      expect(mockedAxios.post).toHaveBeenCalledWith('/gateway/app1/app1-id/management/shutdown');
+    });
+  });
 });
diff --git a/src/test/javascript/spec/app/applications/instance/instance.service.spec.ts b/src/test/javascript/spec/app/applications/instance/instance.service.spec.ts
index edb7d9a..5630aeb 100644
--- a/src/test/javascript/spec/app/applications/instance/instance.service.spec.ts
+++ b/src/test/javascript/spec/app/applications/instance/instance.service.spec.ts
@@ -4,7 +4,8 @@ import axios from 'axios';
 
 const mockedAxios: any = axios;
 jest.mock('axios', () => ({
-  get: jest.fn()
+  get: jest.fn(),
+  post: jest.fn()
 }));
 
 const data: Array<any> = [
@@ -92,4 +93,15 @@ describe('Instance Service', () => {
     await expect(instanceService.findActiveProfiles(route)).rejects.toEqual(error);
     expect(mockedAxios.get).toHaveBeenCalledWith('gateway/test/test-id/management/info');
   });
+
+  it('should make a post request to the shutdown actuator endpoint', async () => {
+    const instance = data[0];
+    const returnValue = {
+      message: 'Shutting down, bye...'
+    };
+    mockedAxios.post.mockReturnValue(Promise.resolve(returnValue));
+
+    await expect(instanceService.shutdownInstance(instance)).resolves.toEqual(returnValue);
+    expect(mockedAxios.post).toHaveBeenCalledWith('/gateway/app1/app1-id/management/shutdown');
+  });
 });