Forward broker requests to the given broker (#142)

*Motivation*

In #141 we added the support to handle forwarded request in the backend. This change modifies the frontend to attach `x-pulsar-cluster` for cluster related admin requests and `x-pulsar-broker` for broker related admin requests.

This change is based on #141 
diff --git a/front-end/src/api/brokerStats.js b/front-end/src/api/brokerStats.js
index 1913b1e..40f3385 100644
--- a/front-end/src/api/brokerStats.js
+++ b/front-end/src/api/brokerStats.js
@@ -17,13 +17,20 @@
 
 export function fetchBrokerStatsTopics(broker) {
   return request({
-    url: SPRING_BASE_URL_V2 + `/broker-stats/topics?broker=` + broker,
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-broker': broker
+    },
+    url: `/admin/v2/broker-stats/topics`,
     method: 'get'
   })
 }
 
 export function fetchBrokerStatsMetrics(broker) {
   return request({
+    headers: {
+      'x-pulsar-broker': broker
+    },
     url: SPRING_BASE_URL_V2 + `/broker-stats/metrics?broker=` + broker,
     method: 'get'
   })
diff --git a/front-end/src/api/brokers.js b/front-end/src/api/brokers.js
index ddc26f1..cdac134 100644
--- a/front-end/src/api/brokers.js
+++ b/front-end/src/api/brokers.js
@@ -19,6 +19,9 @@
 
 export function fetchBrokers(cluster) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: SPRING_BASE_URL_V2 + `/brokers/${cluster}`,
     method: 'get'
   })
@@ -26,41 +29,59 @@
 
 export function fetchBrokersByDirectBroker(cluster) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/brokers/${cluster}`,
     method: 'get'
   })
 }
 
-export function fetchBrokersConfiguration() {
+export function fetchBrokersConfiguration(broker) {
   return request({
+    headers: {
+      'x-pulsar-broker': broker
+    },
     url: BASE_URL_V2 + `/brokers/configuration`,
     method: 'get'
   })
 }
 
-export function fetchBrokersRuntimeConfiguration() {
+export function fetchBrokersRuntimeConfiguration(broker) {
   return request({
+    headers: {
+      'x-pulsar-broker': broker
+    },
     url: BASE_URL_V2 + `/brokers/configuration/runtime`,
     method: 'get'
   })
 }
 
-export function fetchBrokersInternalConfiguration() {
+export function fetchBrokersInternalConfiguration(cluster) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/brokers/internal-configuration`,
     method: 'get'
   })
 }
 
-export function fetchBrokersDynamicConfiguration() {
+export function fetchBrokersDynamicConfiguration(cluster) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/brokers/configuration/values`,
     method: 'get'
   })
 }
 
-export function updateBrokersDynamicConfiguration(configName, configValue) {
+export function updateBrokersDynamicConfiguration(cluster, configName, configValue) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/brokers/configuration/${configName}/${configValue}`,
     method: 'post'
   })
@@ -68,13 +89,19 @@
 
 export function fetchBrokersOwnedNamespaces(cluster, broker) {
   return request({
+    headers: {
+      'x-pulsar-broker': broker
+    },
     url: BASE_URL_V2 + `/brokers/${cluster}/${broker}/ownedNamespaces`,
     method: 'get'
   })
 }
 
-export function fetchBrokersHealth() {
+export function fetchBrokersHealth(broker) {
   return request({
+    headers: {
+      'x-pulsar-broker': broker
+    },
     url: BASE_URL_V2 + `/brokers/health`,
     method: 'get'
   })
diff --git a/front-end/src/api/clusters.js b/front-end/src/api/clusters.js
index ac6f59e..b25b78b 100644
--- a/front-end/src/api/clusters.js
+++ b/front-end/src/api/clusters.js
@@ -27,6 +27,7 @@
 
 export function fetchClusterConfig(cluster) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}`,
     method: 'get'
   })
@@ -34,6 +35,7 @@
 
 export function putCluster(cluster, data) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}`,
     method: 'put',
     data
@@ -42,6 +44,7 @@
 
 export function updateCluster(cluster, data) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}`,
     method: 'post',
     data
@@ -50,6 +53,7 @@
 
 export function deleteCluster(cluster) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}`,
     method: 'delete'
   })
