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.git


The following commit(s) were added to refs/heads/master by this push:
     new ef3514c  Add delete and peek functionality to dashboard (#4468)
ef3514c is described below

commit ef3514c65c7509a32561297ae6e9f17551c1ee52
Author: rshermanTHG <[email protected]>
AuthorDate: Mon Jun 10 02:38:16 2019 +0100

    Add delete and peek functionality to dashboard (#4468)
    
    ### Motivation
    
    
    Add some administration functionality to the dashboard.
    
    1. Ability to peek at the contents of messages in a subscriptions backlog
    2. Able to clear the backlog in a subscription
    3. Able to delete subscriptions that have no consumers
    4. Able to delete a namespace that has no topics
    
    ### Modifications
    
    All modifications are to the dashboard module only.
    
    - Added a new page that lists messages in the backlog as a link. When this 
link is clicked on the message pops up in a modal dialog
    - Added a deleted flag to several models and added this flag to the 
appropriate filters. (when an object is deleted this will keep the dashboard 
reflective of the state of the brokers until the next collector run).
    - Added functionality to delete a subscription if there are no consumers. 
Marks the subscription as deleted in the database. If this leaves a topic with 
no subscriptions then the topic is also marked in the database as deleted.
    - Added functionality to delete a namespace if there are no topics for that 
namespace. Marks the namespace as deleted in the database.
---
 dashboard/django/collector.py                      | 18 +++-
 dashboard/django/dashboard/settings.py             |  2 +
 .../migrations/0002_support_deleted_objects.py     | 53 ++++++++++++
 dashboard/django/stats/models.py                   | 14 +++-
 dashboard/django/stats/static/stats/additional.css | 59 +++++++++++++
 dashboard/django/stats/templates/stats/base.html   |  6 +-
 .../django/stats/templates/stats/messages.html     | 51 ++++++++++++
 .../django/stats/templates/stats/namespace.html    |  7 +-
 dashboard/django/stats/templates/stats/peek.html   | 22 +++++
 dashboard/django/stats/templates/stats/topic.html  | 21 ++++-
 .../django/stats/templatetags/stats_extras.py      |  4 +
 dashboard/django/stats/urls.py                     |  5 ++
 dashboard/django/stats/views.py                    | 97 +++++++++++++++++++---
 13 files changed, 340 insertions(+), 19 deletions(-)

diff --git a/dashboard/django/collector.py b/dashboard/django/collector.py
index 3ce0064..22974e3 100755
--- a/dashboard/django/collector.py
+++ b/dashboard/django/collector.py
@@ -99,7 +99,8 @@ def _fetch_broker_stats(cluster, broker_host_port, timestamp):
 
         namespace, _ = Namespace.objects.get_or_create(
             name=namespace_name,
-            property=property)
+            property=property,
+            timestamp=timestamp)
         namespace.clusters.add(cluster)
         namespace.save()
 
@@ -324,6 +325,18 @@ def _fetch_broker_stats(cluster, broker_host_port, 
timestamp):
             replication.topic = replication.topic
             replication.save()
 
+    tenants = get(broker_url, '/admin/v2/tenants')
+    for tenant_name in tenants:
+        namespaces = get(broker_url, '/admin/v2/namespaces/' + tenant_name)
+        for namespace_name in namespaces:
+            property, _ = Property.objects.get_or_create(name=tenant_name)
+            namespace, _ = Namespace.objects.get_or_create(
+                name=namespace_name,
+                property=property,
+                timestamp=timestamp)
+            namespace.clusters.add(cluster)
+            namespace.save()
+
 
 def update_or_create_object(db_bundles, db_topics, db_consumers, 
db_subscriptions):
     # For DB providers we have to insert or update one by one
@@ -404,7 +417,8 @@ def purge_db():
     Topic.objects.filter(timestamp__lt=threshold).delete()
     Subscription.objects.filter(timestamp__lt=threshold).delete()
     Consumer.objects.filter(timestamp__lt=threshold).delete()
-    logger.info("Finsihed purge db")
+    Namespace.objects.filter(timestamp__lt=threshold).delete()
+    logger.info("Finished purge db")
 
 
 def collect_and_purge():
diff --git a/dashboard/django/dashboard/settings.py 
b/dashboard/django/dashboard/settings.py
index 244169b..5b229f6 100644
--- a/dashboard/django/dashboard/settings.py
+++ b/dashboard/django/dashboard/settings.py
@@ -184,3 +184,5 @@ LOGGING = {
         }
     }
 }
