Add: storage info widget and storage api endpoint refactoring project structure add version app

This commit is contained in:
Stepan Zhukovsky 2023-09-07 13:07:18 +09:00
parent e95de1b553
commit 016994d594
28 changed files with 487 additions and 216 deletions

View File

@ -5,7 +5,7 @@ from django.utils.html import format_html
from django.utils.translation import ngettext from django.utils.translation import ngettext
from .models import Platform, Archive, Ticket from .models import Platform, Archive, Ticket
from .utils import sizify from .utils.helpers import sizify
class PlatformAdmin(admin.ModelAdmin): class PlatformAdmin(admin.ModelAdmin):

View File

@ -18,4 +18,5 @@ router.register(r'tickets', views.TicketViewSet)
urlpatterns = [ urlpatterns = [
# CRUD: # CRUD:
path('v1/', include(router.urls)), path('v1/', include(router.urls)),
path('v1/storage/', views.StorageInfo.as_view(), name='storage-info'),
] ]

View File

@ -1,4 +1,5 @@
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.conf import settings
from rest_framework import status from rest_framework import status
# from rest_framework.decorators import action # from rest_framework.decorators import action
@ -10,6 +11,7 @@ from rest_framework.parsers import (
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework import views
from rest_framework import filters from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@ -18,6 +20,7 @@ from drf_spectacular.utils import extend_schema
from drf_spectacular.openapi import OpenApiParameter from drf_spectacular.openapi import OpenApiParameter
from collector.models import Archive, Ticket, Platform from collector.models import Archive, Ticket, Platform
from collector.utils.helpers import get_mount_fs_info
from .filters import ArchiveFilter, TicketFilter from .filters import ArchiveFilter, TicketFilter
from .permissions import IsGuestUpload from .permissions import IsGuestUpload
@ -122,3 +125,10 @@ class TicketViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(user=self.request.user) serializer.save(user=self.request.user)
class StorageInfo(views.APIView):
"""Info about storage total/used/free space"""
def get(self, request):
return Response(get_mount_fs_info(settings.MEDIA_ROOT))

View File

@ -0,0 +1,14 @@
from django.conf import settings
from .utils.helpers import get_mount_fs_info
def metadata(request):
return {
"version": settings.VERSION,
"environment": settings.ENVIRONMENT,
}
def storage_info(request):
return get_mount_fs_info(settings.MEDIA_ROOT)

View File

@ -47,7 +47,7 @@ class Migration(migrations.Migration):
name='Archive', name='Archive',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(upload_to=collector.utils.logs_dir_path)), ('file', models.FileField(upload_to=collector.utils.helpers.logs_dir_path)),
('size', models.BigIntegerField(editable=False)), ('size', models.BigIntegerField(editable=False)),
('md5', models.CharField(editable=False, max_length=1024)), ('md5', models.CharField(editable=False, max_length=1024)),
('time_create', models.DateTimeField(auto_now_add=True)), ('time_create', models.DateTimeField(auto_now_add=True)),

View File

@ -6,7 +6,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from .utils import logs_dir_path from .utils.helpers import logs_dir_path
class Archive(models.Model): class Archive(models.Model):

View File

@ -0,0 +1,69 @@
// formatted byte size to human readable:
const sizify = (value) => {
let ext = ''
if (value < 512000) {
value = value / 1024.0
ext = 'KB'
} else if (value < 4194304000) {
value = value / 1048576.0
ext = 'MB'
} else {
value = value / 1073741824.0
ext = 'GB'
};
return `${Math.round(value * 10) / 10} ${ext}`
};
// fix update bootstrap tooltip func:
const updateBsTooltip = (instance) => {
let tt = bootstrap.Tooltip.getInstance(instance);
tt.dispose();
bootstrap.Tooltip.getOrCreateInstance(instance);
};
// update storage info widget:
const updateStorageInfo = () => {
// set storage items vars:
let storageIcon = $("#storage_icon")
let storageProgressContainer = $("#storage_progress_container")
let storage_progress = $("#storage_progress")
// set API url:
const storageUrl = storage_progress.attr("storage-url")
$.ajax({
type: "GET",
url: storageUrl,
headers: {
"Content-Type":"application/json"
},
dataType: "json",
success: function (data, textStatus, jqXHR) {
// JSON answer:
let storage = data.storage
// set updated fields:
let storageInfoNewFields = [
`Total: ${sizify(storage.total)}`,
'<br>',
`Used: ${sizify(storage.used)}`,
'<br>',
`Free: ${sizify(storage.free)}`
].join('')
// progress bar update:
storage_progress.attr("style", `width:${storage.used_percent}%`)
// progress bar color update:
if (storage.used_percent > 90) {
storage_progress.attr("class", "progress-bar bg-danger");
} else if (storage.used_percent > 80) {
storage_progress.attr("class", "progress-bar bg-warning");
} else {
storage_progress.attr("class", "progress-bar bg-success");
};
// tooltips update:
storageIcon.attr("data-bs-title", `Storage used: ${storage.used_percent}%`)
storageProgressContainer.attr("data-bs-title", storageInfoNewFields)
updateBsTooltip(storageIcon)
updateBsTooltip(storageProgressContainer)
}
});
};
export {sizify, updateBsTooltip, updateStorageInfo};