@@ -57,7 +61,10 @@
 
 export function updateClusterPeer(cluster, data) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/peers`,
     method: 'post',
     data
@@ -66,6 +73,7 @@
 
 export function getClusterPeer(cluster) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}/peers`,
     method: 'get'
   })
@@ -73,6 +81,7 @@
 
 export function listClusterDomainName(cluster) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}/failureDomains`,
     method: 'get'
   })
@@ -80,6 +89,7 @@
 
 export function getClusterDomainName(cluster, domainName) {
   return request({
+    headers: { 'x-pulsar-cluster': cluster },
     url: BASE_URL_V2 + `/clusters/${cluster}/failureDomains/${domainName}`,
     method: 'get'
   })
@@ -87,7 +97,10 @@
 
 export function createClusterDomainName(cluster, domainName, data) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/failureDomains/${domainName}`,
     method: 'post',
     data
@@ -96,7 +109,10 @@
 
 export function updateClusterDomainName(cluster, domainName, data) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/failureDomains/${domainName}`,
     method: 'post',
     data
@@ -105,7 +121,10 @@
 
 export function deleteClusterDomainName(cluster, domainName) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/failureDomains/${domainName}`,
     method: 'post'
   })
diff --git a/front-end/src/api/isolationPolicies.js b/front-end/src/api/isolationPolicies.js
index ca40afd..984601a 100644
--- a/front-end/src/api/isolationPolicies.js
+++ b/front-end/src/api/isolationPolicies.js
@@ -17,6 +17,9 @@
 
 export function fetchIsolationPolicies(cluster) {
   return request({
+    headers: {
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/namespaceIsolationPolicies`,
     method: 'get'
   })
@@ -24,7 +27,10 @@
 
 export function updateIsolationPolicies(cluster, policyName, data) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/namespaceIsolationPolicies/${policyName}`,
     method: 'post',
     data
@@ -33,7 +39,10 @@
 
 export function deleteIsolationPolicies(cluster, policyName) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
     url: BASE_URL_V2 + `/clusters/${cluster}/namespaceIsolationPolicies/${policyName}`,
     method: 'delete'
   })
diff --git a/front-end/src/api/namespaces.js b/front-end/src/api/namespaces.js
index 1d28d1c..a4bbffa 100644
--- a/front-end/src/api/namespaces.js
+++ b/front-end/src/api/namespaces.js
@@ -182,8 +182,20 @@
 }
 
 export function unloadBundle(tenantNamespace, bundle) {
+  return unloadBundleImpl('', '', tenantNamespace, bundle)
+}
+
+export function unloadBundleOnBroker(broker, tenantNamespace, bundle) {
+  return unloadBundleImpl('', broker, tenantNamespace, bundle)
+}
+
+export function unloadBundleImpl(cluster, broker, tenantNamespace, bundle) {
   return request({
-    headers: { 'Content-Type': 'application/json' },
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster,
+      'x-pulsar-broker': broker
+    },
     url: BASE_URL_V2 + `/namespaces/${tenantNamespace}/${bundle}/unload`,
     method: 'put'
   })
diff --git a/front-end/src/utils/http.js b/front-end/src/utils/http.js
new file mode 100644
index 0000000..f676394
--- /dev/null
+++ b/front-end/src/utils/http.js
@@ -0,0 +1,17 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export function isValidResponse(response) {
+  return response.status === 200 || response.status === 204
+}
diff --git a/front-end/src/utils/request.js b/front-end/src/utils/request.js
index efa76a2..df7d81f 100644
--- a/front-end/src/utils/request.js
+++ b/front-end/src/utils/request.js
@@ -44,13 +44,6 @@
 
 // response interceptor
 service.interceptors.response.use(
-  // response => response,
-  /**
-   * 下面的注释为通过在response里,自定义code来标示请求状态
-   * 当code返回如下情况则说明权限有问题,登出并返回到登录页
-   * 如想通过 xmlhttprequest 来状态码标识 逻辑可写在下面error中
-   * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除
-   */
   response => {
     // const res = response.data
     if (response.status < 500 && response.status >= 200) {
@@ -60,7 +53,6 @@
     }
   },
   error => {
-    console.log('err' + error) // for debug
     let message = ''
     if (error.response.status === 404) {
       if (error.response.data.length <= 0) {
@@ -76,11 +68,16 @@
       }
     } else if (error.response.status === 400) {
       if (error.response.data.hasOwnProperty('message') && error.response.data.message.indexOf('no active environment') > 0) {
-        router.replace({ path: '/environments' })
+        router.replace({
+          path: '/environments'
+        })
         return
       }
     } else {
-      message = error.response.data.reason
+      message = error.response.data
+      if (message.indexOf('Trying to subscribe with incompatible') >= 0) {
+        message = 'Incompatible schema detected while heartbeating'
+      }
     }
     Message({
       message: message,
diff --git a/front-end/src/views/management/brokers/broker.vue b/front-end/src/views/management/brokers/broker.vue
index 053da9f..6f59657 100644
--- a/front-end/src/views/management/brokers/broker.vue
+++ b/front-end/src/views/management/brokers/broker.vue
@@ -94,11 +94,13 @@
 import { fetchBrokerStatsMetrics, fetchBrokerStatsTopics } from '@/api/brokerStats'
 import { fetchBrokersHealth, fetchBrokers } from '@/api/brokers'
 import { fetchIsolationPolicies } from '@/api/isolationPolicies'
-import { unloadBundle } from '@/api/namespaces'
+import { unloadBundleOnBroker } from '@/api/namespaces'
 import { fetchBrokersRuntimeConfiguration } from '@/api/brokers'
 import jsonEditor from '@/components/JsonEditor'
 import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
 import MdInput from '@/components/MDinput'
+import { isValidResponse } from '@/utils/http'
+
 const defaultForm = {
   cluster: '',
   broker: ''
@@ -140,6 +142,16 @@
       fetchBrokerStatsTopics(this.postForm.broker).then(response => {
         if (!response.data) return
         this.brokerStatsTopic = response.data
+        if ((typeof this.brokerStatsTopic) === 'string') {
+          // failed to fetch broker stats
+          this.brokerStatsTopic = {}
+          this.$notify({
+            title: 'error',
+            message: 'Failed to fetch broker stats from broker ' + this.postForm.broker,
+            type: 'error',
+            duration: 3000
+          })
+        }
         for (var tenantNamespace in this.brokerStatsTopic) {
           var tn = tenantNamespace.split('/')
           for (var bundle in this.brokerStatsTopic[tenantNamespace]) {
@@ -233,18 +245,27 @@
       })
     },
     handleUnloadBundle(row) {
-      unloadBundle(row.tenant + '/' + row.namespace, row.bundle).then(response => {
-        this.$notify({
-          title: 'success',
-          message: 'Unload bundle success',
-          type: 'success',
-          duration: 3000
-        })
+      unloadBundleOnBroker(this.postForm.broker, row.tenant + '/' + row.namespace, row.bundle).then(response => {
+        if (isValidResponse(response)) {
+          this.$notify({
+            title: 'success',
+            message: 'Successfully unload namespace bundle from the broker',
+            type: 'success',
+            duration: 3000
+          })
+        } else {
+          this.$notify({
+            title: 'error',
+            message: 'Failed to unload namespace bundle from the broker : ' + response.data,
+            type: 'error',
+            duration: 3000
+          })
+        }
       })
     },
     handleHeartBeat() {
-      fetchBrokersHealth().then(response => {
-        if (response.data === 'ok') {
+      fetchBrokersHealth(this.postForm.broker).then(response => {
+        if (isValidResponse(response)) {
           this.$notify({
             title: 'success',
             message: 'Health Check success',
@@ -254,7 +275,7 @@
         } else {
           this.$notify({
             title: 'error',
-            message: 'Health Check failed',
+            message: 'Health Check failed: \n' + response.data,
             type: 'error',
             duration: 3000
           })
@@ -262,7 +283,7 @@
       })
     },
     handleRuntimeConfig() {
-      fetchBrokersRuntimeConfiguration().then(response => {
+      fetchBrokersRuntimeConfiguration(this.postForm.broker).then(response => {
         this.dialogFormVisible = true
         this.jsonValue = response.data
       })