Compare commits

..

5 Commits

Author SHA1 Message Date
R. Miles McCain
c718d49c54 Use Alpine 3.14 2021-12-31 13:26:44 -05:00
R. Miles McCain
04f095b4fb Use new a17t and Tailwind 2021-12-31 13:25:10 -05:00
R. Miles McCain
fc38fd88bd Use new a17t/Tailwind 2021-12-31 13:02:16 -05:00
R. Miles McCain
7d92a557f4 Use latest alpine 2021-12-31 12:10:19 -05:00
R. Miles McCain
e12848b094 Lessen priorities on field buttons 2021-12-21 00:58:11 -05:00
55 changed files with 2065 additions and 334 deletions

View File

@@ -41,6 +41,10 @@ RUN apk --purge del .build-deps && \
# Install Shynet
COPY shynet .
# Build Tailwind CSS and build static files
COPY tailwind.config.js .
RUN npx tailwindcss -i ./a17t/static/a17t/css/tailwind.css -o ./a17t/static/a17t/dist/tailwind.css --jit --minify
RUN python manage.py collectstatic --noinput && \
python manage.py compilemessages

View File

@@ -208,22 +208,6 @@ In a single-page application, the page never reloads. (That's the entire point o
Fortunately, Shynet offers a simple method you can call from anywhere within your JavaScript to indicate that a new page has been loaded: `Shynet.newPageLoad()`. Add this method call to the code that handles routing in your app, and you'll be ready to go.
### API
All the information displayed on the dashboard can be obtained via API on url ```//shynet.example.com/api/v1/dashboard/```. By default this endpoint will return the full data from all services over the last last 30 days. The `Authentication` header should be set to use user's parsonal API token (```'Authorization: Token <user API token>'```).
There are 3 optional query parameters:
* `uuid` - to get data only from one service
* `startDate` - to set start date in format YYYY-MM-DD
* `endDate` - to set end date in format YYYY-MM-DD
Example in HTTPie:
```http get '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01' 'Authorization:Token {{user_api_token}}'```
Example in cURL:
```curl -H 'Authorization:Token {{user_api_token}}' '//shynet.example.com/api/v1/dashboard/?uuid={{service_uuid}}&startDate=2021-01-01&endDate=2050-01-01'```
---
## Troubleshooting

46
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,46 @@
version: '3'
services:
shynet:
container_name: shynet_main
build: .
restart: unless-stopped
expose:
- 8080
env_file:
# Create a file called '.env' if it doesn't already exist.
# You can use `TEMPLATE.env` as a guide.
- .env
environment:
- DB_HOST=db
networks:
- internal
depends_on:
- db
db:
container_name: shynet_database
image: postgres
restart: always
environment:
- "POSTGRES_USER=${DB_USER}"
- "POSTGRES_PASSWORD=${DB_PASSWORD}"
- "POSTGRES_DB=${DB_NAME}"
volumes:
- shynet_db:/var/lib/postgresql/data
networks:
- internal
webserver:
container_name: shynet_webserver
image: nginx
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
depends_on:
- shynet
networks:
- internal
volumes:
shynet_db:
networks:
internal:

View File

@@ -17,7 +17,7 @@ spec:
spec:
containers:
- name: "shynet-webserver"
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
image: "milesmcc/shynet:latest"
imagePullPolicy: Always
envFrom:
- secretRef:
@@ -42,7 +42,7 @@ spec:
spec:
containers:
- name: "shynet-celeryworker"
image: "milesmcc/shynet:edge" # Change to the version appropriate for you (e.g., :latest)
image: "milesmcc/shynet:latest"
command: ["./celeryworker.sh"]
imagePullPolicy: Always
envFrom:
@@ -95,7 +95,7 @@ spec:
selector:
app: shynet-webserver
---
apiVersion: networking.k8s.io/v1
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shynet-webserver-ingress

1885
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,8 @@
"homepage": "https://github.com/milesmcc/shynet#readme",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"a17t": "^0.5.1",
"a17t": "^0.10.1",
"tailwindcss": "^3.0.1",
"apexcharts": "^3.24.0",
"datamaps": "^0.5.9",
"flag-icon-css": "^3.5.0",

17
poetry.lock generated
View File

@@ -300,17 +300,6 @@ python3-openid = ">=3.0.8"
requests = "*"
requests-oauthlib = ">=0.3.0"
[[package]]
name = "django-cors-headers"
version = "3.11.0"
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
Django = ">=2.2"
[[package]]
name = "django-coverage-plugin"
version = "2.0.2"
@@ -960,7 +949,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "9fa33a531809239cfa88d8d896484af7d8be765b2a0f4878ab5434d5af6b30d2"
content-hash = "009655b041e17f83ac3f1b423241ee299b0e706274b5b77519d3c087a4a032f8"
[metadata.files]
aiohttp = [
@@ -1231,10 +1220,6 @@ django = [
django-allauth = [
{file = "django-allauth-0.45.0.tar.gz", hash = "sha256:6d46be0e1480316ccd45476db3aefb39db70e038d2a543112d314b76bb999a4e"},
]
django-cors-headers = [
{file = "django-cors-headers-3.11.0.tar.gz", hash = "sha256:eb98389bf7a2afc5d374806af4a9149697e3a6955b5a2dc2bf049f7d33647456"},
{file = "django_cors_headers-3.11.0-py3-none-any.whl", hash = "sha256:a22be2befd4069c4fc174f11cf067351df5c061a3a5f94a01650b4e928b0372b"},
]
django-coverage-plugin = [
{file = "django_coverage_plugin-2.0.2-py3-none-any.whl", hash = "sha256:4206c85ffba0301f83aecc38e5b01b1b9a4b45a545d9456a827e3fabea18d952"},
{file = "django_coverage_plugin-2.0.2.tar.gz", hash = "sha256:e91e3a0c8de2b3766a144cdd30dbbf7a79e5c532a5dcc1373ce7eaad83b358b3"},

View File

@@ -26,7 +26,6 @@ django-health-check = "^3.16.4"
django-npm = "^1.0.0"
python-dotenv = "^0.18.0"
django-debug-toolbar = "^3.2.1"
django-cors-headers = "^3.11.0"
[tool.poetry.dev-dependencies]
pytest-sugar = "^0.9.4"

View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.litepicker .container__main {
@apply card p-2;
}
}

View File

@@ -28,10 +28,10 @@
{% endfor %}
{% elif field|is_input %}
{% include 'a17t/includes/label.html' %}
{{field|add_class:"input my-1"}}
{{field|add_class:"input ~neutral my-1"}}
{% elif field|is_textarea %}
{% include 'a17t/includes/label.html' %}
{{ field|add_class:'textarea my-1' }}
{{ field|add_class:'textarea ~neutral my-1' }}
{% elif field|is_select %}
{% include 'a17t/includes/label.html' %}
<div class="select {% if field.errors|length > 0 %}~critical{% endif %} my-1">
@@ -39,7 +39,7 @@
</div>
{% else %}
{% include 'a17t/includes/label.html' %}
{{field|add_class:"field my-1"}}
{{field|add_class:"field ~neutral my-1"}}
{% endif %}
{% for error in field.errors %}

View File

@@ -1,6 +1,5 @@
{% load static %}
<link rel="stylesheet" href="{% static 'a17t/dist/a17t.css' %}">
<script async src="{% static '@fortawesome/fontawesome-free/js/all.min.js' %}" data-mutate-approach="sync"></script>
<link href="{% static 'a17t/dist/tailwind.css' %}" rel="stylesheet">
<link href="{% static 'inter-ui/Inter (web)/inter.css' %}" rel="stylesheet">

View File

@@ -1,23 +1,23 @@
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
<div class="w-full md:w-auto mb-2">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto mr-1">Previous</a>
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button ~neutral @low w-auto mr-1">Previous</a>
{% else %}
<a class="button field !low bg-neutral-000 w-auto mr-1" disabled>Previous</a>
<a class="button ~neutral @low w-auto mr-1" disabled>Previous</a>
{% endif %}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto">Next</a>
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button ~neutral @low w-auto">Next</a>
{% else %}
<a class="button field !low bg-neutral-000 w-auto" disabled>Next</a>
<a class="button ~neutral @low w-auto" disabled>Next</a>
{% endif %}
</div>
<ul class="pagination-list w-full md:w-auto mb-2 flex">
{% for pnum in begin %}
{% ifequal page.number pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
<li><a class="button ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
@@ -25,9 +25,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %}
{% ifequal page.number pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
<li><a class="button ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}
@@ -36,9 +36,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %}
{% ifequal page.number pnum %}
<li><a class="button field !low w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
<li><a class="button ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field !low bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}

View File

View File

@@ -1 +0,0 @@
# from django.contrib import admin

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'

View File

@@ -1,23 +0,0 @@
from django.http import JsonResponse
from django.contrib.auth.models import AnonymousUser
from core.models import User
class ApiTokenRequiredMixin:
def _get_user_by_token(self, request):
token = request.headers.get('Authorization')
if not token or not token.startswith('Token '):
return AnonymousUser()
token = token.split(' ')[1]
user = User.objects.filter(api_token=token).first()
return user if user else AnonymousUser()
def dispatch(self, request, *args, **kwargs):
request.user = self._get_user_by_token(request)
if not request.user.is_authenticated:
return JsonResponse(data={}, status=403)
return super().dispatch(request, *args, **kwargs)

View File

@@ -1 +0,0 @@
# from django.db import models

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,7 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path("dashboard/", views.DashboardApiView.as_view(), name="services"),
]

View File

@@ -1,60 +0,0 @@
import uuid
from django.http import JsonResponse
from django.db.models import Q
from django.db.models.query import QuerySet
from django.views.generic import View
from dashboard.mixins import DateRangeMixin
from core.models import Service
from .mixins import ApiTokenRequiredMixin
def is_valid_uuid(value):
try:
uuid.UUID(value)
return True
except ValueError:
return False
class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View):
def get(self, request, *args, **kwargs):
services = Service.objects.filter(
Q(owner=request.user) | Q(collaborators__in=[request.user])
).distinct()
uuid = request.GET.get('uuid')
if uuid and is_valid_uuid(uuid):
services = services.filter(uuid=uuid)
try:
start = self.get_start_date()
end = self.get_end_date()
except ValueError:
return JsonResponse(status=400, data={'error': 'Invalid date format'})
services_data = [
{
'name': s.name,
'uuid': s.uuid,
'link': s.link,
'stats': s.get_core_stats(start, end),
}
for s in services
]
services_data = self._convert_querysets_to_lists(services_data)
return JsonResponse(data={'services': services_data})
def _convert_querysets_to_lists(self, services_data):
for service_data in services_data:
for key, value in service_data['stats'].items():
if isinstance(value, QuerySet):
service_data['stats'][key] = list(value)
for key, value in service_data['stats']['compare'].items():
if isinstance(value, QuerySet):
service_data['stats']['compare'][key] = list(value)
return services_data

View File

@@ -1,24 +0,0 @@
# Generated by Django 3.2.5 on 2021-11-17 07:17
import core.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20200628_1403'),
]
operations = [
migrations.AddField(
model_name='user',
name='api_token',
field=models.TextField(default=core.models._default_api_token, unique=True),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -1,9 +1,8 @@
import ipaddress
import json
import re
import uuid
from secrets import token_urlsafe
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser
@@ -44,14 +43,9 @@ def _parse_network_list(networks: str):
return [ipaddress.ip_network(network.strip()) for network in networks.split(",")]
def _default_api_token():
return token_urlsafe(32)
class User(AbstractUser):
username = models.TextField(default=_default_uuid, unique=True)
email = models.EmailField(unique=True)
api_token = models.TextField(default=_default_api_token, unique=True)
def __str__(self):
return self.email
@@ -209,7 +203,6 @@ class Service(models.Model):
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
return {
"currently_online": currently_online,
"session_count": session_count,

View File

@@ -45,6 +45,11 @@ class DateRangeMixin:
"start": now.replace(day=1),
"end": now,
},
{
"name": "Last month",
"start": now.replace(day=1, month=now.month - 1),
"end": now.replace(day=1, month=now.month) - timezone.timedelta(days=1),
},
{
"name": "This year",
"start": now.replace(day=1, month=1),

View File

@@ -38,17 +38,3 @@
.geo-card--use-table-view .geo-table {
display: inline-block;
}
:root {
--color-neutral-000: white;
--color-neutral-50: #F8FAFC;
--color-neutral-100: #F1F5F9;
--color-neutral-200: #E2E8F0;
--color-neutral-300: #CBD5E1;
--color-neutral-400: #94A3B8;
--color-neutral-500: #64748B;
--color-neutral-600: #475569;
--color-neutral-700: #334155;
--color-neutral-800: #1E293B;
--color-neutral-900: #0F172A;
}

View File

@@ -6,7 +6,7 @@
</div>
<hr class="sep">
{% block main %}
<div class="card ~neutral !low max-w-lg content">
<div class="card max-w-lg content">
{% block card %}
{% endblock %}
</div>

View File

@@ -6,7 +6,7 @@
{% block page_title %}{% trans "Email Addresses" %}{% endblock %}
{% block main %}
<div class="card ~neutral !low max-w-lg">
<div class="card max-w-lg">
{% if user.emailaddress_set.all %}
<p>{% trans 'These are your known email addresses:' %}</p>
@@ -33,7 +33,7 @@
{% endfor %}
<div class="block mt-4">
<button class="button ~neutral !high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~neutral @high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
</div>
@@ -51,10 +51,10 @@
<hr class="sep">
<form method="post" action="{% url 'account_email' %}" class="card ~neutral !low max-w-lg">
<form method="post" action="{% url 'account_email' %}" class="card max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button name="action_add" class="button ~neutral !high" type="submit">{% trans "Add Address" %}</button>
<button name="action_add" class="button ~neutral @high" type="submit">{% trans "Add Address" %}</button>
</form>
{% endblock %}

View File

@@ -18,7 +18,7 @@
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button type="submit" class="button ~positive !high">{% trans 'Confirm' %}</button>
<button type="submit" class="button ~positive @high">{% trans 'Confirm' %}</button>
</form>
{% else %}

View File

@@ -16,7 +16,7 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button class="button ~urge !high mr-2" type="submit">{% trans "Sign In" %}</button>
<button class="button ~urge @high mr-2" type="submit">{% trans "Sign In" %}</button>
<a href="{% url 'account_reset_password' %}" class="button ~neutral mr-2">{% trans "Reset Password" %}</a>
<a href="{{ signup_url }}" class="button ~neutral">Sign Up</a>
</form>

View File

@@ -13,6 +13,6 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button type="submit" class="button ~neutral !high">{% trans 'Sign Out' %}</button>
<button type="submit" class="button ~neutral @high">{% trans 'Sign Out' %}</button>
</form>
{% endblock %}

View File

@@ -2,26 +2,13 @@
{% load i18n a17t_tags %}
{% block head_title %}{% trans "Change authentication info" %}{% endblock %}
{% block page_title %}{% trans "Change authentication info" %}{% endblock %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block page_title %}{% trans "Change Password" %}{% endblock %}
{% block card %}
<form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans "Change Password" %}</button>
</form>
<hr class="sep">
<div>
<p class="label mb-1">Personal API token</p>
<div class="flex justify-between">
<span class='chip ~info !normal'>{{request.user.api_token}}</span>
<form method="POST" action="{% url 'dashboard:api_token_refresh' %}">
{% csrf_token %}
<button type="submit" name="action" class="button ~neutral @high">{% trans "Refresh token" %}</button>
</form>
</div>
<p class="support mt-1">To learn more about the API, see our <a href="https://github.com/milesmcc/shynet/blob/master/GUIDE.md#api">API guide</a>.</p>
</div>
</div>
{% endblock %}

View File

@@ -18,6 +18,6 @@
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" class="button ~urge !high">{% trans 'Reset Password' %}</button>
<button type="submit" class="button ~urge @high">{% trans 'Reset Password' %}</button>
</form>
{% endblock %}

View File

@@ -13,7 +13,7 @@
<form method="POST" action="{{ action_url }}" class="max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans 'Change Password' %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans 'Change Password' %}</button>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>

View File

@@ -6,5 +6,5 @@
{% block card %}
<p>{% trans 'Your password is now changed.' %}</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
<a href="{% url 'account_login' %}" class="button ~urge @high">Log In</a>
{% endblock %}

View File

@@ -9,6 +9,6 @@
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans 'Set Password' %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans 'Set Password' %}</button>
</form>
{% endblock %}

View File

@@ -14,7 +14,7 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button type="submit" class="button ~urge !high">{% trans "Sign Up" %}</button>
<button type="submit" class="button ~urge @high">{% trans "Sign Up" %}</button>
</form>
{% endblock %}

View File

@@ -23,7 +23,7 @@
{% endblock %}
</head>
<body class="bg-neutral-100 min-h-full overflow-x-hidden">
<body class="bg-neutral-50 min-h-full overflow-x-hidden">
{% block body %}
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
@@ -34,7 +34,7 @@
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
</a>
<a tabindex="0" role="button" class="button ~neutral !low md:hidden"
<a tabindex="0" role="button" class="text-neutral-600 md:hidden"
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
<span class="icon">
<i class="fas fa-bars"></i>
@@ -110,7 +110,7 @@
{% if messages %}
<div>
{% for message in messages %}
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article>
<article class="card {{message.tags}} @high mb-2 w-full">{{message}}</article>
{% endfor %}
</ul>
</div>

View File

@@ -1,18 +1,20 @@
<form method="GET" id="datePicker">
<div class="~urge">
<form method="GET" id="datePicker" class="~urge">
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
</form>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral !low bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly>
<input type="input" id="rangePicker" placeholder="Date range" class="button ~neutral @low cursor-pointer" style="max-width: 200px;" readonly>
</div>
<style>
:root {
--litepicker-button-prev-month-color-hover: var(--color-urge);
--litepicker-button-next-month-color-hover: var(--color-urge);
--litepicker-day-color-hover: var(--color-urge);
--litepicker-is-today-color: var(--color-urge);
--litepicker-is-in-range-color: var(--color-urge-normal-fill);
--litepicker-is-start-color-bg: var(--color-urge);
--litepicker-is-end-color-bg: var(--color-urge);
--litepicker-button-apply-color-bg: var(--color-urge);
--litepicker-button-prev-month-color-hover: #7c3aed;
--litepicker-button-next-month-color-hover: #7c3aed;
--litepicker-day-color-hover: #7c3aed;
--litepicker-is-today-color: #7c3aed;
--litepicker-is-in-range-color: #ddd6fe;
--litepicker-is-start-color-bg: #7c3aed;
--litepicker-is-end-color-bg: #7c3aed;
--litepicker-button-apply-color-bg: #7c3aed;
}
.litepicker .container__predefined-ranges, .litepicker .container__months {

View File

@@ -1,6 +1,6 @@
{% load humanize helpers %}
<a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
<a class="card chart-card overflow-visible service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
{% with stats=object.stats %}
<div class="p-4 md:flex justify-between overflow-none">
<div class="flex items-center mb-4 md:mb-0 md:flex-1 md:min-w-0 truncate pr-0 md:pr-2">

View File

@@ -1,4 +1,4 @@
<div class="card ~neutral !high font-mono text-sm whitespace-pre-wrap break-all">{% filter force_escape %}<noscript>
<div class="card ~neutral @high font-mono text-sm whitespace-pre-wrap break-all">{% filter force_escape %}<noscript>
<img src="{{script_protocol}}{{request.get_host}}{% url 'ingress:endpoint_pixel' object.uuid %}">
</noscript>
<script defer src="{{script_protocol}}{{request.get_host}}{% url 'ingress:endpoint_script' object.uuid %}"></script>{% endfilter %}

View File

@@ -1,6 +1,6 @@
{% load helpers %}
<div>
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{{label}}"
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a>
<a class="portal {% if request.get_full_path|startswith:url %}text-urge-600{% endif %} flex items-center" title="{{label}}"
href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a>
</div>

View File

@@ -2,7 +2,7 @@
{% with stats=object.get_daily_stats %}
{% if stats.currently_online > 0 %}
<span class="chip ~positive !high whitespace-nowrap">
<span class="chip ~positive @high whitespace-nowrap">
{{stats.currently_online|intcomma}} online
</span>
{% endif %}

View File

@@ -13,7 +13,7 @@
</div>
{% has_perm "core.create_service" user as can_create %}
{% if can_create %}
<a href="{% url 'dashboard:service_create' %}" class="button field !low bg-neutral-000 w-auto">+ New Service</a>
<a href="{% url 'dashboard:service_create' %}" class="button ~neutral @low w-auto">+ New Service</a>
{% endif %}
</div>
</div>
@@ -21,7 +21,7 @@
{% for object in object_list|dictsortreversed:"stats.session_count" %}
{% include 'dashboard/includes/service_overview.html' %}
{% empty %}
<p class="aside ~urge !high">You don't have any services yet. {% if can_create %}Get started by <a href="{% url 'dashboard:service_create' %}" class="underline">creating one</a>.{% endif %}</p>
<p class="aside ~urge @high">You don't have any services yet. {% if can_create %}Get started by <a href="{% url 'dashboard:service_create' %}" class="underline">creating one</a>.{% endif %}</p>
{% endfor %}
{% if object_list %}

View File

@@ -4,6 +4,6 @@
<section class="content">
<h2>{{request.site.name}} Analytics</h2>
<p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
<a href="{% url 'account_login' %}" class="button ~urge @high">Log In</a>
</section>
{% endblock %}

View File

@@ -6,7 +6,7 @@
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
{% has_perm 'core.change_service' user object as can_update %}
{% if can_update %}
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field !low bg-neutral-000 w-auto">Manage &rarr;</a>
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button ~neutral @low w-auto">Manage &rarr;</a>
{% endif %}
{% endblock %}
@@ -19,7 +19,7 @@
{% include 'dashboard/includes/service_snippet.html' %}
</div>
{% else %}
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral @high px-6" id="stats">
{% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
<article class="">
<p class="label text-gray-400">Sessions</p>
@@ -93,12 +93,12 @@
</article>
{% endwith %}
</div>
<div class="card overflow-visible ~neutral !low py-0 mb-6">
<div class="card overflow-visible py-0 mb-6">
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity click_zoom=True %}
</div>
{% endif %}
<div id="card-grid" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -132,22 +132,22 @@
</tbody>
</table>
</div>
<div class="geo-map card ~neutral !low py-2 overflow-y-hidden">
<div class="geo-map card py-2 overflow-y-hidden">
<p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)">
Sessions by Geography &nbsp
<button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 button ~urge !low">
<button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 text-urge-600">
(view table)
</button>
</p>
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
</div>
<div class="geo-table card ~neutral !low limited-height py-2">
<div class="geo-table card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
<th>
Country &nbsp
<button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 button ~urge !low">
<button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 text-urge-600">
(view map)
</button>
</th>
@@ -180,7 +180,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -214,7 +214,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -248,7 +248,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -283,7 +283,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -318,7 +318,7 @@
</table>
</div>
</div>
<div class="card ~neutral !low py-2 overflow-auto">
<div class="card py-2 overflow-auto">
{% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8 md:h-12">
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more

View File

@@ -7,14 +7,14 @@
{% block content %}
<h4 class="heading leading-none">Create Service</h4>
<hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
<form class="card p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
{% include 'dashboard/includes/service_form.html' %}
</div>
<div class="section ~urge !normal p-4">
<button type="submit" class="button ~urge !high">Create</button>
<a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">Cancel</a>
<div class="section ~urge @low p-4">
<button type="submit" class="button ~urge @high">Create</button>
<a href="{% url 'dashboard:dashboard' %}" class="ml-4 text-urge-600">Cancel</a>
</div>
</form>
{% endblock %}

View File

@@ -5,16 +5,16 @@
{% block head_title %}Delete {{object.name}}{% endblock %}
{% block service_content %}
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
<form class="card p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
<p>Are you sure you want to delete this service? All of its
analytics and associated data will be permanently deleted.</p>
{{form|a17t}}
</div>
<div class="section ~critical !normal p-4">
<button type="submit" class="button ~critical !high">Delete</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~critical !low">Cancel</a>
<div class="section ~critical @low p-4">
<button type="submit" class="button ~critical @high">Delete</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="ml-4 text-critical-600">Cancel</a>
</div>
</form>
{% endblock %}

View File

@@ -5,11 +5,11 @@
{% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %}
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">Analytics &rarr;</a>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button ~neutral @low w-auto">Analytics &rarr;</a>
{% endblock %}
{% block service_content %}
<article class="card ~neutral !high">
<article class="card ~neutral @high">
<div class="md:flex items-center justify-between">
<div>
<h3 class="heading text-2xl mr-4">
@@ -18,7 +18,7 @@
</div>
<div>
<p class="font-medium text-lg">{{session.start_time|date:"M j Y, g:i a"}} to
{{session.last_seen|date:"g:i a"}}{% if session.is_currently_active %} <span class="chip ~positive !high text-base">Online</span>{% endif %}</p>
{{session.last_seen|date:"g:i a"}}{% if session.is_currently_active %} <span class="chip ~positive @high text-base">Online</span>{% endif %}</p>
</div>
</div>
<hr class="sep h-8 md:h-12">
@@ -70,7 +70,7 @@
<div class="md:w-2/12 mb-2 md:mr-4 pt-4 md:text-right">
<div class="text-lg font-medium">{{hit.start_time|date:"g:i a"}}</div>
</div>
<div class="md:flex card ~neutral !low flex-grow justify-between">
<div class="md:flex card flex-grow justify-between">
<div class="mb-4 md:mb-0 md:w-1/2">
<p class="label font-medium text-lg truncate">{{hit.location|default:"Unknown"|urlize}}</p>
{% if hit.referrer %}

View File

@@ -6,11 +6,11 @@
{% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low bg-neutral-000 w-auto">Analytics &rarr;</a>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button ~neutral @low bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %}
{% block service_content %}
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-auto">
<div class="card mb-8 pt-2 max-w-full overflow-x-auto">
{% include 'dashboard/includes/session_list.html' %}
</div>
{% pagination page_obj request %}

View File

@@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Management{% endblock %}
{% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">View &rarr;</a>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral @low w-auto">View &rarr;</a>
{% endblock %}
{% block service_content %}
@@ -15,35 +15,20 @@
{% include 'dashboard/includes/service_snippet.html' %}
<hr class="sep h-4">
<h5>Settings</h5>
<form class="card ~neutral !low p-0" method="POST">
<form class="card p-0" method="POST">
{% csrf_token %}
<div class="p-4">
{% include 'dashboard/includes/service_form.html' %}
</div>
<div class="section ~neutral !normal p-4 flex justify-between">
<div class="section ~neutral @low p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
<button type="submit" class="button ~neutral @high">Save</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral @low">Cancel</a>
</div>
<div>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical @high">Delete</a>
</div>
</div>
</form>
<hr class="sep h-4">
<h5>API</h5>
<div class="card ~neutral !low content">
<p>Shynet provides a simple API that you can use to pull data programmatically. You can access this data via this URL:</p>
<code class="text-sm">{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}</code>
<p>
There are 2 optional query parameters:
<ul>
<li><code class="text-sm">startDate</code> &mdash; to set the start date (in format YYYY-MM-DD)</li>
<li><code class="text-sm">endDate</code> &mdash; to set the end date (in format YYYY-MM-DD)</li>
</ul>
</p>
<p>Example using cURL:</p>
<code class="text-sm">curl -H 'Authorization: Token {{request.user.api_token}}' '{{script_protocol}}{{request.get_host}}{% url 'api:services' %}?uuid={{object.uuid}}&startDate=2021-01-01&endDate=2050-01-01'</code>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,6 @@
from django.urls import path
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from . import views
@@ -26,9 +28,4 @@ urlpatterns = [
views.ServiceSessionView.as_view(),
name="service_session",
),
path(
"api-token-refresh/",
views.RefreshApiTokenView.as_view(),
name="api_token_refresh",
),
]

View File

@@ -3,7 +3,8 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.db.models import Q
from django.shortcuts import get_object_or_404, reverse, redirect
from django.shortcuts import get_object_or_404, reverse
from django.utils import timezone
from django.views.generic import (
CreateView,
DeleteView,
@@ -11,12 +12,11 @@ from django.views.generic import (
ListView,
TemplateView,
UpdateView,
View,
)
from rules.contrib.views import PermissionRequiredMixin
from analytics.models import Session
from core.models import Service, _default_api_token
from core.models import Service
from .forms import ServiceForm
from .mixins import DateRangeMixin
@@ -155,10 +155,3 @@ class ServiceSessionView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
data = super().get_context_data(**kwargs)
data["object"] = get_object_or_404(Service, pk=self.kwargs.get("pk"))
return data
class RefreshApiTokenView(LoginRequiredMixin, View):
def post(self, request):
request.user.api_token = _default_api_token()
request.user.save()
return redirect('account_change_password')

View File

@@ -22,7 +22,7 @@ from django.contrib.messages import constants as messages
load_dotenv()
# Increment on new releases
VERSION = "0.12.0"
VERSION = "0.11.0"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -59,19 +59,16 @@ INSTALLED_APPS = [
"core",
"dashboard.apps.DashboardConfig",
"analytics",
"api",
"allauth",
"allauth.account",
"allauth.socialaccount",
"debug_toolbar",
"corsheaders",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@@ -309,15 +306,12 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
NPM_ROOT_PATH = "../"
NPM_FILE_PATTERNS = {
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
"apexcharts": [os.path.join("dist", "apexcharts.min.js")],
"litepicker": [
os.path.join("dist", "nocss", "litepicker.js"),
os.path.join("dist", "css", "litepicker.css"),
os.path.join("dist", "plugins", "ranges.js"),
],
"turbolinks": [os.path.join("dist", "turbolinks.js")],
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
"inter-ui": [os.path.join("Inter (web)", "*")],
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
"datamaps": [os.path.join("dist", "datamaps.world.min.js")],
@@ -373,6 +367,3 @@ DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
)
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_METHODS = ["GET", "OPTIONS"]

View File

@@ -25,5 +25,4 @@ urlpatterns = [
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
path("healthz/", include("health_check.urls")),
path("", include(("core.urls", "core"), namespace="core")),
path("api/v1/", include(("api.urls", "api"), namespace="api")),
]

22
tailwind.config.js Normal file
View File

@@ -0,0 +1,22 @@
let colors = require("tailwindcss/colors")
module.exports = {
content: ["./**/*.{html,py}"],
theme: {
extend: {
colors: {
neutral: colors.slate,
positive: colors.green,
urge: colors.violet,
warning: colors.yellow,
info: colors.blue,
critical: colors.red,
inf: "white",
zero: colors.slate[900]
}
},
},
plugins: [
require("a17t")
],
}