diff --git a/CHANGELOG.md b/CHANGELOG.md index 515b8ecf..cae71802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. - Add the "Badges" page in docs - Add support for multiple recipients in incoming email (#669) - Upgrade to fido2 1.0.0, requests 2.28.1, segno 1.5.2 +- Implement auto-refresh and running indicator in the My Projects page (#681) ### Bug Fixes - Fix the display of ignored pings with non-zero exit status diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 54fb9784..1aa6f596 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -394,19 +394,6 @@ class Project(models.Model): for profile in q: profile.update_next_nag_date() - def overall_status(self): - if not hasattr(self, "_overall_status"): - self._overall_status = "up" - for check in self.check_set.all(): - check_status = check.get_status() - if check_status == "grace" and self._overall_status == "up": - self._overall_status = "grace" - elif check_status == "down": - self._overall_status = "down" - break - - return self._overall_status - def get_n_down(self): result = 0 for check in self.check_set.all(): diff --git a/hc/front/views.py b/hc/front/views.py index 137ce6e8..2e09710c 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -1,3 +1,4 @@ +from collections import defaultdict from datetime import timedelta as td import email import json @@ -298,29 +299,51 @@ def switch_channel(request, code, channel_code): return HttpResponse() +def _get_project_summary(profile): + statuses = defaultdict(lambda: {"status": "up", "started": False}) + q = profile.checks_from_all_projects() + q = q.annotate(project_code=F("project__code")) + for check in q: + summary = statuses[check.project_code] + if check.last_start: + summary["started"] = True + + if summary["status"] != "down": + status = check.get_status() + if status == "down" or (status == "grace" and summary["status"] == "up"): + summary["status"] = status + + return statuses + + def index(request): - if request.user.is_authenticated: - project_ids = request.profile.projects().values("id") + if not request.user.is_authenticated: + return redirect("hc-login") - q = Project.objects.filter(id__in=project_ids) - q = q.annotate(n_checks=Count("check", distinct=True)) - q = q.annotate(n_channels=Count("channel", distinct=True)) - q = q.annotate(owner_email=F("owner__email")) + summary = _get_project_summary(request.profile) + if "refresh" in request.GET: + return JsonResponse({str(k): v for k, v in summary.items()}) - projects = list(q) - # Primary sort key: projects with overall_status=down go first - # Secondary sort key: project's name - projects.sort(key=lambda p: (p.overall_status() != "down", p.name)) + q = request.profile.projects() + q = q.annotate(n_checks=Count("check", distinct=True)) + q = q.annotate(n_channels=Count("channel", distinct=True)) + q = q.annotate(owner_email=F("owner__email")) + projects = list(q) + for project in projects: + project.overall_status = summary[project.code]["status"] + project.any_started = summary[project.code]["started"] - ctx = { - "page": "projects", - "projects": projects, - "last_project_id": request.session.get("last_project_id"), - } + # Primary sort key: projects with overall_status=down go first + # Secondary sort key: project's name + projects.sort(key=lambda p: (p.overall_status != "down", p.name)) - return render(request, "front/projects.html", ctx) + ctx = { + "page": "projects", + "projects": projects, + "last_project_id": request.session.get("last_project_id"), + } - return redirect("hc-login") + return render(request, "front/projects.html", ctx) def dashboard(request): diff --git a/static/css/projects.css b/static/css/projects.css index 0bba2db3..1d872022 100644 --- a/static/css/projects.css +++ b/static/css/projects.css @@ -26,11 +26,6 @@ text-overflow: ellipsis; } -#project-selector .project .status { - position: absolute; - left: 24px; -} - #project-selector #add-project .project { border-style: dashed; padding: 0; @@ -46,3 +41,8 @@ color: var(--text-color); } +.indicators { + position: absolute; + left: 24px; + width: 24px; +} \ No newline at end of file diff --git a/static/js/projects.js b/static/js/projects.js new file mode 100644 index 00000000..0a1c129d --- /dev/null +++ b/static/js/projects.js @@ -0,0 +1,32 @@ +$(function () { + var base = document.getElementById("base-url").getAttribute("href").slice(0, -1); + + // Schedule refresh to run every 3s when tab is visible and user + // is active, every 60s otherwise + var lastStatus = {}; + var lastStarted = {}; + function refreshStatus() { + $.ajax({ + url: base + "?refresh=1", + dataType: "json", + timeout: 2000, + success: function(data) { + for (var code in data) { + var el = data[code]; + + if (el.status != lastStatus[code]) { + $("#" + code + " div.status").attr("class", "status ic-" + el.status); + lastStatus[code] = el.status; + } + + if (el.started != lastStarted[code]) { + $("#" + code + " div.spinner").toggleClass("started", el.started); + lastStarted[code] = el.started; + } + } + } + }); + } + + adaptiveSetInterval(refreshStatus); +}); diff --git a/templates/front/projects.html b/templates/front/projects.html index b3588c59..0edac1cd 100644 --- a/templates/front/projects.html +++ b/templates/front/projects.html @@ -14,9 +14,12 @@ <div id="project-selector" class="row"> {% for project in projects %} <a href="{% url 'hc-checks' project.code %}"> - <div class="col-sm-6 col-md-4"> + <div id="{{ project.code }}" class="col-sm-6 col-md-4"> <div class="panel project {% if project.id == last_project_id %}selected{% endif %}"> - <div class="status ic-{{ project.overall_status }}"></div> + <div class="indicators"> + <div class="status ic-{{ project.overall_status }}"></div> + <div class="spinner {% if project.any_started %}started{% endif %}"></div> + </div> <h4>{{ project }}</h4> <div> {{ project.n_checks }} check{{ project.n_checks|pluralize }}, @@ -51,6 +54,8 @@ {% compress js %} <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script> +<script src="{% static 'js/adaptive-setinterval.js' %}"></script> <script src="{% static 'js/add_project_modal.js' %}"></script> +<script src="{% static 'js/projects.js' %}"></script> {% endcompress %} {% endblock %}