Repository: jhipster/jhipster-control-center
Pull request #6 (2020-04-02 16:37:45)

add applications component to frontend
display list of microservices and their basic informations


Patch:
diff --git a/src/main/java/tech/jhipster/controlcenter/web/rest/ServiceDiscoveryResource.java b/src/main/java/tech/jhipster/controlcenter/web/rest/ServiceDiscoveryResource.java
index c98afc7..745ecb8 100644
--- a/src/main/java/tech/jhipster/controlcenter/web/rest/ServiceDiscoveryResource.java
+++ b/src/main/java/tech/jhipster/controlcenter/web/rest/ServiceDiscoveryResource.java
@@ -11,8 +11,7 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -36,10 +35,11 @@ public class ServiceDiscoveryResource {
      * registered to the service discovery provider.
      */
     @GetMapping("/services/instances")
-    public ResponseEntity<Map<String, List<ServiceInstance>>> getAllServiceInstances() {
+    public ResponseEntity<List<ServiceInstance>> getAllServiceInstances() {
         Map<String, List<ServiceInstance>> instances = discoveryClient.getServices().stream()
             .collect(Collectors.toMap(Function.identity(), discoveryClient::getInstances));
-        return new ResponseEntity<>(instances, HttpStatus.OK);
+        List<ServiceInstance> res = instances.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
+        return new ResponseEntity<>(res, HttpStatus.OK);
     }
 
     /**
diff --git a/src/main/webapp/app/applications/applications-modal.component.ts b/src/main/webapp/app/applications/applications-modal.component.ts
new file mode 100644
index 0000000..82754dc
--- /dev/null
+++ b/src/main/webapp/app/applications/applications-modal.component.ts
@@ -0,0 +1,31 @@
+import ApplicationsService, { Application } from './applications.service';
+import { Component, Inject, Prop, Vue } from 'vue-property-decorator';
+
+@Component
+export default class JhiApplicationsModal extends Vue {
+  public activeProfiles: any = null;
+  @Prop() currentApplication!: Application;
+  @Prop() currentRoute!: any;
+  @Inject('applicationsService') private applicationsService: () => ApplicationsService;
+
+  public mounted(): void {
+    this.refreshProfile();
+  }
+
+  /** Update profile of current application */
+  public refreshProfile(): void {
+    /* istanbul ignore else */
+    if (this.currentApplication.serviceId !== 'consul') {
+      this.applicationsService()
+        .findActiveProfiles(this.currentRoute)
+        .then(res => {
+          this.activeProfiles = res.data;
+        })
+        .catch(error => {
+          this.activeProfiles = error.error;
+        });
+    } else {
+      this.activeProfiles = {};
+    }
+  }
+}
diff --git a/src/main/webapp/app/applications/applications-modal.vue b/src/main/webapp/app/applications/applications-modal.vue
new file mode 100644
index 0000000..df5e62b
--- /dev/null
+++ b/src/main/webapp/app/applications/applications-modal.vue
@@ -0,0 +1,81 @@
+<template>
+    <div class="modal-body pad">
+        <div v-if="currentApplication && currentRoute">
+            <div class="table">
+                <table class="table table-bordered">
+                    <tbody>
+                        <tr>
+                            <td class="text-left align-middle">
+                                <div>URI</div>
+                            </td>
+                            <td class="text-left">
+                                <a :href="currentApplication.instance.uri" target="_blank">
+                                    {{currentApplication.instance.uri}}
+                                </a>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="text-left">
+                                Port
+                            </td>
+                            <td class="text-left">
+                                {{currentApplication.instance.port}}
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="text-left">
+                                Secure
+                            </td>
+                            <td class="text-left">
+                                {{currentApplication.instance.secure}}
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="text-left">
+                                Route
+                            </td>
+                            <td class="text-left">
+                                <span class="badge badge-pill badge-info">
+                                    {{currentRoute}}
+                                </span>
+                            </td>
+                        </tr>
+                        <tr v-if="activeProfiles">
+                            <td class="text-left">
+                                Active Profiles
+                            </td>
+                            <td class="text-left align-middle">
+                                <span v-for="content in activeProfiles.activeProfiles" v-bind:key="content" class="badge badge-pill badge-success mr-2">
+                                    {{ content }}
+                                </span>
+                            </td>
+                        </tr>
+                        <tr v-if="currentApplication.instance.metadata && currentApplication.instance.metadata.profile">
+                            <td class="text-left">
+                                Profile
+                            </td>
+                            <td class="text-left">
+                                <span class="badge badge-pill badge-primary">{{currentApplication.instance.metadata.profile}}</span>
+                            </td>
+                        </tr>
+                        <tr v-if="currentApplication.instance.metadata && currentApplication.instance.metadata.version">
+                            <td class="text-left">
+                                Version
+                            </td>
+                            <td class="text-left">
+                                <span class="badge badge-pill badge-primary">{{currentApplication.instance.metadata.version}}</span>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+        <div v-if="currentRoute && currentRoute.error">
+            <h4>Error</h4>
+            <pre>{{currentRoute.error}}</pre>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" src="./applications-modal.component.ts">
+</script>
diff --git a/src/main/webapp/app/applications/applications.component.ts b/src/main/webapp/app/applications/applications.component.ts
new file mode 100644
index 0000000..3e127fd
--- /dev/null
+++ b/src/main/webapp/app/applications/applications.component.ts
@@ -0,0 +1,67 @@
+import ApplicationsService, { Application, Instance } from './applications.service';
+import { Component, Inject, Vue } from 'vue-property-decorator';
+import JhiApplicationsModal from './applications-modal.vue';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+
+@Component({
+  components: {
+    'applications-modal': JhiApplicationsModal
+  }
+})
+export default class JhiApplications extends Vue {
+  public applications?: Array<Application> = null;
+  public applicationsRoute: Array<any> = null;
+  public currentApplication: Application = null;
+  public currentRoute: any = null;
+  public applicationModal: any = null;
+  private unsubscribe$ = new Subject();
+  @Inject('applicationsService') private applicationsService: () => ApplicationsService;
+
+  public mounted(): void {
+    this.refreshApplicationsData();
+    this.refreshApplicationsRoute();
+  }
+
+  /** Update applicationsData which contains list of applications */
+  public refreshApplicationsData(): void {
+    this.applicationsService()
+      .findAll()
+      .pipe(takeUntil(this.unsubscribe$))
+      .subscribe(applications => {
+        this.applications = applications;
+      });
+  }
+
+  /** Update applicationsRoute which contains list of routes */
+  public refreshApplicationsRoute(): void {
+    this.applicationsService()
+      .findAllGatewayRoute()
+      .then(res => {
+        this.applicationsRoute = res.data;
+      })
+      .catch(error => {
+        this.applicationsRoute = error.error;
+      });
+  }
+
+  /** Display details about an application in a b-modal */
+  public showApplication(application: any, uri: string): void {
+    this.currentApplication = application;
+    for (let i = 0; i < this.applicationsRoute.length; i++) {
+      if (this.applicationsRoute[i].uri === uri) {
+        this.currentRoute = this.applicationsRoute[i].route_id;
+        break;
+      }
+    }
+    this.applicationModal = <any>this.$refs.applicationModal;
+    this.applicationModal.show();
+  }
+
+  /* istanbul ignore next */
+  beforeDestroy(): any {
+    // prevent memory leak when component destroyed
+    this.unsubscribe$.next();
+    this.unsubscribe$.complete();
+  }
+}
diff --git a/src/main/webapp/app/applications/applications.service.ts b/src/main/webapp/app/applications/applications.service.ts
new file mode 100644
index 0000000..08e6288
--- /dev/null
+++ b/src/main/webapp/app/applications/applications.service.ts
@@ -0,0 +1,59 @@
+import axios, { AxiosPromise } from 'axios';
+import Vue from 'vue';
+import Component from 'vue-class-component';
+import { Observable } from 'rxjs';
+
+export interface Application {
+  constructor: Function;
+  serviceId: string;
+  instance: Instance;
+}
+
+export type Instance = { [key in InstanceKey]?: any };
+
+export type InstanceKey = 'instanceId' | 'uri' | 'serviceId' | 'port' | 'secure' | 'metadata';
+
+@Component
+export default class ApplicationsService extends Vue {
+  /** get all applications instances */
+  public findAll(): Observable<Array<Application>> {
+    return Observable.create(observer => {
+      axios
+        .get('api/services/instances')
+        .then(res => {
+          // generate application list from http request
+          const applications = this.parseJsonArrayApplicationsToArray(res.data);
+          observer.next(applications);
+          observer.complete();
+        })
+        .catch(error => {
+          observer.error(error);
+        });
+    });
+  }
+
+  /** get all gateway routes of applications instances */
+  public findAllGatewayRoute(): AxiosPromise<any> {
+    return axios.get('management/gateway/routes');
+  }
+
+  /** get active profiles of an application instance */
+  public findActiveProfiles(gatewayRoute: string): AxiosPromise<any> {
+    return axios.get('gateway/' + String(gatewayRoute) + '/management/info');
+  }
+
+  /** convert json array of applications to array of applications */
+  parseJsonArrayApplicationsToArray(data: any): Array<Application> {
+    const applications: Array<Application> = [];
+    data.map(application => {
+      const app = new class implements Application {
+        serviceId: string;
+        instance: Instance;
+      }();
+      app.serviceId = application.serviceId;
+      app.instance = application;
+      applications.push(app);
+    });
+    return applications;
+  }
+}
diff --git a/src/main/webapp/app/applications/applications.vue b/src/main/webapp/app/applications/applications.vue
new file mode 100644
index 0000000..631e204
--- /dev/null
+++ b/src/main/webapp/app/applications/applications.vue
@@ -0,0 +1,43 @@
+<template>
+    <div>
+        <h2>
+            <span id="applications-page-heading">Applications Instances</span>
+        </h2>
+        <div class="table-responsive">
+            <table class="table table-striped table-bordered table-responsive d-table">
+                <thead>
+                <tr>
+                    <th class="w-50">Service</th>
+                    <th class="w-50">Instance</th>
+                    <th>Detail</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr v-for="content in applications" :key="content.instance.instanceId">
+                    <td class="table-hover">
+                        <a :href="content.instance.uri" target="_blank">{{ content.instance.serviceId }}</a>
+                    </td>
+                    <td class="table-hover">
+                        <span class="badge badge-pill badge-light">{{ content.instance.instanceId }}</span>
+                    </td>
+                    <td class="text-center">
+                        <a id="showDetail" class="hand" v-on:click="showApplication(content, content.instance.uri)" v-if="content">
+                            <font-awesome-icon icon="eye"></font-awesome-icon>
+                        </a>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+        <b-modal ref="applicationModal">
+            <h4 slot="modal-title" v-if="currentApplication && currentRoute" class="modal-title" id="showApplicationLabel">
+                <span class="text-capitalize">{{ currentApplication.serviceId }}</span>
+            </h4>
+            <applications-modal :current-application="currentApplication" :current-route="currentRoute"></applications-modal>
+        </b-modal>
+    </div>
+</template>
+
+<script lang="ts" src="./applications.component.ts">
+</script>
+
diff --git a/src/main/webapp/app/core/jhi-navbar/jhi-navbar.vue b/src/main/webapp/app/core/jhi-navbar/jhi-navbar.vue
index ae813e0..e1f4fe0 100644
--- a/src/main/webapp/app/core/jhi-navbar/jhi-navbar.vue
+++ b/src/main/webapp/app/core/jhi-navbar/jhi-navbar.vue
@@ -58,6 +58,21 @@
                         <span>API</span>
                     </b-dropdown-item>
                 </b-nav-item-dropdown>
+                <b-nav-item-dropdown
+                    id="applications-menu"
+                    v-if="hasAnyAuthority('ROLE_ADMIN')"
+                    :class="{'router-link-active': subIsActive('/applications')}"
+                    active-class="active"
+                    class="pointer">
+                    <span slot="button-content" class="navbar-dropdown-menu">
+                        <font-awesome-icon icon="cloud" />
+                        <span>Applications</span>
+                    </span>
+                    <b-dropdown-item to="/applications/jhi-applications">
+                        <font-awesome-icon icon="th-list" />
+                        <span>Services</span>
+                    </b-dropdown-item>
+                </b-nav-item-dropdown>
                 <b-nav-item-dropdown
                     right
                     href="javascript:void(0);"
diff --git a/src/main/webapp/app/main.ts b/src/main/webapp/app/main.ts
index 1c259ab..0f1e0f5 100644
--- a/src/main/webapp/app/main.ts
+++ b/src/main/webapp/app/main.ts
@@ -21,6 +21,7 @@ import AccountService from './account/account.service';
 import '../content/scss/vendor.scss';
 import AlertService from '@/shared/alert/alert.service';
 import ConfigurationService from '@/admin/configuration/configuration.service';
+import ApplicationsService from '@/applications/applications.service';
 
 /* tslint:disable */
 
@@ -77,7 +78,9 @@ new Vue({
     metricsService: () => new MetricsService(),
     alertService: () => alertService,
     // jhipster-needle-add-entity-service-to-main - JHipster will import entities services here
-    accountService: () => accountService
+    accountService: () => accountService,
+
+    applicationsService: () => new ApplicationsService()
   },
   store
 });
