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:
parent
f04305601a
commit
26485a2933
|
@ -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
|
||||
)
|
|
@ -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 %}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue