Repository: openwisp/openwisp-utils (language: python)

Issue:
[bug] Currently open wifi sessions does not show anything if zero
If there are zero wifi clients connected to the system I would expect the dashboard pie chart to show the number zero, but it doesn't show anything, just plain gray.


Discussion / hints:
![Screenshot from 2022-06-24 16-15-34](https://user-images.githubusercontent.com/32094433/175519845-2ad657b6-a77f-4700-86a0-3d000a29dba9.png)

This occurs with other charts too. Probably a bug in openwisp-utils? 

Fix patch:
diff --git a/openwisp_utils/admin_theme/static/admin/js/ow-dashboard.js b/openwisp_utils/admin_theme/static/admin/js/ow-dashboard.js
index 31b77399..fe36a21a 100644
--- a/openwisp_utils/admin_theme/static/admin/js/ow-dashboard.js
+++ b/openwisp_utils/admin_theme/static/admin/js/ow-dashboard.js
@@ -1,6 +1,21 @@
 (function() {
+
   'use strict';
 
+  function slugify(str)
+    {
+        str = str.replace(/^\s+|\s+$/g, '');
+        // Make the string lowercase
+        str = str.toLowerCase();
+        // Remove invalid chars
+        str = str.replace(/[^a-z0-9 -]/g, '')
+          // Collapse whitespace and replace by -
+          .replace(/\s+/g, '-')
+          // Collapse dashes
+          .replace(/-+/g, '-');
+        return str;
+    }
+
   let elementsParam = Object.values(owDashboardCharts),
       container = document.getElementById('plot-container');
 
@@ -39,10 +54,11 @@
       hole: 0.6,
       showlegend: !elementsParam[i].hasOwnProperty('quick_link')
     },
-    element = document.createElement('div');
+    element = document.createElement('div'),
+    totalValues = 0;
 
     // Show a graph depicting disabled graph when there is insufficient data
-    if (elementsParam[i].query_params.values.length == 0) {
+    if (elementsParam[i].query_params.values.length === 0) {
       data.values = [1];
       data.labels = ['Not enough data'];
       data.marker = {
@@ -72,23 +88,22 @@
       data.filters = elementsParam[i].filters;
 
       // add total to pie chart
-      var total = 0;
       for (var c = 0; c < data.values.length; c++) {
-        total += data.values[c];
+        totalValues += data.values[c];
       }
-      layout.annotations = [
-        {
-          font: {
-            size: 20,
-            weight: 'bold'
-          },
-          showarrow: false,
-          text: `<b>${total}</b>`,
-          x: 0.5,
-          y: 0.5
-        }
-      ];
     }
+    layout.annotations = [
+      {
+        font: {
+          size: 20,
+          weight: 'bold'
+        },
+        showarrow: false,
+        text: `<b>${totalValues}</b>`,
+        x: 0.5,
+        y: 0.5
+      }
+    ];
 
     Plotly.newPlot(element, [data], layout, options);
 
@@ -126,6 +141,7 @@
         quickLinkContainer
       );
     }
+    element.classList.add(slugify(elementsParam[i].name));
     container.appendChild(element);
   }
 


Test patch:
diff --git a/tests/test_project/tests/test_selenium.py b/tests/test_project/tests/test_selenium.py
index b86937bd..3339d921 100644
--- a/tests/test_project/tests/test_selenium.py
+++ b/tests/test_project/tests/test_selenium.py
@@ -6,7 +6,7 @@
 from selenium.webdriver.support import expected_conditions as EC
 from selenium.webdriver.support.ui import WebDriverWait
 
-from ..models import Book, Shelf
+from ..models import Book, Operator, Shelf
 from . import CreateMixin
 from .utils import SeleniumTestMixin
 
@@ -643,3 +643,35 @@ def test_input_filters(self):
             self.web_driver.find_element_by_css_selector('.field-clear').click()
             self.web_driver.find_element_by_xpath(admin_xpath)
             self.web_driver.find_element_by_xpath(user_xpath)
+
+
+class TestDashboardCharts(SeleniumTestMixin, CreateMixin, StaticLiveServerTestCase):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+
+    def setUp(self):
+        self.admin = self._create_admin()
+        self.web_driver.set_window_size(1600, 768)
+
+    def test_pie_chart_zero_annotation(self):
+        Operator.objects.all().delete()
+        self.login()
+        url = reverse('admin:index')
+        self.open(url)
+        try:
+            WebDriverWait(self.web_driver, 10).until(
+                EC.visibility_of_element_located(
+                    (
+                        By.CSS_SELECTOR,
+                        '.operator-project-distribution .annotation-text tspan',
+                    )
+                )
+            )
+        except TimeoutException:
+            self.fail('Failed to find annotation text element in the chart')
+        else:
+            annotation_text = self.web_driver.find_element_by_css_selector(
+                '.operator-project-distribution .annotation-text tspan'
+            )
+            self.assertEqual(annotation_text.text, '0')