diff --git a/src/main/webapp/app/router/index.ts b/src/main/webapp/app/router/index.ts
index d3dca53..99b7b74 100644
--- a/src/main/webapp/app/router/index.ts
+++ b/src/main/webapp/app/router/index.ts
@@ -14,6 +14,8 @@ const JhiDocsComponent = () => import('../admin/docs/docs.vue');
 const JhiHealthComponent = () => import('../admin/health/health.vue');
 const JhiLogsComponent = () => import('../admin/logs/logs.vue');
 const JhiMetricsComponent = () => import('../admin/metrics/metrics.vue');
+const JhiApplicationsComponent = () => import('../applications/applications.vue');
+
 /* tslint:disable */
 // jhipster-needle-add-entity-to-router-import - JHipster will import entities to the router here
 
@@ -70,6 +72,12 @@ export default new Router({
       name: 'JhiConfigurationComponent',
       component: JhiConfigurationComponent,
       meta: { authorities: [Authority.ADMIN] }
+    },
+    {
+      path: '/applications/jhi-applications',
+      name: 'JhiApplicationsComponent',
+      component: JhiApplicationsComponent,
+      meta: { authorities: [Authority.ADMIN] }
     }
     // jhipster-needle-add-entity-to-router - JHipster will add entities to the router here
   ]