View File

@ -1,3 +1,6 @@
import {updateStorageInfo} from "./helpers.js";
$(function () { $(function () {
console.log("JQ is ready to work"); console.log("JQ is ready to work");
@ -24,8 +27,11 @@ $(function () {
success: function (data, textStatus, jqXHR) { success: function (data, textStatus, jqXHR) {
console.log(jqXHR.status); console.log(jqXHR.status);
$(archiveListElement).hide(1500); $(archiveListElement).hide(1500);
setTimeout(() => {
updateStorageInfo();
}, 3000);
}, },
error: function (data, textStatus, jqXHR) { error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.status); console.log(jqXHR.status);
} }
}); });
@ -57,7 +63,7 @@ $(function () {
success: function (data, textStatus, jqXHR) { success: function (data, textStatus, jqXHR) {
console.log(jqXHR.status) console.log(jqXHR.status)
}, },
error: function (data, textStatus, jqXHR) { error: function (jqXHR, textStatus, errorThrown) {
console.log(data) console.log(data)
console.log(jqXHR.status) console.log(jqXHR.status)
} }
@ -82,11 +88,14 @@ $(function () {
console.log(jqXHR.status); console.log(jqXHR.status);
if (delDiv.length) { if (delDiv.length) {
delDiv.hide(1500); delDiv.hide(1500);
setTimeout(() => {
updateStorageInfo();
}, 3000);
} else { } else {
window.location.href = redirectUrl; window.location.href = redirectUrl;
} }
}, },
error: function (data, textStatus, jqXHR) { error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.status); console.log(jqXHR.status);
} }
}); });

View File

