You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by si...@apache.org on 2019/12/25 15:54:05 UTC

[pulsar-manager] branch master updated: Support sub and unsub (#240)

This is an automated email from the ASF dual-hosted git repository.

sijie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-manager.git


The following commit(s) were added to refs/heads/master by this push:
     new 1a74888  Support sub and unsub (#240)
1a74888 is described below

commit 1a748880f8135c8efe26fa4a5cbc4772bd8abd60
Author: tuteng <gu...@apache.org>
AuthorDate: Wed Dec 25 23:53:56 2019 +0800

    Support sub and unsub (#240)
    
    Fix #167
    
    ### Motivation
    
    Add operation for sub and unsub
    
    ### Modifications
    
    * Add sub and unsub operations
    * Support topic and partition topic
    
    ### Verifying this change
    
    Local test pass
---
 front-end/src/api/subscriptions.js                 | 22 +++++
 front-end/src/lang/en.js                           |  9 +-
 front-end/src/lang/zh.js                           |  9 +-
 front-end/src/utils/request.js                     |  2 +
 .../views/management/topics/partitionedTopic.vue   | 98 +++++++++++++++++++---
 front-end/src/views/management/topics/topic.vue    | 71 +++++++++++++++-
 6 files changed, 193 insertions(+), 18 deletions(-)

diff --git a/front-end/src/api/subscriptions.js b/front-end/src/api/subscriptions.js
index e2f5b56..5a002f2 100644
--- a/front-end/src/api/subscriptions.js
+++ b/front-end/src/api/subscriptions.js
@@ -29,9 +29,31 @@ export function putSubscription(tenant, namespace, topic, subscription) {
   })
 }
 
+export function putSubscriptionOnCluster(cluster, persistent, tenantNamespaceTopic, subscription) {
+  return request({
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
+    url: BASE_URL_V2 + `/${persistent}/${tenantNamespaceTopic}/subscription/${subscription}`,
+    method: 'put'
+  })
+}
+
 export function deleteSubscription(tenant, namespace, topic, subscription) {
   return request({
     url: BASE_URL_V2 + `/persistent/${tenant}/${namespace}/${topic}/subscription/${subscription}`,
     method: 'delete'
   })
 }
+
+export function deleteSubscriptionOnCluster(cluster, persistent, tenantNamespaceTopic, subscription) {
+  return request({
+    headers: {
+      'Content-Type': 'application/json',
+      'x-pulsar-cluster': cluster
+    },
+    url: BASE_URL_V2 + `/${persistent}/${tenantNamespaceTopic}/subscription/${subscription}`,
+    method: 'delete'
+  })
+}
diff --git a/front-end/src/lang/en.js b/front-end/src/lang/en.js
index 69d1327..8264b31 100644
--- a/front-end/src/lang/en.js
+++ b/front-end/src/lang/en.js
@@ -557,7 +557,14 @@ export default {
       storage: 'STORAGE',
       storageSize: 'Storage Size',
       entries: 'Entries',
-      segments: 'Segments'
+      segments: 'Segments',
+      newSub: 'New Subscription',
+      sub: 'Subscribe',
+      unsub: 'Unsubscribe',
+      subNotification: 'Please input correct sub name',
+      createSubSuccess: 'Create subscription successfully',
+      deleteSubSuccess: 'Delete subscription successfully',
+      deleteSubConfirm: 'Do you want to delete this subscription?'
     },
     segment: {
       label: 'Segment',
diff --git a/front-end/src/lang/zh.js b/front-end/src/lang/zh.js
index 0c1e983..d09b9b3 100644
--- a/front-end/src/lang/zh.js
+++ b/front-end/src/lang/zh.js
@@ -557,7 +557,14 @@ export default {
       storage: 'STORAGE',
       storageSize: 'Storage Size',
       entries: 'Entries',
-      segments: 'Segments'
+      segments: 'Segments',
+      newSub: 'New Subscription',
+      sub: 'Subscribe',
+      unsub: 'Unsubscribe',
+      subNotification: 'Please input correct sub name',
+      createSubSuccess: 'Create subscription successfully',
+      deleteSubSuccess: 'Delete subscription successfully',
+      deleteSubConfirm: 'Do you want to delete this subscription?'
     },
     segment: {
       label: 'Segment',
diff --git a/front-end/src/utils/request.js b/front-end/src/utils/request.js
index 92cc701..07dcca3 100644
--- a/front-end/src/utils/request.js
+++ b/front-end/src/utils/request.js
@@ -75,6 +75,8 @@ service.interceptors.response.use(
         })
         return
       }
+    } else if (error.response.data.hasOwnProperty('reason')) {
+      message = error.response.data.reason
     } else {
       message = error.response.data
       if (message.indexOf('Trying to subscribe with incompatible') >= 0) {
diff --git a/front-end/src/views/management/topics/partitionedTopic.vue b/front-end/src/views/management/topics/partitionedTopic.vue
index 7ad9e58..1601773 100644
--- a/front-end/src/views/management/topics/partitionedTopic.vue
+++ b/front-end/src/views/management/topics/partitionedTopic.vue
@@ -56,6 +56,13 @@
           <el-table-column :label="$t('common.outBytes')" prop="outBytes"/>
         </el-table>
         <h4>{{ $t('topic.subscription.subscriptions') }}</h4>
+        <el-button
+          class="filter-item"
+          type="success"
+          style="margin-bottom: 15px"
+          @click="handleCreateSub">
+          New Sub
+        </el-button>
         <el-row :gutter="24">
           <el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 24}" :xl="{span: 24}">
             <el-table
@@ -128,6 +135,9 @@
                       <el-dropdown-item :command="{'action': 'clear', 'subscription': scope.row.subscription }">
                         {{ $t('topic.subscription.clear') }}
                       </el-dropdown-item>
+                      <el-dropdown-item :command="{'action': 'unsub', 'subscription': scope.row.subscription }">
+                        {{ $t('topic.subscription.unsub') }}
+                      </el-dropdown-item>
                     </el-dropdown-menu>
                   </el-dropdown>
                 </template>
@@ -268,6 +278,12 @@
             <span>{{ $t('topic.subscription.clearMessageConfirm') }}</span>
           </el-form-item>
         </el-form-item>
+        <el-form-item v-if="dialogStatus==='createSub'">
+          <el-input v-model="currentSubscription" placeholder="Please input sub name"/>
+        </el-form-item>
+        <el-form-item v-if="dialogStatus==='unsub'">
+          <h4>{{ $t('topic.subscription.deleteSubConfirm') }}</h4>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleOptions()">{{ $t('table.confirm') }}</el-button>
           <el-button @click="dialogFormVisible=false">{{ $t('table.cancel') }}</el-button>
@@ -294,6 +310,7 @@ import { fetchTopicsByPulsarManager } from '@/api/topics'
 import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
 import { formatBytes } from '@/utils/index'
 import { numberFormatter } from '@/filters/index'
+import { putSubscriptionOnCluster, deleteSubscriptionOnCluster } from '@/api/subscriptions'
 
 const defaultForm = {
   persistent: '',
@@ -347,7 +364,9 @@ export default {
         delete: this.$i18n.t('topic.deleteTopic'),
         expire: this.$i18n.t('topic.subscription.msgExpired'),
         clear: this.$i18n.t('topic.subscription.clearMessage'),
-        reset: this.$i18n.t('topic.subscription.resetByTime')
+        reset: this.$i18n.t('topic.subscription.resetByTime'),
+        createSub: this.$i18n.t('topic.subscription.sub'),
+        deleteSub: this.$i18n.t('topic.subscription.unsub')
       },
       dialogFormVisible: false,
       dialogStatus: '',
@@ -414,6 +433,7 @@ export default {
     initTopicStats() {
       fetchPartitionTopicStats(this.postForm.persistent, this.tenantNamespaceTopic, true).then(response => {
         if (!response.data) return
+        this.partitionTopicStats = []
         this.partitionTopicStats.push({
           inMsg: numberFormatter(response.data.msgRateIn, 2),
           outMsg: numberFormatter(response.data.msgRateOut, 2),
@@ -422,22 +442,30 @@ export default {
         })
         var prefix = this.postForm.persistent + '://' + this.tenantNamespaceTopic
         var tempPartitionsList = Object.keys(response.data.partitions)
+        this.partitionsList = []
         for (var i = 0; i < tempPartitionsList.length; i++) {
           var key = prefix + '-partition-' + i
-          var partition = this.postForm.topic + '-partition-' + i
-          this.partitionsList.push({
-            'partition': partition,
-            'producers': response.data.partitions[key].publishers.length,
-            'subscriptions': Object.keys(response.data.partitions[key].subscriptions).length,
-            'inMsg': numberFormatter(response.data.partitions[key].msgRateIn, 2),
-            'outMsg': numberFormatter(response.data.partitions[key].msgRateOut, 2),
-            'inBytes': formatBytes(response.data.partitions[key].msgThroughputIn),
-            'outBytes': formatBytes(response.data.partitions[key].msgThroughputOut),
-            'storageSize': formatBytes(response.data.partitions[key].storageSize, 0),
-            'partitionTopicLink': '/management/topics/' + this.postForm.persistent + '/' + this.tenantNamespaceTopic + '-partition-' + i + '/topic'
-          })
+          if (response.data.partitions.hasOwnProperty(key)) {
+            var partition = this.postForm.topic + '-partition-' + i
+            var publishers = 0
+            if (response.data.partitions[key].hasOwnProperty('publishers')) {
+              publishers = response.data.partitions[key].publishers.length
+            }
+            this.partitionsList.push({
+              'partition': partition,
+              'producers': publishers,
+              'subscriptions': Object.keys(response.data.partitions[key].subscriptions).length,
+              'inMsg': numberFormatter(response.data.partitions[key].msgRateIn, 2),
+              'outMsg': numberFormatter(response.data.partitions[key].msgRateOut, 2),
+              'inBytes': formatBytes(response.data.partitions[key].msgThroughputIn),
+              'outBytes': formatBytes(response.data.partitions[key].msgThroughputOut),
+              'storageSize': formatBytes(response.data.partitions[key].storageSize, 0),
+              'partitionTopicLink': '/management/topics/' + this.postForm.persistent + '/' + this.tenantNamespaceTopic + '-partition-' + i + '/topic'
+            })
+          }
         }
         var index = 0
+        this.subscriptionsList = []
         for (var s in response.data.subscriptions) {
           index += 1
           var type = 'Exclusive'
@@ -630,6 +658,12 @@ export default {
             case 'clear':
               this.clearAllSubMessage()
               break
+            case 'createSub':
+              this.createSub()
+              break
+            case 'unsub':
+              this.deleteSub()
+              break
           }
         }
       })
@@ -687,6 +721,44 @@ export default {
         this.dialogFormVisible = false
         this.getPartitionTopicInfo()
       })
+    },
+    handleCreateSub() {
+      this.currentSubscription = ''
+      this.dialogStatus = 'createSub'
+      this.dialogFormVisible = true
+    },
+    createSub() {
+      if (this.currentSubscription.length <= 0) {
+        this.$notify({
+          title: 'error',
+          message: this.$i18n.t('topic.subscription.subNotification'),
+          type: 'error',
+          duration: 3000
+        })
+        return
+      }
+      putSubscriptionOnCluster(this.getCurrentCluster(), this.postForm.persistent, this.tenantNamespaceTopic, this.currentSubscription).then(response => {
+        this.$notify({
+          title: 'success',
+          message: this.$i18n.t('topic.subscription.createSubSuccess'),
+          type: 'success',
+          duration: 3000
+        })
+        this.initTopicStats()
+        this.dialogFormVisible = false
+      })
+    },
+    deleteSub() {
+      deleteSubscriptionOnCluster(this.getCurrentCluster(), this.postForm.persistent, this.tenantNamespaceTopic, this.currentSubscription).then(response => {
+        this.$notify({
+          title: 'success',
+          message: this.$i18n.t('topic.subscription.deleteSubSuccess'),
+          type: 'success',
+          duration: 3000
+        })
+        this.initTopicStats()
+        this.dialogFormVisible = false
+      })
     }
   }
 }
diff --git a/front-end/src/views/management/topics/topic.vue b/front-end/src/views/management/topics/topic.vue
index 11f6fe7..43a0b3a 100644
--- a/front-end/src/views/management/topics/topic.vue
+++ b/front-end/src/views/management/topics/topic.vue
@@ -264,6 +264,13 @@
           </el-col>
         </el-row>
         <h4>{{ $t('topic.subscription.subscriptions') }}</h4>
+        <el-button
+          class="filter-item"
+          type="success"
+          style="margin-bottom: 15px"
+          @click="handleCreateSub">
+          {{ $t('topic.subscription.newSub') }}
+        </el-button>
         <el-row :gutter="24">
           <el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 24}" :xl="{span: 24}">
             <el-table
