add important links section
This commit is contained in:
parent
eba01c013a
commit
cb623ca83b
6 changed files with 158 additions and 41 deletions
|
@ -1,11 +1,16 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from ordered_model.admin import OrderedModelAdmin
|
from ordered_model.admin import OrderedModelAdmin
|
||||||
|
|
||||||
from sdbs_infra.dashboard.models import Service
|
from sdbs_infra.dashboard.models import Service, Link
|
||||||
|
|
||||||
|
|
||||||
|
class LinkAdmin(OrderedModelAdmin):
|
||||||
|
list_display = ('short_name', 'url', 'move_up_down_links')
|
||||||
|
|
||||||
|
|
||||||
class ServiceAdmin(OrderedModelAdmin):
|
class ServiceAdmin(OrderedModelAdmin):
|
||||||
list_display = ('short_name', 'url', 'move_up_down_links')
|
list_display = ('short_name', 'url', 'move_up_down_links')
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Link, LinkAdmin)
|
||||||
admin.site.register(Service, ServiceAdmin)
|
admin.site.register(Service, ServiceAdmin)
|
||||||
|
|
28
sdbs_infra/dashboard/migrations/0007_link.py
Normal file
28
sdbs_infra/dashboard/migrations/0007_link.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-06-20 17:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dashboard', '0006_service_port'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Link',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')),
|
||||||
|
('short_name', models.CharField(max_length=64)),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='links')),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('url', models.URLField()),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Important Link',
|
||||||
|
'verbose_name_plural': 'Important Links',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,5 @@
|
||||||
import socket
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from ordered_model.models import OrderedModel
|
from ordered_model.models import OrderedModel
|
||||||
|
|
||||||
|
@ -21,3 +19,17 @@ class Service(OrderedModel):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.short_name} ({self.url})"
|
return f"{self.short_name} ({self.url})"
|
||||||
|
|
||||||
|
|
||||||
|
class Link(OrderedModel):
|
||||||
|
short_name = models.CharField(null=False, max_length=64)
|
||||||
|
image = models.ImageField(null=True, blank=True, upload_to='links')
|
||||||
|
description = models.TextField(null=True, blank=True)
|
||||||
|
url = models.URLField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.short_name} ({self.url})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Important Link"
|
||||||
|
verbose_name_plural = "Important Links"
|
||||||
|
|
|
@ -25,10 +25,17 @@ a {
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
h1 {
|
||||||
|
margin: 1rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 2rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -36,7 +43,7 @@ main {
|
||||||
margin-bottom: -1rem;
|
margin-bottom: -1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service {
|
.box {
|
||||||
flex-basis: 20%;
|
flex-basis: 20%;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -45,44 +52,63 @@ main {
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 1000px) {
|
@media screen and (max-width: 1000px) {
|
||||||
.service {
|
.box {
|
||||||
flex-basis: 40%;
|
flex-basis: 40%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.service {
|
.box {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-content {
|
.box-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-content img {
|
.link .box-content {
|
||||||
flex-grow: 1;
|
flex-direction: row;
|
||||||
min-width: 50%;
|
justify-content: space-evenly;
|
||||||
max-width: 100%;
|
}
|
||||||
|
|
||||||
|
.service .box-content {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-content img {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service .label h3 {
|
.service img {
|
||||||
|
min-width: 50%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link img {
|
||||||
|
min-height: 1rem;
|
||||||
|
max-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .label h3 {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service .label .description {
|
.link .label h3 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .label .description {
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
|
|
|
@ -10,12 +10,30 @@
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>/-\ infrastructure</h1>
|
<h1>/-\ infrastructure</h1>
|
||||||
<h2>status page (internal services)</h2>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<h2>important links</h2>
|
||||||
|
<section class="links boxes">
|
||||||
|
{% for link in links %}
|
||||||
|
<a class="link box" href="{{ link.url }}">
|
||||||
|
<section class="box-content">
|
||||||
|
{% if link.image_url %}
|
||||||
|
<img src="{{ link.image_url }}" alt="image for {{ link.short_name }}"/>
|
||||||
|
{% endif %}
|
||||||
|
<div class="label">
|
||||||
|
<h3>{{ link.short_name }}</h3>
|
||||||
|
{% if link.description %}
|
||||||
|
<p class="description">{{ link.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
|
<h2>internal services</h2>
|
||||||
|
<section class="services boxes">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
<a class="service status-{{ service.status }}" href="{{ service.url }}">
|
<a class="service box status-{{ service.status }}" href="{{ service.url }}">
|
||||||
<section class="service-content">
|
<section class="box-content">
|
||||||
{% if service.image_url %}
|
{% if service.image_url %}
|
||||||
<img src="{{ service.image_url }}" alt="image for {{ service.short_name }}"/>
|
<img src="{{ service.image_url }}" alt="image for {{ service.short_name }}"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -31,7 +49,7 @@
|
||||||
</section>
|
</section>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</main>
|
</section>
|
||||||
<section class="stats">
|
<section class="stats">
|
||||||
VPS STATS — {{ vps_stats|safe }}
|
VPS STATS — {{ vps_stats|safe }}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -9,20 +9,45 @@ from bs4 import BeautifulSoup
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from humanize import naturalsize
|
from humanize import naturalsize
|
||||||
|
|
||||||
from sdbs_infra.dashboard.models import Service, ServiceStatus
|
from sdbs_infra.dashboard.models import Service, ServiceStatus, Link
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
template_name = "index.html"
|
template_name = "index.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'links': asyncio.run(self.process_links(list(Link.objects.all()))),
|
||||||
'services': asyncio.run(self.process_services(list(Service.objects.all()))),
|
'services': asyncio.run(self.process_services(list(Service.objects.all()))),
|
||||||
'vps_stats': self.vps_stats()
|
'vps_stats': self.vps_stats()
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
async def process_links(self, links):
|
||||||
async def process_services(services):
|
result = []
|
||||||
|
|
||||||
|
session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1))
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
index_text = None, None
|
||||||
|
if not link.image:
|
||||||
|
try:
|
||||||
|
async with session.get(link.url) as response:
|
||||||
|
index_status, index_text = response.status, await response.text()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
image = link.image.url if link.image else self.extract_favicon(link.url, index_text)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'image_url': image,
|
||||||
|
**vars(link)
|
||||||
|
})
|
||||||
|
|
||||||
|
await session.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def process_services(self, services):
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1))
|
session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1))
|
||||||
|
@ -49,20 +74,7 @@ class IndexView(TemplateView):
|
||||||
else:
|
else:
|
||||||
status = ServiceStatus.UNKNOWN
|
status = ServiceStatus.UNKNOWN
|
||||||
|
|
||||||
image = None
|
image = service.image.url if service.image else self.extract_favicon(service.url, index_text)
|
||||||
if service.image:
|
|
||||||
image = service.image.url
|
|
||||||
elif index_text:
|
|
||||||
parsed_html = BeautifulSoup(index_text, features="html.parser")
|
|
||||||
link_tags = parsed_html.find_all('link')
|
|
||||||
for rel in ['apple-touch-icon', 'shortcut', 'icon']:
|
|
||||||
for link_tag in link_tags:
|
|
||||||
if rel in link_tag.attrs['rel']:
|
|
||||||
link = link_tag.attrs['href']
|
|
||||||
if service.url not in link:
|
|
||||||
image = service.url + (link if link.startswith("/") else f"/{link}")
|
|
||||||
else:
|
|
||||||
image = link
|
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
'status': status.value,
|
'status': status.value,
|
||||||
|
@ -71,9 +83,25 @@ class IndexView(TemplateView):
|
||||||
})
|
})
|
||||||
|
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_favicon(url, index_text):
|
||||||
|
if not index_text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed_html = BeautifulSoup(index_text, features="html.parser")
|
||||||
|
link_tags = parsed_html.find_all('link')
|
||||||
|
for rel in ['apple-touch-icon', 'shortcut', 'icon']:
|
||||||
|
for link_tag in link_tags:
|
||||||
|
if rel in link_tag.attrs['rel']:
|
||||||
|
href = link_tag.attrs['href']
|
||||||
|
if url not in href:
|
||||||
|
image = url + (href if href.startswith("/") else f"/{href}")
|
||||||
|
else:
|
||||||
|
image = href
|
||||||
|
return image
|
||||||
|
|
||||||
# noinspection PyListCreation
|
# noinspection PyListCreation
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def vps_stats():
|
def vps_stats():
|
||||||
|
|
Loading…
Reference in a new issue