+
+SERVICE_URL = os.getenv("SERVICE_URL")
diff --git a/dashboard/django/stats/migrations/0002_support_deleted_objects.py 
b/dashboard/django/stats/migrations/0002_support_deleted_objects.py
new file mode 100644
index 0000000..6e04eec
--- /dev/null
+++ b/dashboard/django/stats/migrations/0002_support_deleted_objects.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-06-03 13:24
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('stats', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='namespace',
+            name='deleted',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='namespace',
+            name='timestamp',
+            field=models.BigIntegerField(db_index=True, default=0),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='subscription',
+            name='deleted',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='topic',
+            name='deleted',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AlterField(
+            model_name='namespace',
+            name='name',
+            field=models.CharField(max_length=200),
+        ),
+        migrations.AlterIndexTogether(
+            name='namespace',
+            index_together={('name', 'timestamp', 'deleted')},
+        ),
+        migrations.AlterIndexTogether(
+            name='subscription',
+            index_together={('name', 'topic', 'timestamp', 'deleted')},
+        ),
+        migrations.AlterIndexTogether(
+            name='topic',
+            index_together={('name', 'cluster', 'timestamp', 'deleted')},
+        ),
+    ]
diff --git a/dashboard/django/stats/models.py b/dashboard/django/stats/models.py
index 637036d..8daaa5b 100644
--- a/dashboard/django/stats/models.py
+++ b/dashboard/django/stats/models.py
@@ -64,9 +64,11 @@ class Property(Model):
 
 @python_2_unicode_compatible
 class Namespace(Model):
-    name = CharField(max_length=200, unique=True)
+    name = CharField(max_length=200)
     property = ForeignKey(Property, on_delete=SET_NULL, db_index=True, 
null=True)
     clusters = ManyToManyField(Cluster)
+    timestamp = BigIntegerField(db_index=True)
+    deleted = BooleanField(default=False)
 
     def is_global(self):
         return self.name.split('/', 2)[1] == 'global'
@@ -74,6 +76,9 @@ class Namespace(Model):
     def __str__(self):
         return self.name
 
+    class Meta:
+        index_together = ('name', 'timestamp', 'deleted')
+
 @python_2_unicode_compatible
 class Bundle(Model):
     timestamp = BigIntegerField(db_index=True)
@@ -95,6 +100,7 @@ class Topic(Model):
     bundle = ForeignKey(Bundle, on_delete=SET_NULL, db_index=True, null=True)
 
     timestamp              = BigIntegerField(db_index=True)
+    deleted                = BooleanField(default=False)
     averageMsgSize         = IntegerField(default=0)
     msgRateIn              = DecimalField(max_digits = 12, decimal_places=1, 
default=0)
     msgRateOut             = DecimalField(max_digits = 12, decimal_places=1, 
default=0)
@@ -134,7 +140,7 @@ class Topic(Model):
         return url
 
     class Meta:
-        index_together = ('name', 'cluster', 'timestamp')
+        index_together = ('name', 'cluster', 'timestamp', 'deleted')
 
     def __str__(self):
         return self.name
@@ -146,6 +152,7 @@ class Subscription(Model):
     namespace        = ForeignKey(Namespace, on_delete=SET_NULL, null=True, 
db_index=True)
 
     timestamp        = BigIntegerField(db_index=True)
+    deleted          = BooleanField(default=False)
     msgBacklog       = BigIntegerField(default=0)
     msgRateExpired   = DecimalField(max_digits = 12, decimal_places=1, 
default=0)
     msgRateOut       = DecimalField(max_digits = 12, decimal_places=1, 
default=0)
@@ -161,6 +168,9 @@ class Subscription(Model):
     subscriptionType = CharField(max_length=1, choices=SUBSCRIPTION_TYPES, 
default='N')
     unackedMessages  = BigIntegerField(default=0)
 
+    class Meta:
+        index_together = ('name', 'topic', 'timestamp', 'deleted')
+
     def __str__(self):
         return self.name
 