@@ -304,7 +311,7 @@
               <el-table-column :label="$t('topic.subscription.backlog')" min-width="30px" align="center">
                 <template slot-scope="scope">
                   <span>{{ scope.row.backlog }}</span>
-                  <el-dropdown>
+                  <el-dropdown @command="handleCommand(scope.row.subscription)">
                     <span class="el-dropdown-link"><i class="el-icon-more"/></span>
                     <el-dropdown-menu slot="dropdown">
                       <router-link :to="scope.row.subscriptionLink + '?topTab=backlogOperation&leftTab=skip'" class="link-type">
@@ -319,6 +326,7 @@
                       <router-link :to="scope.row.subscriptionLink + '?topTab=backlogOperation&leftTab=reset'" class="link-type">
                         <el-dropdown-item command="reset">{{ $t('topic.subscription.reset') }}</el-dropdown-item>
                       </router-link>
+                      <el-dropdown-item command="unsub">{{ $t('topic.subscription.unsub') }}</el-dropdown-item>
                     </el-dropdown-menu>
                   </el-dropdown>
                 </template>
@@ -502,8 +510,16 @@
         <el-form-item v-if="dialogStatus==='delete'">
           <h4>{{ $t('topic.deleteTopicMessage') }}</h4>
         </el-form-item>
