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 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):
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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))
|
||||||
|
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',
|
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)),
|
||||||
|
@ -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):
|
||||||
|
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 () {
|
$(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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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 %}
|
||||||
|
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>
|
</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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 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'
|
||||||
|
@ -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 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:
|
||||||
|
@ -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>
|
||||||
|
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">
|
<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>
|
||||||
|
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