diff --git a/dashboard/django/stats/static/stats/additional.css 
b/dashboard/django/stats/static/stats/additional.css
new file mode 100644
index 0000000..bd6ced8
--- /dev/null
+++ b/dashboard/django/stats/static/stats/additional.css
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+.autoscroll {
+    overflow: auto;
+}
+
+input.small-button {
+    padding: 2px;
+}
+
+/*Create a red cross using class x*/
+.x{
+    width: 20px;
+    height: 20px;
+    position:relative;
+    border-radius:2px;
+}
+.x::after,.x::before{
+    position:absolute;
+    top:9px;
+    left:0px;
+    content:'';
+    display:block;
+    width:20px;
+    height:2px;
+    background-color:red;
+
+}
+.x::after{
+    -webkit-transform: rotate(45deg);
+    -moz-transform: rotate(45deg);
+    -ms-transform: rotate(45deg);
+    -o-transform: rotate(45deg);
+    transform: rotate(45deg);
+}
+.x::before{
+    -webkit-transform: rotate(-45deg);
+    -moz-transform: rotate(-45deg);
+    -ms-transform: rotate(-45deg);
+    -o-transform: rotate(-45deg);
+    transform: rotate(-45deg);
+}
\ No newline at end of file
diff --git a/dashboard/django/stats/templates/stats/base.html 
b/dashboard/django/stats/templates/stats/base.html
index 9ffdb77..d798460 100644
--- a/dashboard/django/stats/templates/stats/base.html
+++ b/dashboard/django/stats/templates/stats/base.html
@@ -25,10 +25,11 @@
 <title>Pulsar Dashboard</title>
 <link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" 
%}" />
 <link rel="stylesheet" type="text/css" href="{% static 
"admin/css/changelists.css" %}" />
+<link rel="stylesheet" type="text/css" href="{% static 'stats/additional.css' 
%}">
 
 <script
-  src="https://code.jquery.com/jquery-3.1.1.slim.min.js";
-  integrity="sha256-/SIrNqv8h6QGKDuNoLGA4iret+kyesCkHGzVUUV0shc="
+  src="https://code.jquery.com/jquery-3.1.1.min.js";
+  integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
   crossorigin="anonymous"></script>
 
 {% block extrastyle %}{% endblock %}
@@ -81,6 +82,7 @@
     <div id="content" class="{% block coltype %}colM{% endblock %}">
         {% block pretitle %}{% endblock %}
         {% block content_title %}{% if title %}<h1>{{ title }}</h1>{% endif 
%}{% endblock %}
+        {% block content_subtitle %}{% if subtitle %}<h2>{{ subtitle }}</h2>{% 
endif %}{% endblock %}
         {% block content %}
         {% block object-tools %}{% endblock %}
         {{ content }}
diff --git a/dashboard/django/stats/templates/stats/messages.html 
b/dashboard/django/stats/templates/stats/messages.html
new file mode 100644
index 0000000..9374e79
--- /dev/null
+++ b/dashboard/django/stats/templates/stats/messages.html
@@ -0,0 +1,51 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you 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.
+
+-->
+{% extends "stats/base.html" %}
+
+{% load humanize %}
+{% load stats_extras %}
+{% block extrahead %}
+{% load static %}
+
+<script 
src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js";></script>
+<link rel="stylesheet" 
href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css";
 />