diff --git a/webpack/webpack.dev.js b/webpack/webpack.dev.js
index 4be9caf..c4858f3 100644
--- a/webpack/webpack.dev.js
+++ b/webpack/webpack.dev.js
@@ -33,7 +33,7 @@ module.exports = merge(baseWebpackConfig, {
     port: 9060,
     proxy: [
       {
-        context: ['/api', '/services', '/management', '/swagger-resources', '/v2/api-docs', '/h2-console', '/auth'],
+        context: ['/api', '/services', '/management', '/gateway', '/swagger-resources', '/v2/api-docs', '/h2-console', '/auth'],
         target: 'http://127.0.0.1:1337',
         secure: false,
         headers: { host: 'localhost:9000' }
diff --git a/webpack/webpack.dev.js.rej b/webpack/webpack.dev.js.rej
index dcbb222..72e1b79 100644
--- a/webpack/webpack.dev.js.rej
+++ b/webpack/webpack.dev.js.rej
@@ -4,9 +4,10 @@ diff a/webpack/webpack.dev.js b/webpack/webpack.dev.js	(rejected hunks)
 -          '/api',
 -          '/services',
 -          '/management',
+-          '/gateway',
 -          '/swagger-resources',
 -          '/v2/api-docs',
 -          '/h2-console',
 -          '/auth'
 -        ],
-+        context: ['/api', '/services', '/management', '/swagger-resources', '/v2/api-docs', '/h2-console', '/auth'],
++        context: ['/api', '/services', '/management', '/gateway', '/swagger-resources', '/v2/api-docs', '/h2-console', '/auth'],
diff --git a/webpack/webpack.prod.js b/webpack/webpack.prod.js
index c17db7b..40e342f 100644
--- a/webpack/webpack.prod.js
+++ b/webpack/webpack.prod.js
@@ -68,7 +68,7 @@ const webpackConfig = merge(baseWebpackConfig, {
     new HtmlWebpackPlugin({
       template: './src/main/webapp/index.html',
       chunks: ['vendors', 'main', 'global'],
-      chunksSortMode: 'manual',
+      // chunksSortMode: 'manual',
       inject: true,
       minify: {
         removeComments: true,


Test patch:
diff --git a/src/test/javascript/spec/app/applications/applications-modal.component.spec.ts b/src/test/javascript/spec/app/applications/applications-modal.component.spec.ts
new file mode 100644
index 0000000..b9671b2
--- /dev/null
+++ b/src/test/javascript/spec/app/applications/applications-modal.component.spec.ts
@@ -0,0 +1,73 @@
+import { createLocalVue, Wrapper, shallowMount } from '@vue/test-utils';
+import * as config from '@/shared/config/config';
+import ApplicationsModal from '@/applications/applications-modal.vue';
+import ApplicationsModalClass from '@/applications/applications-modal.component';
+import axios from 'axios';
+import { Application } from '@/applications/applications.service';
+
+const localVue = createLocalVue();
+config.initVueApp(localVue);
+localVue.component('font-awesome-icon', {});
+const mockedApplicationsService = { findActiveProfiles: jest.spyOn(axios, 'get') };
+const app = {
+  serviceId: 'app1',
+  instance: {
+    instanceId: 'app1-id',
+    serviceId: 'app1',
+    port: 8080,
+    secure: false,
+    metadata: { someData: 'test' },
+    uri: 'http://127.0.0.01:8080'
+  }
+};
+
+describe('Applications Modal Component', () => {
+  let wrapper: Wrapper<ApplicationsModalClass>;
+  let applicationsModal: ApplicationsModalClass;
+
+  beforeEach(async () => {
+    wrapper = shallowMount<ApplicationsModalClass>(ApplicationsModal, {
+      propsData: {
+        currentApplication: app,
+        currentRoute: {}
+      },
+      localVue,
+      provide: {
+        applicationsService: () => mockedApplicationsService
+      }
+    });
+    applicationsModal = wrapper.vm;
+  });
+
+  it('should be a Vue instance', () => {
+    expect(wrapper.isVueInstance()).toBeTruthy();
+  });
+
+  describe('active profiles', () => {
+    it('should refresh profile', async () => {
+      // GIVEN
+      mockedApplicationsService.findActiveProfiles.mockReturnValue(Promise.resolve({}));
+      const route = mockedApplicationsService.findActiveProfiles.mock.calls[0][0];
+
+      // WHEN
+      applicationsModal.refreshProfile();
+      await applicationsModal.$nextTick();
+
+      // THEN
+      expect(mockedApplicationsService.findActiveProfiles).toHaveBeenCalledWith(route);
+    });
+
+    it('should get an error when refresh profile', async () => {
+      // GIVEN
+      mockedApplicationsService.findActiveProfiles.mockReturnValue(Promise.reject({}));
+      const route = mockedApplicationsService.findActiveProfiles.mock.calls[0][0];
+
+      // WHEN
+      applicationsModal.refreshProfile();
+      await applicationsModal.$nextTick();
+
+      // THEN
+      expect(mockedApplicationsService.findActiveProfiles).toHaveBeenCalledWith(route);
+    });
+  });
+});
diff --git a/src/test/javascript/spec/app/applications/applications.component.spec.ts b/src/test/javascript/spec/app/applications/applications.component.spec.ts
new file mode 100644
index 0000000..a8ef1ad
--- /dev/null
+++ b/src/test/javascript/spec/app/applications/applications.component.spec.ts
@@ -0,0 +1,134 @@
+import { shallowMount, createLocalVue, Wrapper, mount } from '@vue/test-utils';
+import axios from 'axios';
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
+import * as config from '@/shared/config/config';
+import Applications from '@/applications/applications.vue';
+import ApplicationsModal from '@/applications/applications-modal.vue';
+import ApplicationsClass from '@/applications/applications.component';
+import ApplicationsService from '@/applications/applications.service';
+
+const localVue = createLocalVue();
+const mockedAxios: any = axios;
+
+config.initVueApp(localVue);
+localVue.component('font-awesome-icon', FontAwesomeIcon);
+localVue.component('applications-modal', ApplicationsModal);
+localVue.directive('b-modal', {});
+
+jest.mock('axios', () => ({
+  get: jest.fn()
+}));
+
+const stubbedModal = {
+  template: '<div></div>',
+  methods: {
+    show() {}
+  }
+};
+
+describe('Applications Component', () => {
+  let wrapper: Wrapper<ApplicationsClass>;
+  let applications: ApplicationsClass;
+
+  beforeEach(() => {
+    mockedAxios.get.mockReturnValue(Promise.resolve({}));
+    wrapper = mount<ApplicationsClass>(Applications, {
+      localVue,
+      stubs: {
+        bModal: stubbedModal
+      },
+      provide: {
+        applicationsService: () => new ApplicationsService()
+      }
+    });
+    applications = wrapper.vm;
+  });
+
+  it('should be a Vue instance', () => {
+    expect(wrapper.isVueInstance()).toBeTruthy();
+  });
+
+  describe('should refresh list of applications data', () => {
+    it('should call refreshApplicationsData on init', async () => {
+      // GIVEN
+      mockedAxios.get.mockReturnValue(Promise.resolve({}));
+
+      // WHEN
+      applications.refreshApplicationsData();
+      await applications.$nextTick();
+
+      // THEN
+      expect(mockedAxios.get).toHaveBeenCalledWith('api/services/instances');
+    });
+    it('should handle error on refreshing applications data', async () => {
+      // GIVEN
+      mockedAxios.get.mockReturnValue(Promise.reject({}));
+
+      // WHEN
+      applications.refreshApplicationsData();
+      await applications.$nextTick();
+
+      // THEN
+      expect(mockedAxios.get).toHaveBeenCalledWith('api/services/instances');
+    });
+  });
+
+  describe('should refresh list of applications route data', () => {
+    it('should call refreshApplicationsRoute on init', async () => {
+      // GIVEN
+      mockedAxios.get.mockReturnValue(Promise.resolve({}));
+
+      // WHEN
+      applications.refreshApplicationsRoute();
+      await applications.$nextTick();
+
+      // THEN
+      expect(mockedAxios.get).toHaveBeenCalledWith('management/gateway/routes');
+    });
+    it('should handle error on refreshing applications route data', async () => {
+      // GIVEN
+      mockedAxios.get.mockReturnValue(Promise.reject({}));
+
+      // WHEN
+      applications.refreshApplicationsRoute();
+      await applications.$nextTick();
+
+      // THEN
+      expect(mockedAxios.get).toHaveBeenCalledWith('management/gateway/routes');
+    });
+  });
+
+  // TODO fix test for method showApplication
+  describe('should call showApplication', () => {
+    it('should show selected application', async () => {
+      // GIVEN
+      const spy = jest.spyOn(applications, 'showApplication');
+      const app = {
+        app1: [
+          {
+            instanceId: 'app1-id',
+            serviceId: 'app1',
+            host: '127.0.0.1',
+            port: 8080,
+            secure: false,
+            metadata: { someData: 'test' },
+            uri: 'http://127.0.0.01:8080',
+            scheme: null
+          }
+        ]
+      };
+      const uri = 'http://127.0.0.01:8080';
+      applications.applicationsRoute = [
+        { 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
+      applications.showApplication(app, uri);
+      await applications.$nextTick();
+      // THEN
+      expect(spy).toHaveBeenCalled();
+      spy.mockRestore();
+    });
+  });
+});
diff --git a/src/test/javascript/spec/app/applications/applications.service.spec.ts b/src/test/javascript/spec/app/applications/applications.service.spec.ts
new file mode 100644
index 0000000..f8f6f03
--- /dev/null
+++ b/src/test/javascript/spec/app/applications/applications.service.spec.ts
@@ -0,0 +1,99 @@
+import ApplicationsService, { Application } from '@/applications/applications.service';
+import axios from 'axios';
+import { Observable } from 'rxjs';
+
+const mockedAxios: any = axios;
+jest.mock('axios', () => ({
+  get: jest.fn()
+}));
+
+const data: Array<any> = [
+  {
+    serviceId: 'app1',
+    instance: {
+      instanceId: 'app1-id',
+      serviceId: 'app1',
+      port: 8080,
+      secure: false,
+      metadata: { someData: 'test' },
+      uri: 'http://127.0.0.01:8080'
+    }
+  },
+  {
+    serviceId: 'app2',
+    instance: {
+      instanceId: 'app2-id',
+      serviceId: 'app2',
+      port: 8081,
+      secure: false,
+      metadata: { someData: 'test' },
+      uri: 'http://127.0.0.02:8081'
+    }
+  }
+];
+
+describe('Applications Service', () => {
+  let applicationsService: ApplicationsService;
+
+  beforeEach(() => {
+    applicationsService = new ApplicationsService();
+    mockedAxios.get.mockReset();
+  });
+
+  it('should get list of applications', async () => {
+    const spy = jest.spyOn(applicationsService, 'findAll').mockReturnValue(Observable.of(data));
+
+    expect(
+      applicationsService.findAll().subscribe(res => {
+        expect(res).toEqual(data);
+      })
+    );
+    expect(applicationsService.parseJsonArrayApplicationsToArray(data)).toHaveLength(2);
+    expect(spy).toHaveBeenCalled();
+  });
+
+  it('should get list of gateway route', async () => {
+    const routes = [
+      {
+        route_id: 'app1:id',
+        serviceId: 'app1',
+        otherData: 'test'
+      },
+      {
+        route_id: 'app2:id',
+        serviceId: 'app2',
+        otherData: 'test'
+      }
+    ];
+    mockedAxios.get.mockImplementationOnce(() => Promise.resolve(routes));
+
+    await expect(applicationsService.findAllGatewayRoute()).resolves.toEqual(routes);
+    expect(mockedAxios.get).toHaveBeenCalledWith('management/gateway/routes');
+  });
+
+  it('should get data error from this request', async () => {
+    const error = 'an error';
+    mockedAxios.get.mockImplementationOnce(() => Promise.reject(error));
+
+    await expect(applicationsService.findAllGatewayRoute()).rejects.toEqual(error);
+    expect(mockedAxios.get).toHaveBeenCalledWith('management/gateway/routes');
+  });
+
+  it('should get data profiles of an application', async () => {
+    const profiles = { 'display-ribbon-on-profiles': 'dev', activeProfiles: ['dev', 'test'] };
+    const route = 'test/test-id';
+    mockedAxios.get.mockImplementationOnce(() => Promise.resolve(profiles));
+
+    await expect(applicationsService.findActiveProfiles(route)).resolves.toEqual(profiles);
+    expect(mockedAxios.get).toHaveBeenCalledWith('gateway/test/test-id/management/info');
+  });
+
+  it('should get data error from this request', async () => {
+    const error = 'an error';
+    const route = 'test/test-id';
+    mockedAxios.get.mockImplementationOnce(() => Promise.reject(error));
+
+    await expect(applicationsService.findActiveProfiles(route)).rejects.toEqual(error);
+    expect(mockedAxios.get).toHaveBeenCalledWith('gateway/test/test-id/management/info');
+  });
+});