@ -1,3 +1,5 @@
import {updateStorageInfo} from "./helpers.js";
$(function () { $(function () {
const uploadForm = document.getElementById('upload_form'); const uploadForm = document.getElementById('upload_form');
const input_file = document.getElementById('id_file'); const input_file = document.getElementById('id_file');
@ -6,7 +8,7 @@ $(function () {
$("#upload_form").submit(function(e){ $("#upload_form").submit(function(e){
e.preventDefault(); e.preventDefault();
$form = $(this) // $form = $(this)
let formData = new FormData(this); let formData = new FormData(this);
let upload_token = formData.get("token") let upload_token = formData.get("token")
const media_data = input_file.files[0]; const media_data = input_file.files[0];
@ -51,6 +53,11 @@ $(function () {
].join('') ].join('')
uploadForm.reset() uploadForm.reset()
progress_bar.classList.add('not-visible') progress_bar.classList.add('not-visible')
try {
updateStorageInfo();
} catch (error) {
console.log(error)
};
}, },
error: function(jqXHR, textStatus, errorThrown){ error: function(jqXHR, textStatus, errorThrown){
console.log(jqXHR); console.log(jqXHR);

View File

@ -51,5 +51,5 @@
{% endblock main %} {% endblock main %}
{% block jquery %} {% block jquery %}
<script src="{% static 'collector/js/jq.upload.progress.js' %}"></script> <script type="module" src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
{% endblock jquery %} {% endblock jquery %}

View File

@ -0,0 +1,39 @@
<li class="nav-item col-lg-auto d-flex align-items-center">
<i
class="nav-link me-1 bi bi-sd-card"
aria-current="page"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="Storage used: {{ storage.used_percent }}%"
>
</i>
<div
class="progress"
role="progressbar"
aria-label="storage used"
aria-valuenow="25"
aria-valuemin="0"
aria-valuemax="100"
style="width: 125px"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="bottom"
data-bs-title="
Total: {{ storage.total|filesizeformat }}
<br>
Used: {{ storage.used|filesizeformat }}
<br>
Free: {{ storage.free|filesizeformat }}
"
>
<div
class="progress-bar
{% if storage.used_percent > 90 %} bg-danger
{% elif storage.used_percent > 80 %} bg-warning
{% else %} bg-success
{% endif %}"
style="width: {{ storage.used_percent }}%"
>
</div>
</div>
</li>

View File

@ -56,5 +56,5 @@
</div> </div>
{% endblock main %} {% endblock main %}
{% block jquery %} {% block jquery %}
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script> <script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
{% endblock jquery %} {% endblock jquery %}

View File

@ -86,9 +86,6 @@
{% include 'collector/includes/pagination.html' %} {% include 'collector/includes/pagination.html' %}
</div> </div>
{% endblock main %} {% endblock main %}
{% block bs %}
<script src="{% static 'collector/js/bs.tooltip.js' %}"></script>
{% endblock bs %}
{% block jquery %} {% block jquery %}
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script> <script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
{% endblock jquery %} {% endblock jquery %}

View File

@ -1,43 +0,0 @@
def logs_dir_path(instance, filename):
"""
file will be uploaded to
MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename>
"""
return f'{instance.ticket.number}/{filename}'
def sizify(value: int) -> str:
"""Simple kb/mb/gb size snippet for admin panel custom field:
Args:
value (int): size of file from Filefield
Returns:
str: format human readable size like 4.2 Gb
"""
if value < 512000:
value = value / 1024.0
ext = 'Kb'
elif value < 4194304000:
value = value / 1048576.0
ext = 'Mb'
else:
value = value / 1073741824.0
ext = 'Gb'
return f'{round(value, 2)} {ext}'
class PageTitleViewMixin:
title = 'Collector'
def get_title(self, *args, **kwargs):
"""
Return the class title attr by default,
but you can override this method to further customize
"""
return self.title
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.get_title()
return context

View File

@ -0,0 +1,38 @@
import shutil
def logs_dir_path(instance, filename):
"""
file will be uploaded to
MEDIA_ROOT/view/<filename>
"""
return f'{instance.ticket.number}/{filename}'
def sizify(value: int) -> str:
"""Simple kb/mb/gb size snippet for admin panel custom field:
Args:
value (int): size of file from Filefield
Returns:
str: format human readable size like 4.2 Gb
"""
if value < 512000:
value = value / 1024.0
ext = 'KB'
elif value < 4194304000:
value = value / 1048576.0
ext = 'MB'
else:
value = value / 1073741824.0
ext = 'GB'
return f'{round(value, 1)} {ext}'
def get_mount_fs_info(path):
mount_info = shutil.disk_usage(path)._asdict()
mount_info['used_percent'] = round(
mount_info['used'] / mount_info['total'] * 100
)
return {'storage': mount_info}

View File

@ -0,0 +1,23 @@
class ExtraContextMixin:
"""The class adds additional context
to all child view classes that inherit from it.
Overrides the get_context_data method for CBV
"""
title = 'Collector'
def get_title(self, *args, **kwargs):
"""
Return the class title attr by default,
but you can override this method to further customize
"""
return self.title
def get_context_data(self, **kwargs):
context = {}
try:
context = super().get_context_data(**kwargs)
except Exception:
pass
context['title'] = self.get_title()
return context

View File

@ -3,25 +3,22 @@ from django.http import FileResponse
from django.views import generic from django.views import generic
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render
from two_factor.views import OTPRequiredMixin from two_factor.views import OTPRequiredMixin
from .forms import TicketForm, ArchiveForm from .forms import TicketForm, ArchiveForm
from .models import Archive, Ticket from .models import Archive, Ticket
from .utils import PageTitleViewMixin from .utils.mixins import ExtraContextMixin
class ArchiveUploadView(PageTitleViewMixin, generic.View): class ArchiveUploadView(ExtraContextMixin, generic.TemplateView):
form_class = ArchiveForm() form_class = ArchiveForm()
template = 'collector/archive_upload.html', template_name = 'collector/archive_upload.html'
def get(self, request): def get_context_data(self, **kwargs):
return render( context = super().get_context_data(**kwargs)
request, context['form'] = self.form_class
self.template, return context
context={'form': self.form_class}
)
def get_title(self): def get_title(self):
return f'{self.title} - upload' return f'{self.title} - upload'
@ -41,7 +38,7 @@ class ArchiveHandlerView(
return FileResponse(self.object.file) return FileResponse(self.object.file)
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView):
model = Ticket model = Ticket
form_class = TicketForm form_class = TicketForm
template_name = 'collector/ticket_create.html' template_name = 'collector/ticket_create.html'
@ -54,7 +51,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
return super().form_valid(form) return super().form_valid(form)
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
model = Ticket model = Ticket
form_class = TicketForm form_class = TicketForm
template_name = 'collector/ticket_create.html' template_name = 'collector/ticket_create.html'
@ -69,7 +66,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
return super().form_valid(form) return super().form_valid(form)
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView):
model = Ticket model = Ticket
template_name = 'collector/tickets.html' template_name = 'collector/tickets.html'
context_object_name = 'tickets' context_object_name = 'tickets'
@ -98,7 +95,7 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
return super().get_queryset() return super().get_queryset()
class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): # noqa:E501 class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501
model = Ticket model = Ticket
template_name = 'collector/tickets.html' template_name = 'collector/tickets.html'
context_object_name = 'tickets' context_object_name = 'tickets'
@ -114,7 +111,7 @@ class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListVi
) )
class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): class DetailTicket(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
model = Ticket model = Ticket
template_name = 'collector/ticket.html' template_name = 'collector/ticket.html'
context_object_name = 'ticket' context_object_name = 'ticket'

View File

@ -0,0 +1,29 @@
"""
An application for uploading archives with log files
for their subsequent download and check issues
that have arisen with software products.
The purpose of creating this application is
the ability to securely exchange and store log files containing sensitive data.
I have not found an application that would allow an unauthorized client
to upload data without providing him with authorization credentials.
You can use other applications for this,
such as Google cloud, Yandex cloud, DropBox etc, but in this case,
you do not have a tool that would allow you to automatically restrict uploads
later until you explicitly deny access to the shared link.
This app allows you to upload files using a unique token
associated with a support ticket.
This token has a limit on the number of file upload attempts.
Also, if the ticket is resolved, then the token is invalid.
"""
# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀
# █░▀░█ ██▄ ░█░ █▀█ ▄
# -------------------
__author__ = "MOIS3Y"
__credits__ = ["Stepan Zhukovsky"]
__license__ = "GPL v3.0"
__version__ = "0.1.0"
__maintainer__ = "Stepan Zhukovsky"
__email__ = "stepan@zhukovsky.me"
__status__ = "Development"

View File

@ -2,6 +2,7 @@ import environ
from pathlib import Path from pathlib import Path
from datetime import timedelta from datetime import timedelta
from . import __version__
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -9,6 +10,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Set default environ variables: # Set default environ variables:
env = environ.Env( env = environ.Env(
# set casting default value # set casting default value
VERSION=(str, __version__),
ENVIRONMENT=(str, 'development'),
DEBUG=(bool, False), DEBUG=(bool, False),
SECRET_KEY=(str, 'j9QGbvM9Z4otb47'), SECRET_KEY=(str, 'j9QGbvM9Z4otb47'),
SQLITE_URL=(str, f'sqlite:///{BASE_DIR / "data/db.sqlite3"}'), SQLITE_URL=(str, f'sqlite:///{BASE_DIR / "data/db.sqlite3"}'),
@ -20,6 +23,9 @@ env = environ.Env(
# Read .env file if exist: # Read .env file if exist:
environ.Env.read_env(BASE_DIR / '.env') environ.Env.read_env(BASE_DIR / '.env')
VERSION = env('VERSION')
ENVIRONMENT = env('ENVIRONMENT')
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY') SECRET_KEY = env('SECRET_KEY')
@ -78,10 +84,14 @@ TEMPLATES = [
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
# default:
'django.template.context_processors.debug', 'django.template.context_processors.debug',
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
# collector:
'collector.context_processors.metadata',
'collector.context_processors.storage_info',
], ],
}, },
}, },
@ -174,6 +184,7 @@ REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501 # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501
# 'PAGE_SIZE': 3, # 'PAGE_SIZE': 3,
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
} }
if DEBUG: if DEBUG:

View File

@ -40,9 +40,12 @@
<body> <body>
{% block collector_content %}{% endblock collector_content %} {% block collector_content %}{% endblock collector_content %}
{% block account_content %}{% endblock account_content %} {% block account_content %}{% endblock account_content %}
<!-- BS dependences JS-->
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script> <script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
<!-- Theme switcher JS-->
<script src="{% static '/js/bs.theme.mode.js' %}"></script> <script src="{% static '/js/bs.theme.mode.js' %}"></script>
<!-- BS tooltip JS-->
<script src="{% static '/js/bs.tooltip.js' %}"></script>
{% block collector_scripts %}{% endblock collector_scripts %} {% block collector_scripts %}{% endblock collector_scripts %}
{% block account_scripts %}{% endblock account_scripts %} {% block account_scripts %}{% endblock account_scripts %}
</body> </body>

View File

@ -0,0 +1,28 @@
<a
class="navbar-brand"
href="{% url 'collector:index' %}"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="bottom"
data-bs-title="
Version: {{ version }}
<br>
{% if environment != production %}
Staging: {{ environment|capfirst }}
{% endif %}
"
>
Logs Collector
<i class="bi bi-file-earmark-zip-fill"></i>
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="collapse nav fields"
>
<span class="navbar-toggler-icon"></span>
</button>

View File

@ -0,0 +1,61 @@
{% if request.user.is_authenticated %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary">
<i class="bi bi-person-square"></i> {{ request.user }}
</button>
<button
type="button"
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if request.user.is_staff %}
<li>
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
><i class="bi bi-shield-shaded"></i> Admin</a>
</li>
{% endif %}
<li>
<a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}" target="_blank"
><i class="bi bi-braces-asterisk"></i> Swagger</a>
</li>
<li>
<a class="dropdown-item" type="button" href="{% url 'redoc' %}" target="_blank"
><i class="bi bi-file-earmark-medical"></i> Redoc</a>
</li>
<li>
<a
href="{% url 'two_factor:profile' %}"
class="dropdown-item"
type="button">
<i class="bi bi-gear"></i> Settings
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a
href="{% url 'account:logout' %}"
class="dropdown-item"
type="button"><i class="bi bi-door-closed"></i> Logout
</a>
</li>
</ul>
</div>
</li>
{% else %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<a
type="button"
href="{% url 'two_factor:login' %}"
class="btn btn-outline-secondary"
>
<i class="bi bi-box-arrow-in-right"></i></i> Login
</a>
</div>
</li>
{% endif %}

View File

@ -0,0 +1,47 @@
{% load collector_extras %}
{% get_platforms as platforms %}
<li class="nav-item dropdown">
<button
class="nav-link dropdown-toggle"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
><i class="bi bi-filter-circle"></i> Tickets
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" aria-current="page" href="{% url 'collector:create' %}">
<i class="bi bi-pencil-square"></i> Create ticket
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<a class="dropdown-item" aria-current="page" href="{% url 'collector:upload' %}">
<i class="bi bi-archive"></i>
Upload archive
</a>
<li><hr class="dropdown-divider" /></li>
{% for platform in platforms %}
<li>
<a
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
href="{{ platform.get_absolute_url }}"
>{{ platform.pretty_name}}
</a>
</li>
{% endfor %}
<li><hr class="dropdown-divider" /></li>
<li>
<a
class="dropdown-item {% if request.GET.resolved %}active{% endif %}"
href="{% url 'collector:tickets' %}?resolved=true">
<i class="bi bi-check-circle"></i> Resolved
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
<i class="bi bi-funnel"></i> Reset filter
</a>
</li>
</ul>
</li>

View File

@ -1,157 +1,22 @@
{% load collector_extras %}
{% get_platforms as platforms %}
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container"> <div class="container">
<a class="navbar-brand" href="{% url 'collector:index' %}"> <!--Brand logo -->
Logs Collector {% include 'includes/brand.html' %}
<i class="bi bi-file-earmark-zip-fill"></i> <!-- Left fields -->
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Переключатель навигации"
>
<span class="navbar-toggler-icon"></span>
</button>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto"> <ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
<li class="nav-item dropdown"> <!-- Menu -->
<button {% include 'includes/menu.html' %}
class="nav-link dropdown-toggle" <!-- Storage -->
role="button" {% include 'includes/storage.html' %}
data-bs-toggle="dropdown"
aria-expanded="false"
><i class="bi bi-filter-circle"></i> Filters
</button>
<ul class="dropdown-menu">
{% for platform in platforms %}
<li>
<a
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
href="{{ platform.get_absolute_url }}"
>{{ platform.pretty_name}}
</a>
</li>
{% endfor %}
<li><hr class="dropdown-divider" /></li>
<li>
<a
class="dropdown-item {% if request.GET.resolved %}active{% endif %}"
href="{% url 'collector:tickets' %}?resolved=true">
<i class="bi bi-check-circle"></i> Resolved
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
<i class="bi bi-funnel"></i> Reset filter
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'collector:create' %}">
<i class="bi bi-pencil-square"></i> Create
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{% url 'collector:upload' %}">
<i class="bi bi-archive"></i>
Upload
</a>
</li>
</ul> </ul>
<!-- Search --> <!-- Search -->
<ul class="navbar-nav flex-row flex-wrap me-md-auto"> {% include 'includes/search.html' %}
<li class="nav-item py-2 col-12 col-lg-auto">
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
name="search"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="Type the ticket number or comma-separated numbers"
/>
<button
class="btn btn-outline-success"
type="submit">
<i class="bi bi-search"></i>
</button>
</form>
</li>
</ul>
<!-- Right fields --> <!-- Right fields -->
<ul class="navbar-nav flex-row flex-wrap ms-md-auto"> <ul class="navbar-nav flex-row flex-wrap ms-md-auto">
<!-- User settings --> <!-- User settings -->
{% if request.user.is_authenticated %} {% include 'includes/extra_menu.html' %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary">
<i class="bi bi-person-square"></i> {{ request.user }}
</button>
<button
type="button"
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if request.user.is_staff %}
<li>
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
><i class="bi bi-shield-shaded"></i> Admin</a>
</li>
{% endif %}
<li>
<a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}"
><i class="bi bi-braces-asterisk"></i> Swagger</a>
</li>
<li>
<a class="dropdown-item" type="button" href="{% url 'redoc' %}"
><i class="bi bi-file-earmark-medical"></i> Redoc</a>
</li>
<li>
<a
href="{% url 'two_factor:profile' %}"
class="dropdown-item"
type="button">
<i class="bi bi-gear"></i> Settings
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<a
href="{% url 'account:logout' %}"
class="dropdown-item"
type="button"><i class="bi bi-door-closed"></i> Logout
</a>
</li>
</ul>
</div>
</li>
{% else %}
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="btn-group">
<a
type="button"
href="{% url 'two_factor:login' %}"
class="btn btn-outline-secondary"
>
<i class="bi bi-box-arrow-in-right"></i></i> Login
</a>
</div>
</li>
{% endif %}
<!-- Separator --> <!-- Separator -->
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto"> <li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div> <div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>

