ui: Add support for filtering and ordering playbook index

This allows the playbook index to be filtered and ordered by doing
things like:
- ?name=playbookname
- ?path=/etc/ansible
- ?status=completed&status=failed
- ?order=id (oldest at the top)
- ?order=-id (most recent at the top)
- ?order=-duration (longest running playbook at the top)

Change-Id: I02a69f507106d434307ce99f4a153e5338377dda
This commit is contained in:
David Moreau Simard 2019-11-05 14:00:05 -05:00
parent f04305601a
commit 26485a2933
No known key found for this signature in database
GPG Key ID: 938880DAFC753E80
4 changed files with 153 additions and 4 deletions

28
ara/ui/forms.py Normal file
View File

@ -0,0 +1,28 @@
# Copyright (c) 2019 Red Hat, Inc.
#
# This file is part of ARA Records Ansible.
#
# ARA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ARA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from ara.api import models
class PlaybookSearchForm(forms.Form):
name = forms.CharField(label="Playbook name", max_length=255, required=False)
path = forms.CharField(label="Playbook path", max_length=255, required=False)
status = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple, choices=models.Playbook.STATUS, required=False
)

View File

@ -1,5 +1,83 @@
{% extends "base.html" %}
{% block body %}
{% if not static_generation %}
{% load datetime_formatting %}
{% if search_query %}
<details id="search" open>
<summary>Search, sort and filter</summary>
<a href="/">
<button class="pf-c-button pf-m-plain pf-m-link" type="button" aria-label="Remove">
<i class="fas fa-times" aria-hidden="true"></i> Clear filters
</button>
</a>
{% else %}
<details id="search">
<summary>Search, Sort and Filter by date</summary>
{% endif %}
<div class="pf-l-flex pf-m-space-items-xl">
<div class="pf-l-flex__item">
<h1 class="pf-c-title pf-m-2xl"><i class="fas fa-search"></i> Search</h1>
<ul class="pf-c-list">
<form novalidate action="/" method="get" class="pf-c-form pf-m-horizontal">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="name">
<span class="pf-c-form__label-text">Playbook name</span>
</label>
<div class="pf-c-form__horizontal-group">
<input class="pf-c-form-control" type="text" id="name" name="name" value="{% if search_form.name.value is not null %}{{ search_form.name.value }}{% endif %}" />
</div>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="name">
<span class="pf-c-form__label-text">Playbook path</span>
</label>
<div class="pf-c-form__horizontal-group">
<input class="pf-c-form-control" type="text" id="path" name="path" value="{% if search_form.path.value is not null %}{{ search_form.path.value }}{% endif %}" />
</div>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="status">
<span class="pf-c-form__label-text">Playbook status</span>
</label>
<div class="pf-c-form__horizontal-group">
<fieldset class="pf-c-form__fieldset" aria-labelledby="select-checkbox-expanded-label">
{% for value, text in search_form.status.field.choices %}
<label class="pf-c-check pf-c-select__menu-item" for="select-checkbox-expanded-active">
{% if value in search_form.status.data %}
<input class="pf-c-check__input" type="checkbox" id="{{ value }}" name="status" value="{{ value }}" checked />
{% else %}
<input class="pf-c-check__input" type="checkbox" id="{{ value }}" name="status" value="{{ value }}" />
{% endif %}
<span class="pf-c-check__label">{{ value }}</span>
</label>
{% endfor %}
</fieldset>
</div>
</div>
<input type="submit" value="Submit">
</form>
</ul>
</div>
<div class="pf-l-flex__item">
<h1 class="pf-c-title pf-m-2xl"><i class="fas fa-sort"></i> Sort by</h1>
<ul class="pf-c-list">
<li>Started: <a href="?order=started">ascending</a> | <a href="?order=-started">descending</a></li>
<li>Ended: <a href="?order=ended">ascending</a> | <a href="?order=-ended">descending</a></li>
<li>Duration: <a href="?order=duration">ascending</a> | <a href="?order=-duration">descending</a></li>
</ul>
</div>
<div class="pf-l-flex__item">
<h1 class="pf-c-title pf-m-2xl"><i class="fas fa-clock"></i> Filter by date</h1>
<ul class="pf-c-list">
<li><a href="?started_after={% past_timestamp with days=30 %}">Last 30 days</a></li>
<li><a href="?started_after={% past_timestamp with days=7 %}">Last 7 days</a></li>
<li><a href="?started_after={% past_timestamp with hours=24 %}">Last 24 hours</a></li>
<li><a href="?started_after={% past_timestamp with minutes=60 %}">Last 60 minutes</a></li>
</ul>
</div>
</div>
</details>
{% endif %}
{% for playbook in playbooks %}
{% include "partials/playbook_card.html" %}
{% endfor %}

View File

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with ARA. If not, see <http://www.gnu.org/licenses/>.
import datetime
from django import template
from django.utils.dateparse import parse_datetime
@ -31,3 +33,28 @@ def format_duration(duration):
@register.filter(name="format_date")
def format_datetime(datetime):
return parse_datetime(datetime).strftime("%a, %d %b %Y %H:%M:%S %z")
@register.simple_tag(name="past_timestamp")
def past_timestamp(weeks=0, days=0, hours=0, minutes=0, seconds=0):
"""
Produces a timestamp from the past compatible with the API.
Used to provide time ranges by templates.
Expects a dictionary of arguments to timedelta, for example:
datetime.timedelta(hours=24)
datetime.timedelta(days=7)
See: https://docs.python.org/3/library/datetime.html#datetime.timedelta
"""
delta = dict()
if weeks:
delta["weeks"] = weeks
if days:
delta["days"] = days
if hours:
delta["hours"] = hours
if minutes:
delta["minutes"] = minutes
if seconds:
delta["seconds"] = seconds
return (datetime.datetime.now() - datetime.timedelta(**delta)).isoformat()

View File

@ -2,21 +2,37 @@ from rest_framework import generics
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from ara.api import models, serializers
from ara.api import filters, models, serializers
from ara.ui import forms
class Index(generics.RetrieveAPIView):
class Index(generics.ListAPIView):
"""
Returns a list of playbook summaries
"""
queryset = models.Playbook.objects.all()
filterset_class = filters.PlaybookFilter
renderer_classes = [TemplateHTMLRenderer]
template_name = "index.html"
def get(self, request, *args, **kwargs):
serializer = serializers.ListPlaybookSerializer(self.queryset.all(), many=True)
return Response({"page": "index", "playbooks": serializer.data})
# TODO: Can we retrieve those fields automatically ?
fields = ["order", "name", "started_after", "status"]
search_query = False
for field in fields:
if field in request.GET:
search_query = True
if search_query:
search_form = forms.PlaybookSearchForm(request.GET)
else:
search_form = forms.PlaybookSearchForm()
serializer = serializers.ListPlaybookSerializer(self.filter_queryset(self.queryset.all()), many=True)
return Response(
{"page": "index", "playbooks": serializer.data, "search_form": search_form, "search_query": search_query}
)
class Playbook(generics.RetrieveAPIView):