+        <el-form-item v-if="dialogStatus==='createSub'">
+          <el-input v-model="subName" placeholder="Please input sub name"/>
+        </el-form-item>
+        <el-form-item v-if="dialogStatus==='deleteSub'">
+          <h4>{{ $t('topic.subscription.deleteSubConfirm') }}</h4>
+        </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="deleteTopic">{{ $t('table.confirm') }}</el-button>
+          <el-button v-if="dialogStatus==='delete'" type="primary" @click="deleteTopic">{{ $t('table.confirm') }}</el-button>
+          <el-button v-if="dialogStatus==='createSub'" type="primary" @click="createSub">{{ $t('table.confirm') }}</el-button>
+          <el-button v-if="dialogStatus==='deleteSub'" type="primary" @click="deleteSub">{{ $t('table.confirm') }}</el-button>
           <el-button @click="dialogFormVisible=false">{{ $t('table.cancel') }}</el-button>
         </el-form-item>
       </el-form>
@@ -534,6 +550,7 @@ import {
 import Pagination from '@/components/Pagination' // Secondary package based on el-pagination
 import { formatBytes } from '@/utils/index'
 import { numberFormatter } from '@/filters/index'
+import { putSubscriptionOnCluster, deleteSubscriptionOnCluster } from '@/api/subscriptions'
 
 const defaultForm = {
   persistent: '',
@@ -612,8 +629,11 @@ export default {
       currentTabName: '',
       nonPersistent: false,
       textMap: {
-        delete: this.$i18n.t('topic.deleteTopic')
+        delete: this.$i18n.t('topic.deleteTopic'),
+        createSub: this.$i18n.t('topic.subscription.sub'),
+        deleteSub: this.$i18n.t('topic.subscription.unsub')
       },
+      subName: '',
       dialogFormVisible: false,
       dialogStatus: '',
       topicPartitions: {},
@@ -727,6 +747,7 @@ export default {
     initTopicStats() {
       fetchTopicStats(this.postForm.persistent, this.getFullTopic()).then(response => {
         if (!response.data) return
+        this.topicStats = []
         this.topicStats.push({
           inMsg: numberFormatter(response.data.msgRateIn, 2),
           outMsg: numberFormatter(response.data.msgRateOut, 2),
@@ -744,6 +765,7 @@ export default {
             'since': response.data.publishers[i].connectedSince
           })
         }
+        this.subscriptionsList = []
         for (var s in response.data.subscriptions) {
           var type = ''
           if (response.data.subscriptions[s].hasOwnProperty('type')) {
@@ -1025,6 +1047,49 @@ export default {
         })
         this.$router.push({ path: '/management/namespaces/' + this.postForm.tenant + '/' + this.postForm.namespace + '/namespace?tab=topics' })
       })
+    },
+    handleCreateSub() {
+      this.subName = ''
+      this.dialogStatus = 'createSub'
+      this.dialogFormVisible = true
+    },
+    createSub() {
+      if (this.subName.length <= 0) {
+        this.$notify({
+          title: 'error',
+          message: this.$i18n.t('topic.subscription.subNotification'),
+          type: 'error',
+          duration: 3000
+        })
+        return
+      }
+      putSubscriptionOnCluster(this.getCurrentCluster(), this.postForm.persistent, this.getFullTopic(), this.subName).then(response => {
+        this.$notify({
+          title: 'success',
+          message: this.$i18n.t('topic.subscription.createSubSuccess'),
+          type: 'success',
+          duration: 3000
+        })
+        this.initTopicStats()
+        this.dialogFormVisible = false
+      })
+    },
+    handleCommand(subName) {
+      this.subName = subName
+      this.dialogStatus = 'deleteSub'
+      this.dialogFormVisible = true
+    },
+    deleteSub() {
+      deleteSubscriptionOnCluster(this.getCurrentCluster(), this.postForm.persistent, this.getFullTopic(), this.subName).then(response => {
+        this.$notify({
+          title: 'success',
+          message: this.$i18n.t('topic.subscription.deleteSubSuccess'),
+          type: 'success',
+          duration: 3000
+        })
+        this.initTopicStats()
+        this.dialogFormVisible = false
+      })
     }
   }
 }