mirror of
https://github.com/MOIS3Y/logs-collector.git
synced 2025-02-01 01:10:52 +01:00
Add: storage info widget and storage api endpoint refactoring project structure add version app
This commit is contained in:
parent
e95de1b553
commit
016994d594
@ -5,7 +5,7 @@ from django.utils.html import format_html
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from .models import Platform, Archive, Ticket
|
||||
from .utils import sizify
|
||||
from .utils.helpers import sizify
|
||||
|
||||
|
||||
class PlatformAdmin(admin.ModelAdmin):
|
||||
|
@ -18,4 +18,5 @@ router.register(r'tickets', views.TicketViewSet)
|
||||
urlpatterns = [
|
||||
# CRUD:
|
||||
path('v1/', include(router.urls)),
|
||||
path('v1/storage/', views.StorageInfo.as_view(), name='storage-info'),
|
||||
]
|
||||
|
@ -1,4 +1,5 @@
|
||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import status
|
||||
# from rest_framework.decorators import action
|
||||
@ -10,6 +11,7 @@ from rest_framework.parsers import (
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from rest_framework import views
|
||||
from rest_framework import filters
|
||||
|
||||
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 collector.models import Archive, Ticket, Platform
|
||||
from collector.utils.helpers import get_mount_fs_info
|
||||
|
||||
from .filters import ArchiveFilter, TicketFilter
|
||||
from .permissions import IsGuestUpload
|
||||
@ -122,3 +125,10 @@ class TicketViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
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))
|
||||
|
14
logs_collector/collector/context_processors.py
Normal file
14
logs_collector/collector/context_processors.py
Normal 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)
|
@ -47,7 +47,7 @@ class Migration(migrations.Migration):
|
||||
name='Archive',
|
||||
fields=[
|
||||
('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)),
|
||||
('md5', models.CharField(editable=False, max_length=1024)),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
|
@ -6,7 +6,7 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from .utils import logs_dir_path
|
||||
from .utils.helpers import logs_dir_path
|
||||
|
||||
|
||||
class Archive(models.Model):
|
||||
|
69
logs_collector/collector/static/collector/js/helpers.js
Normal file
69
logs_collector/collector/static/collector/js/helpers.js
Normal 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};
|
@ -1,3 +1,6 @@
|
||||
import {updateStorageInfo} from "./helpers.js";
|
||||
|
||||
|
||||
$(function () {
|
||||
console.log("JQ is ready to work");
|
||||
|
||||
@ -24,8 +27,11 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
$(archiveListElement).hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
@ -57,7 +63,7 @@ $(function () {
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status)
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(data)
|
||||
console.log(jqXHR.status)
|
||||
}
|
||||
@ -82,11 +88,14 @@ $(function () {
|
||||
console.log(jqXHR.status);
|
||||
if (delDiv.length) {
|
||||
delDiv.hide(1500);
|
||||
setTimeout(() => {
|
||||
updateStorageInfo();
|
||||
}, 3000);
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import {updateStorageInfo} from "./helpers.js";
|
||||
|
||||
$(function () {
|
||||
const uploadForm = document.getElementById('upload_form');
|
||||
const input_file = document.getElementById('id_file');
|
||||
@ -6,7 +8,7 @@ $(function () {
|
||||
|
||||
$("#upload_form").submit(function(e){
|
||||
e.preventDefault();
|
||||
$form = $(this)
|
||||
// $form = $(this)
|
||||
let formData = new FormData(this);
|
||||
let upload_token = formData.get("token")
|
||||
const media_data = input_file.files[0];
|
||||
@ -51,6 +53,11 @@ $(function () {
|
||||
].join('')
|
||||
uploadForm.reset()
|
||||
progress_bar.classList.add('not-visible')
|
||||
try {
|
||||
updateStorageInfo();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
};
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown){
|
||||
console.log(jqXHR);
|
||||
|
@ -51,5 +51,5 @@
|
||||
{% endblock main %}
|
||||
|
||||
{% 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 %}
|
||||
|
39
logs_collector/collector/templates/collector/storage.html
Normal file
39
logs_collector/collector/templates/collector/storage.html
Normal 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>
|
@ -56,5 +56,5 @@
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% 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 %}
|
||||
|
@ -86,9 +86,6 @@
|
||||
{% include 'collector/includes/pagination.html' %}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
{% block bs %}
|
||||
<script src="{% static 'collector/js/bs.tooltip.js' %}"></script>
|
||||
{% endblock bs %}
|
||||
{% 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 %}
|
||||
|
@ -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
|
0
logs_collector/collector/utils/__init__.py
Normal file
0
logs_collector/collector/utils/__init__.py
Normal file
38
logs_collector/collector/utils/helpers.py
Normal file
38
logs_collector/collector/utils/helpers.py
Normal 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}
|
23
logs_collector/collector/utils/mixins.py
Normal file
23
logs_collector/collector/utils/mixins.py
Normal 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
|
@ -3,25 +3,22 @@ from django.http import FileResponse
|
||||
from django.views import generic
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from .forms import TicketForm, ArchiveForm
|
||||
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()
|
||||
template = 'collector/archive_upload.html',
|
||||
template_name = 'collector/archive_upload.html'
|
||||
|
||||
def get(self, request):
|
||||
return render(
|
||||
request,
|
||||
self.template,
|
||||
context={'form': self.form_class}
|
||||
)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'] = self.form_class
|
||||
return context
|
||||
|
||||
def get_title(self):
|
||||
return f'{self.title} - upload'
|
||||
@ -41,7 +38,7 @@ class ArchiveHandlerView(
|
||||
return FileResponse(self.object.file)
|
||||
|
||||
|
||||
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@ -54,7 +51,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||
model = Ticket
|
||||
form_class = TicketForm
|
||||
template_name = 'collector/ticket_create.html'
|
||||
@ -69,7 +66,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@ -98,7 +95,7 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): # noqa:E501
|
||||
class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
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
|
||||
template_name = 'collector/ticket.html'
|
||||
context_object_name = 'ticket'
|
||||
|
@ -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"
|
@ -2,6 +2,7 @@ import environ
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
|
||||
from . import __version__
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@ -9,6 +10,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
# Set default environ variables:
|
||||
env = environ.Env(
|
||||
# set casting default value
|
||||
VERSION=(str, __version__),
|
||||
ENVIRONMENT=(str, 'development'),
|
||||
DEBUG=(bool, False),
|
||||
SECRET_KEY=(str, 'j9QGbvM9Z4otb47'),
|
||||
SQLITE_URL=(str, f'sqlite:///{BASE_DIR / "data/db.sqlite3"}'),
|
||||
@ -20,6 +23,9 @@ env = environ.Env(
|
||||
# Read .env file if exist:
|
||||
environ.Env.read_env(BASE_DIR / '.env')
|
||||
|
||||
VERSION = env('VERSION')
|
||||
ENVIRONMENT = env('ENVIRONMENT')
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env('SECRET_KEY')
|
||||
|
||||
@ -78,10 +84,14 @@ TEMPLATES = [
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
# default:
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'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_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501
|
||||
# 'PAGE_SIZE': 3,
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@ -40,9 +40,12 @@
|
||||
<body>
|
||||
{% block collector_content %}{% endblock collector_content %}
|
||||
{% block account_content %}{% endblock account_content %}
|
||||
|
||||
<!-- BS dependences JS-->
|
||||
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<!-- Theme switcher JS-->
|
||||
<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 account_scripts %}{% endblock account_scripts %}
|
||||
</body>
|
||||
|
28
logs_collector/templates/includes/brand.html
Normal file
28
logs_collector/templates/includes/brand.html
Normal 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>
|
61
logs_collector/templates/includes/extra_menu.html
Normal file
61
logs_collector/templates/includes/extra_menu.html
Normal 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 %}
|
47
logs_collector/templates/includes/menu.html
Normal file
47
logs_collector/templates/includes/menu.html
Normal 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>
|
@ -1,157 +1,22 @@
|
||||
{% load collector_extras %}
|
||||
{% get_platforms as platforms %}
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'collector:index' %}">
|
||||
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="Переключатель навигации"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<!--Brand logo -->
|
||||
{% include 'includes/brand.html' %}
|
||||
<!-- Left fields -->
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
|
||||
<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> 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>
|
||||
<!-- Menu -->
|
||||
{% include 'includes/menu.html' %}
|
||||
<!-- Storage -->
|
||||
{% include 'includes/storage.html' %}
|
||||
</ul>
|
||||
<!-- Search -->
|
||||
<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>
|
||||
{% include 'includes/search.html' %}
|
||||
<!-- Right fields -->
|
||||
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
||||
<!-- User settings -->
|
||||
{% 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' %}"
|
||||
><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 %}
|
||||
{% include 'includes/extra_menu.html' %}
|
||||
<!-- Separator -->
|
||||
<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>
|
||||
|
21
logs_collector/templates/includes/search.html
Normal file
21
logs_collector/templates/includes/search.html
Normal 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>
|
45
logs_collector/templates/includes/storage.html
Normal file
45
logs_collector/templates/includes/storage.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user