+{% endblock %}
+
+{% block title %}Topic | {{topic.name}}{% endblock %}
+
+
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+    <a href="{% url 'home' %}">Home</a>
+    &rsaquo; <a href="{% url 'property' topic.namespace.property.name %}">{{ 
topic.namespace.property }}</a>
+    &rsaquo; <a href="{% url 'namespace' topic.namespace.name %}">{{ 
topic.namespace.name }}</a>
+    &rsaquo; <a href="{% url 'topic' topic.url_name %}">{{ topic.short_name 
}}</a>
+    &rsaquo; {{ subscription.name }}
+</div>
+{% endblock %}
+
+{% block content %}
+{% for i in subscription.msgBacklog|times %}
+<li><a class ='btn' href="{% url 'peek' topic.url_name subscription.name i %}" 
rel="modal:open">view message {{ i }}</a></li>
+
+{% endfor %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/dashboard/django/stats/templates/stats/namespace.html 
b/dashboard/django/stats/templates/stats/namespace.html
index 8af1c5f..11563de 100644
--- a/dashboard/django/stats/templates/stats/namespace.html
+++ b/dashboard/django/stats/templates/stats/namespace.html
@@ -53,6 +53,7 @@
 </div>
 {% endif %}
 
+{% if topics.results %}
 <table>
 <thead>
     <tr>
@@ -91,6 +92,10 @@
 </table>
 
 {% table_footer topics %}
-
+{% else %}
+<form method="GET" action="{% url 'deleteNamespace' namespace.name %}">
+    <input type="submit" value="Delete this namespace"/>
+</form>
+{% endif %}
 
 {% endblock %}
diff --git a/dashboard/django/stats/templates/stats/peek.html 
b/dashboard/django/stats/templates/stats/peek.html
new file mode 100644
index 0000000..ebe4821
--- /dev/null
+++ b/dashboard/django/stats/templates/stats/peek.html
@@ -0,0 +1,22 @@
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you 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.
+
+-->
+
+<pre id="output" class="autoscroll">{{ message_body }}</pre>
\ No newline at end of file
diff --git a/dashboard/django/stats/templates/stats/topic.html 
b/dashboard/django/stats/templates/stats/topic.html
index 467b5a7..ddba7bd 100644
--- a/dashboard/django/stats/templates/stats/topic.html
+++ b/dashboard/django/stats/templates/stats/topic.html
@@ -91,10 +91,13 @@
         <th title="Bytes/s">Throughput out</th>
         <th title="Msg/s">Rate expired</th>
         <th title="Msg/s">Unacked</th>
+        <th title="Delete">Delete Subscription</th>
+        <th title="Clear messages for subscription">Clear Backlog</th>
+        <th title="Peek messages">Peek</th>
     </tr>
 </thead>
 
-<tbody
+<tbody>
 {% for sub, consumers in subscriptions %}
     <tr class="{% cycle 'row1' 'row2' %}">
         {% if consumers %}
@@ -108,6 +111,22 @@
         <td>{{sub.msgThroughputOut | intcomma}}</td>
         <td>{{sub.msgRateExpired | intcomma}}</td>
         <td>{{sub.unackedMessages | intcomma}}</td>
+        {% if consumers %}
+        <td><div title="Cannot delete subscription" class="x"/></td>
+        {% else %}
+        <td>
+            <form method="GET" action="{% url 'deleteSubscription' 
topic.url_name sub.name %}">
+                <input type="submit" value="Delete Subscription" 
class="small-button"/>
+            </form></td>
+<!--        <td><a href="{% url 'deleteSubscription' topic.url_name sub.name 
%}">Delete Subscription</a></td>-->
+        {% endif %}
+        {% if sub.msgBacklog > 0 %}
+        <td><a title="Clear backlog from this subscription" href="{% url 
'clearSubscription' topic.url_name sub.name %}">clear</a></td>
+        <td><a href="{% url 'messages' topic.url_name sub.name %}">View 
Messages</a></td>
+        {% else %}
+        <td></td>
+        <td></td>
+        {% endif %}
     </tr>
 
     <tr class="{% cycle 'row1' 'row2' %}" id="consumers-{{sub.id}}"
diff --git a/dashboard/django/stats/templatetags/stats_extras.py 
b/dashboard/django/stats/templatetags/stats_extras.py
index 78f7427..81ca5cf 100644
--- a/dashboard/django/stats/templatetags/stats_extras.py
+++ b/dashboard/django/stats/templatetags/stats_extras.py
@@ -62,3 +62,7 @@ def mbps(bytes_per_seconds):
 def safe_intcomma(n):
     if not n: return 0
     else: return intcomma(n)