View File

@ -0,0 +1,21 @@
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
<li class="nav-item py-2 col-12 col-lg-auto">
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
<input
class="form-control me-2"
type="search"
placeholder="Search"
aria-label="Search"
name="search"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="Type the ticket number or comma-separated numbers"
/>
<button
class="btn btn-outline-success"
type="submit">
<i class="bi bi-search"></i>
</button>
</form>
</li>
</ul>

View File

@ -0,0 +1,45 @@
{% if storage %}
<li class="nav-item col-lg-auto d-flex align-items-center">
<i
id="storage_icon"
class="nav-link me-1 bi bi-sd-card"
aria-current="page"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="Storage used: {{ storage.used_percent }}%"
>
</i>
<div
id="storage_progress_container"
class="progress"
role="progressbar"
aria-label="storage used"
aria-valuenow="25"
aria-valuemin="0"
aria-valuemax="100"
style="width: 125px"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="bottom"
data-bs-title="
Total: {{ storage.total|filesizeformat }}
<br>
Used: {{ storage.used|filesizeformat }}
<br>
Free: {{ storage.free|filesizeformat }}
"
>
<div
id="storage_progress"
class="progress-bar
{% if storage.used_percent > 90 %} bg-danger
{% elif storage.used_percent > 80 %} bg-warning
{% else %} bg-success
{% endif %}"
style="width: {{ storage.used_percent }}%"
storage-url="{% url 'collector_api:storage-info' %}"
>
</div>
</div>
</li>
{% endif %}