+
[email protected](name='times')
+def times(number):
+    return range(1, number + 1)
\ No newline at end of file
diff --git a/dashboard/django/stats/urls.py b/dashboard/django/stats/urls.py
index 0c488bd..af1c8d6 100644
--- a/dashboard/django/stats/urls.py
+++ b/dashboard/django/stats/urls.py
@@ -24,6 +24,7 @@ from . import views
 urlpatterns = [
     url(r'^property/(?P<property_name>.+)/$', views.property, name='property'),
     url(r'^namespace/(?P<namespace_name>.+)/$', views.namespace, 
name='namespace'),
+    url(r'^deleteNamespace/(?P<namespace_name>.+)$', views.deleteNamespace, 
name='deleteNamespace'),
 
 
     url(r'^brokers/$', views.brokers, name='brokers'),
@@ -34,4 +35,8 @@ urlpatterns = [
     url(r'^topic/(?P<topic_name>.+)/$', views.topic, name='topic'),
 
     url(r'^clusters/$', views.clusters, name='clusters'),
+    url(r'^clearSubscription/(?P<topic_name>.+)/(?P<subscription_name>.+)$', 
views.clearSubscription, name='clearSubscription'),
+    url(r'^deleteSubscription/(?P<topic_name>.+)/(?P<subscription_name>.+)$', 
views.deleteSubscription, name='deleteSubscription'),
+    
url(r'^peek/(?P<topic_name>.+)/(?P<subscription_name>.+)/(?P<message_number>.+)$',
 views.peek, name='peek'),
+    url(r'^messages/(?P<topic_name>.+)/(?P<subscription_name>.+)$', 
views.messages, name='messages'),
 ]
diff --git a/dashboard/django/stats/views.py b/dashboard/django/stats/views.py
index bed8c81..a371aa0a 100644
--- a/dashboard/django/stats/views.py
+++ b/dashboard/django/stats/views.py
@@ -18,11 +18,13 @@
 #
 
 import logging
-from django.shortcuts import render, get_object_or_404
+from django.shortcuts import render, get_object_or_404, redirect
 from django.template import loader
 from django.urls import reverse
 from django.views import generic
-from django.db.models import Q
+from django.db.models import Q, IntegerField
+from dashboard import settings
+import requests, json, re
 
 from django.http import HttpResponseRedirect, HttpResponse
 from .models import *
@@ -42,9 +44,16 @@ class HomeView(generic.ListView):
 def home(request):
     ts = get_timestamp()
     properties = Property.objects.filter(
-            namespace__topic__timestamp = ts,
         ).annotate(
-            numNamespaces = Count('namespace__name', distinct=True),
+            numNamespaces = Subquery(
+                Namespace.objects.filter(
+                    deleted=False,
+                    property=OuterRef('pk')
+                ).values('property')
+                    .annotate(cnt=Count('pk'))
+                    .values('cnt'),
+                output_field=IntegerField()
+            ),
             numTopics    = Count('namespace__topic__name', distinct=True),
             numProducers = Sum('namespace__topic__producerCount'),
             numSubscriptions = Sum('namespace__topic__subscriptionCount'),
@@ -71,7 +80,8 @@ def property(request, property_name):
     ts = get_timestamp()
     namespaces = Namespace.objects.filter(
             property = property,
-            topic__timestamp = ts,
+            timestamp = ts,
+            deleted = False,
         ).annotate(
             numTopics    = Count('topic'),
             numProducers = Sum('topic__producerCount'),
@@ -97,18 +107,20 @@ def property(request, property_name):
 def namespace(request, namespace_name):
     selectedClusterName = request.GET.get('cluster')
 
-    namespace = get_object_or_404(Namespace, name=namespace_name)
+    namespace = get_object_or_404(Namespace, name=namespace_name, 
timestamp=get_timestamp(), deleted=False)
     topics = Topic.objects.select_related('broker', 'namespace', 'cluster')
     if selectedClusterName:
         topics = topics.filter(
                     namespace     = namespace,
                     timestamp     = get_timestamp(),
-                    cluster__name = selectedClusterName
+                    cluster__name = selectedClusterName,
+                    deleted       = False
                 )
     else:
         topics = topics.filter(
                     namespace = namespace,
-                    timestamp = get_timestamp()
+                    timestamp = get_timestamp(),
+                    deleted   = False
                 )
 
     topics = Table(request, topics, default_sort='name')
@@ -120,6 +132,15 @@ def namespace(request, namespace_name):
     })
 
 
+def deleteNamespace(request, namespace_name):
+    url = settings.SERVICE_URL + '/admin/v2/namespaces/' + namespace_name
+    response = requests.delete(url)
+    status = response.status_code
+    logger.debug("Delete namespace " + namespace_name + " status - " + 
str(status))
+    if status == 204:
+        Namespace.objects.filter(name=namespace_name, 
timestamp=get_timestamp()).update(deleted=True)
+    return redirect('property', property_name=namespace_name.split('/', 1)[0])
+
 def topic(request, topic_name):
     timestamp = get_timestamp()
     topic_name = 'persistent://' + topic_name.split('persistent/', 1)[1]
@@ -131,7 +152,7 @@ def topic(request, topic_name):
         clusters = [x.cluster for x in Topic.objects.filter(name=topic_name, 
timestamp=timestamp).order_by('cluster__name')]
     else:
         topic = get_object_or_404(Topic, name=topic_name, timestamp=timestamp)
-    subscriptions = Subscription.objects.filter(topic=topic).order_by('name')
+    subscriptions = Subscription.objects.filter(topic=topic, 
timestamp=timestamp, deleted=False).order_by('name')
 
     subs = []
 
@@ -170,11 +191,13 @@ def topics(request):
     if selectedClusterName:
         topics = topics.filter(
                     timestamp     = get_timestamp(),
-                    cluster__name = selectedClusterName
+                    cluster__name = selectedClusterName,
+                    deleted = False
                 )
     else:
         topics = topics.filter(
-                    timestamp = get_timestamp()
+                    timestamp = get_timestamp(),
+                    deleted = False
                 )
 
     topics = Table(request, topics, default_sort='cluster__name')
@@ -282,3 +305,55 @@ def clusters(request):
     return render(request, 'stats/clusters.html', {
         'clusters' : clusters,
     })
+
+
+def clearSubscription(request, topic_name, subscription_name):
+    url = settings.SERVICE_URL + '/admin/v2/' + topic_name + '/subscription/' 
+ subscription_name + '/skip_all'
+    requests.post(url)
+    return redirect('topic', topic_name=topic_name)
+
+def deleteSubscription(request, topic_name, subscription_name):
+    url = settings.SERVICE_URL + '/admin/v2/' + topic_name + '/subscription/' 
+ subscription_name
+    response = requests.delete(url)
+    status = response.status_code
+    if status == 204:
+        ts = get_timestamp()
+        topic_db_name = 'persistent://' + topic_name.split('persistent/', 1)[1]
+        topic = Topic.objects.filter(name=topic_db_name, timestamp=ts)[0]
+        Subscription.objects.filter(name=subscription_name, topic=topic, 
timestamp=ts).update(deleted=True)
+        subscriptions = Subscription.objects.filter(topic=topic, 
deleted=False, timestamp=ts)
+        if not subscriptions:
+            topic.deleted=True
+            topic.save(update_fields=['deleted'])
+            m = re.search(r"persistent/(?P<namespace>.*)/.*", topic_name)
+            namespace_name = m.group("namespace")
+            return redirect('namespace', namespace_name=namespace_name)
+    return redirect('topic', topic_name=topic_name)
+
+def messages(request, topic_name, subscription_name):
+    topic_name = 'persistent://' + topic_name.split('persistent/', 1)[1]
+    timestamp = get_timestamp()
+    cluster_name = request.GET.get('cluster')
+
+    if cluster_name:
+        topic = get_object_or_404(Topic, name=topic_name, 
cluster__name=cluster_name, timestamp=timestamp)
+    else:
+        topic = get_object_or_404(Topic, name=topic_name, timestamp=timestamp)
+    subscription = get_object_or_404(Subscription, topic=topic, 
name=subscription_name)
+
+    return render(request, 'stats/messages.html', {
+        'topic' : topic,
+        'subscription' : subscription,
+        'title' : topic.name,
+        'subtitle' : subscription_name,
+    })
+
+def peek(request, topic_name, subscription_name, message_number):
+    url = settings.SERVICE_URL + '/admin/v2/' + topic_name + '/subscription/' 
+ subscription_name + '/position/' + message_number
+    response = requests.get(url)
+    message = response.text
+    message = message[message.index('{'):]
+    context = {
+        'message_body' : json.dumps(json.loads(message), indent=4),
+    }
+    return render(request, 'stats/peek.html', context)
\ No newline at end of file

Reply via email to