Compare commits

..

5 Commits

Author SHA1 Message Date
R. Miles McCain
17bb5cda0d Improve litepicker box shadow 2021-05-14 15:32:16 +00:00
R. Miles McCain
84c647ad43 Update packages 2021-05-14 15:32:08 +00:00
Paweł Jastrzębski
0e37e7f042 Update litepicker and add ranges plugin
Fix litepicker colors

Fix litepicker event

Add custom ranges to litepicker

Fix code style

Remove some date ranges

Fix date ranges

Replace yesterday date range with last 3 days
2021-05-14 15:09:14 +00:00
CasperVerswijvelt
a76e0feaf3 Add custom location url from environment variable
Remove trailing dollar in long and lat placeholder
2021-05-14 15:07:49 +00:00
CasperVerswijvelt
109d977932 Make favicon not squish and add ellipsis overflow
General styling improvements

Many UI Improvements

- Consistent spacing between titles and content
- Removed many ugly text squishing by hiding overflowing text with ellipsis
- Fixed Service favicon being squisched by long service name
- Hide scrollbar in 'more session' screen when content isn't scrollable
- Fix apexcharts tooltips and labels being cut off by card class

Disable wrapping in table cells, prefer ellipsis

Ellipsis overflow for long url on hit page

Fix flex grow in header not working as intended

Remove forgotten truncatechars

Fix code checks, add button role and tabindex
2021-05-14 15:04:30 +00:00
22 changed files with 133 additions and 1517 deletions

View File

@@ -1,43 +0,0 @@
name: Build edge Docker images
on:
push:
branches:
- master
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
TAGS="${DOCKER_IMAGE}:edge"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.prep.outputs.tags }}

View File

@@ -1,44 +0,0 @@
name: Build release Docker images
on:
push:
tags:
- "*"
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
VERSION=${GITHUB_REF#refs/tags/}
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:latest"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.prep.outputs.tags }}

View File

@@ -1,4 +1,4 @@
FROM python:alpine3.12 FROM python:3-alpine
# Getting things ready # Getting things ready
WORKDIR /usr/src/shynet WORKDIR /usr/src/shynet

39
Pipfile.lock generated
View File

@@ -19,7 +19,6 @@
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21", "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59" "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.6.1" "version": "==2.6.1"
}, },
"asgiref": { "asgiref": {
@@ -27,7 +26,6 @@
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78" "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
], ],
"markers": "python_version >= '3.6'",
"version": "==3.3.4" "version": "==3.3.4"
}, },
"billiard": { "billiard": {
@@ -47,17 +45,16 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
], ],
"version": "==2021.5.30" "version": "==2020.12.5"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.0.0" "version": "==4.0.0"
}, },
"defusedxml": { "defusedxml": {
@@ -65,16 +62,15 @@
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.7.1" "version": "==0.7.1"
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:a523d62b7ab2908f551dabc32b99017a86aa7784e32b761708e52be3dce6d35d", "sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862",
"sha256:dc41bf07357f1f4810c1c555b685cb51f780b41e37892d6cc92b89789f2847e1" "sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.12" "version": "==3.1.8"
}, },
"django-allauth": { "django-allauth": {
"hashes": [ "hashes": [
@@ -149,7 +145,6 @@
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.10" "version": "==2.10"
}, },
"kombu": { "kombu": {
@@ -157,23 +152,20 @@
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a", "sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74" "sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.6.11" "version": "==4.6.11"
}, },
"maxminddb": { "maxminddb": {
"hashes": [ "hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc" "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
], ],
"markers": "python_version >= '3.6'",
"version": "==2.0.3" "version": "==2.0.3"
}, },
"oauthlib": { "oauthlib": {
"hashes": [ "hashes": [
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc", "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3" "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
], ],
"markers": "python_version >= '3.6'", "version": "==3.1.0"
"version": "==3.1.1"
}, },
"psycopg2-binary": { "psycopg2-binary": {
"hashes": [ "hashes": [
@@ -285,14 +277,12 @@
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.25.1" "version": "==2.25.1"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
], ],
"version": "==1.3.0" "version": "==1.3.0"
}, },
@@ -308,7 +298,6 @@
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8" "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
], ],
"markers": "python_version >= '3.5'",
"version": "==0.4.1" "version": "==0.4.1"
}, },
"ua-parser": { "ua-parser": {
@@ -321,11 +310,10 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.4"
"version": "==1.26.5"
}, },
"user-agents": { "user-agents": {
"hashes": [ "hashes": [
@@ -340,7 +328,6 @@
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.3.0" "version": "==1.3.0"
}, },
"whitenoise": { "whitenoise": {

View File

@@ -33,8 +33,6 @@ ACCOUNT_SIGNUPS_ENABLED=False
ACCOUNT_EMAIL_VERIFICATION=none ACCOUNT_EMAIL_VERIFICATION=none
# The timezone of the admin panel. Affects how dates are displayed. # The timezone of the admin panel. Affects how dates are displayed.
# This must match a value from the IANA's tz database.
# Wikipedia has a list of valid strings: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TIME_ZONE=America/New_York TIME_ZONE=America/New_York
# Set to "False" if you will not be serving content over HTTPS # Set to "False" if you will not be serving content over HTTPS
@@ -94,10 +92,3 @@ AGGRESSIVE_HASH_SALTING=True
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE # - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE # - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE
# How many services should be displayed on dashboard page?
# Set to big number if you don't want pagination at all.
DASHBOARD_PAGE_SIZE=5
# Should background bars be scaled to full width?
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION=True

View File

@@ -127,16 +127,6 @@
"description": "Custom location url to link to in frontend.", "description": "Custom location url to link to in frontend.",
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE", "value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
"required": false "required": false
},
"DASHBOARD_PAGE_SIZE": {
"description": "How many services should be displayed on dashboard page?",
"value": "5",
"required": false
},
"USE_RELATIVE_MAX_IN_BAR_VISUALIZATION": {
"description": "Should background bars be scaled to full width?",
"value": "True",
"required": false
} }
} }
} }

View File

@@ -17,7 +17,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-webserver" - name: "shynet-webserver"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:dev"
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:
- secretRef: - secretRef:
@@ -42,7 +42,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-celeryworker" - name: "shynet-celeryworker"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:dev"
command: ["./celeryworker.sh"] command: ["./celeryworker.sh"]
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:

1020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,6 @@
"@fortawesome/fontawesome-free": "^5.15.1", "@fortawesome/fontawesome-free": "^5.15.1",
"a17t": "^0.5.1", "a17t": "^0.5.1",
"apexcharts": "^3.24.0", "apexcharts": "^3.24.0",
"datamaps": "^0.5.9",
"flag-icon-css": "^3.5.0", "flag-icon-css": "^3.5.0",
"inter-ui": "^3.15.0", "inter-ui": "^3.15.0",
"litepicker": "^2.0.11" "litepicker": "^2.0.11"

View File

@@ -8,14 +8,11 @@
var Shynet = { var Shynet = {
idempotency: null, idempotency: null,
heartbeatTaskId: null, heartbeatTaskId: null,
skipHeartbeat: false,
sendHeartbeat: function () { sendHeartbeat: function () {
try { try {
if (document.hidden || Shynet.skipHeartbeat) { if (document.hidden) {
return; return;
} }
Shynet.skipHeartbeat = true;
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open( xhr.open(
"POST", "POST",
@@ -23,12 +20,6 @@ var Shynet = {
true true
); );
xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
Shynet.skipHeartbeat = false;
};
xhr.onerror = function () {
Shynet.skipHeartbeat = false;
};
xhr.send( xhr.send(
JSON.stringify({ JSON.stringify({
idempotency: Shynet.idempotency, idempotency: Shynet.idempotency,
@@ -39,14 +30,13 @@ var Shynet = {
window.performance.timing.navigationStart, window.performance.timing.navigationStart,
}) })
); );
} catch (e) {} } catch (e) { }
}, },
newPageLoad: function () { newPageLoad: function () {
if (Shynet.heartbeatTaskId != null) { if (Shynet.heartbeatTaskId != null) {
clearInterval(Shynet.heartbeatTaskId); clearInterval(Shynet.heartbeatTaskId);
} }
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
Shynet.skipHeartbeat = false;
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}")); Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
Shynet.sendHeartbeat(); Shynet.sendHeartbeat();
} }

View File

@@ -8,12 +8,12 @@ from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models.functions import TruncDate, TruncHour from django.db.models.functions import TruncDate
from django.db.utils import NotSupportedError from django.db.utils import NotSupportedError
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
ACTIVE_USER_TIMEDELTA = timezone.timedelta( ACTIVE_USER_TIMEDELTA = timezone.timedelta(
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2 milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
) )
@@ -125,10 +125,8 @@ class Service(models.Model):
Session = apps.get_model("analytics", "Session") Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit") Hit = apps.get_model("analytics", "Hit")
tz_now = timezone.now()
currently_online = Session.objects.filter( currently_online = Session.objects.filter(
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA service=self, last_seen__gt=timezone.now() - ACTIVE_USER_TIMEDELTA
).count() ).count()
sessions = Session.objects.filter( sessions = Session.objects.filter(
@@ -198,36 +196,6 @@ class Service(models.Model):
avg_hits_per_session = hit_count / session_count if session_count > 0 else None avg_hits_per_session = hit_count / session_count if session_count > 0 else None
avg_session_duration = self._get_avg_session_duration(sessions, session_count)
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,
"hit_count": hit_count,
"has_hits": has_hits,
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"chart_data": chart_data,
"chart_tooltip_format": chart_tooltip_format,
"chart_granularity": chart_granularity,
"online": True,
}
def _get_avg_session_duration(self, sessions, session_count):
try: try:
avg_session_duration = sessions.annotate( avg_session_duration = sessions.annotate(
duration=models.F("last_seen") - models.F("start_time") duration=models.F("last_seen") - models.F("start_time")
@@ -242,72 +210,48 @@ class Service(models.Model):
if session_count == 0: if session_count == 0:
avg_session_duration = None avg_session_duration = None
return avg_session_duration session_chart_data = {
k["date"]: k["count"]
def _get_chart_data(self, sessions, hits, start_time, end_time, tz_now): for k in sessions.annotate(date=TruncDate("start_time"))
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart .values("date")
if (end_time - start_time).days < 3:
chart_tooltip_format = "MM/dd HH:mm"
chart_granularity = "hourly"
sessions_per_hour = (
sessions.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("uuid")) .annotate(count=models.Count("uuid"))
.order_by("hour") .order_by("date")
)
chart_data = {
k["hour"]: {"sessions": k["count"]} for k in sessions_per_hour
} }
hits_per_hour = (
hits.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("id"))
.order_by("hour")
)
for k in hits_per_hour:
if k["hour"] not in chart_data:
chart_data[k["hour"]] = {"hits": k["count"], "sessions": 0}
else:
chart_data[k["hour"]]["hits"] = k["count"]
hours_range = range(int((end_time - start_time).total_seconds() / 3600) + 1)
for hour_offset in hours_range:
hour = start_time + timezone.timedelta(hours=hour_offset)
if hour not in chart_data and hour <= tz_now:
chart_data[hour] = {"sessions": 0, "hits": 0}
else:
chart_tooltip_format = "MMM d"
chart_granularity = "daily"
sessions_per_day = (
sessions.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("uuid"))
.order_by("date")
)
chart_data = {k["date"]: {"sessions": k["count"]} for k in sessions_per_day}
hits_per_day = (
hits.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("id"))
.order_by("date")
)
for k in hits_per_day:
chart_data[k["date"]]["hits"] = k["count"]
for day_offset in range((end_time - start_time).days + 1): for day_offset in range((end_time - start_time).days + 1):
day = (start_time + timezone.timedelta(days=day_offset)).date() day = (start_time + timezone.timedelta(days=day_offset)).date()
if day not in chart_data and day <= tz_now.date(): if day not in session_chart_data:
chart_data[day] = {"sessions": 0, "hits": 0} session_chart_data[day] = 0
chart_data = sorted(chart_data.items(), key=lambda k: k[0]) return {
chart_data = { "currently_online": currently_online,
'sessions': [v['sessions'] for k, v in chart_data], "session_count": session_count,
'hits': [v['hits'] for k, v in chart_data], "hit_count": hit_count,
'labels': [str(k) for k, v in chart_data], "has_hits": has_hits,
"avg_hits_per_session": hit_count / (max(session_count, 1)),
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"session_chart_data": json.dumps(
[
{"x": str(key), "y": value}
for key, value in sorted(
session_chart_data.items(), key=lambda k: k[0]
)
]
),
"online": True,
} }
return chart_data, chart_tooltip_format, chart_granularity
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"dashboard:service", "dashboard:service",

View File

@@ -11,11 +11,6 @@
max-height: 400px; max-height: 400px;
} }
.force-limited-height {
max-height: 400px;
overflow: hidden;
}
.rf { .rf {
text-align: right !important; text-align: right !important;
} }

View File

@@ -12,13 +12,11 @@
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script> <script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script> <script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
<script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script> <script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script>
<script src="{% static 'd3/d3.min.js' %}"></script> <link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
<script src="{% static 'topojson/build/topojson.min.js' %}"></script>
<script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script>
<script src="{% static 'dashboard/js/base.js' %}"></script> <script src="{% static 'dashboard/js/base.js' %}"></script>
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}"> <link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}"> <link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
{% block extra_head %} {% block extra_head %}
{% endblock %} {% endblock %}
</head> </head>

View File

@@ -1,6 +0,0 @@
{% load helpers %}
<div
class="absolute h-6 rounded-md"
style="width: {% bar_width count max total %}; top: 6px; left: 0px; height: calc(100% - 12px); background-color: var(--color-urge-100-fallback)"
>
</div>

View File

@@ -2,7 +2,7 @@
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate"> <input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate"> <input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
</form> </form>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly> <input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer w-auto" readonly>
<style> <style>
:root { :root {
--litepicker-button-prev-month-color-hover: var(--color-urge); --litepicker-button-prev-month-color-hover: var(--color-urge);

View File

@@ -1,59 +0,0 @@
{% load helpers %}
<div id="map-chart" class="relative"></div>
<script>
// Colors
const lightBlue = "#C4B5FD";
const highlightBlue = "#8B5CF6";
const white = "#ffffff";
// Data maps
const countryMapData = {};
const countryMapColors = {};
const countryMap = {
{% for country in countries %}"{{country.country|safe|datamap_id}}": {{country.count}},
{% endfor %}
};
// Max session count will be full opacity
const maxSessionCount = Math.max(...Object.values(countryMap));
// Color scale starts from opacity 0.1 - 1.0, 0 sessions gets opacity 0
const minPercentage = 0.1
// Loop over country map and transform data for Datamaps use
const keys = Object.keys(countryMap);
const length = keys.length;
for (let i = 0; i < length; i++) {
countryMapData[keys[i]] = {
sessionCount: countryMap[keys[i]],
color: `rgba(124, 58, 237, ${countryMap[keys[i]] === 0 ? 0 : minPercentage + (countryMap[keys[i]] / maxSessionCount * (1 - minPercentage))})`
};
countryMapColors[keys[i]] = countryMapData[keys[i]].color;
}
// Create datamap
const map = new Datamap({
element: document.getElementById('map-chart'),
projection: 'mercator',
responsive: true,
geographyConfig: {
borderColor: lightBlue,
highlightBorderColor: highlightBlue,
highlightBorderWidth: 1.5,
highlightFillColor: (geography) => geography.color || white,
highlightFillOpacity: 0.9,
popupTemplate: (geography, data) => '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong>: ' + data.sessionCount + ' sessions</div>'
},
fills: {
defaultFill: white
},
data: countryMapData,
aspectRatio: 0.68
});
map.updateChoropleth(countryMapColors);
// Handle resize. TODO: debounce?
window.onresize = () => map.resize();
</script>

View File

@@ -51,7 +51,7 @@
</div> </div>
<hr class="sep h-4"> <hr class="sep h-4">
<div style="bottom: -1px;"> <div style="bottom: -1px;">
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity %} {% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
</div> </div>
{% endwith %} {% endwith %}
</a> </a>

View File

@@ -5,14 +5,9 @@
enabled: false enabled: false
}, },
tooltip: { tooltip: {
shared: true, shared: false,
x: {
format: '{{tooltip_format|default:"MMM d"}}',
},
},
legend: {
show: false,
}, },
colors: ["#805AD5"],
chart: { chart: {
zoom: { zoom: {
enabled: false, enabled: false,
@@ -20,7 +15,7 @@
toolbar: { toolbar: {
show: false, show: false,
}, },
type: 'line', type: 'area',
height: {{height|default:"200"}}, height: {{height|default:"200"}},
offsetY: -1, offsetY: -1,
animations: { animations: {
@@ -29,14 +24,16 @@
sparkline: { sparkline: {
enabled: {% if sparkline %}true{% else %}false{% endif %}, enabled: {% if sparkline %}true{% else %}false{% endif %},
}, },
{% if granularity == "daily" and click_zoom %} fill: {
events: { type: 'gradient',
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) { gradient: {
const day = config.labels[dataPointIndex] shadeIntensity: 1,
window.location.href = `?startDate=${day}&endDate=${day}` inverseColors: false,
opacityFrom: 0.8,
opacityTo: 0,
stops: [0, 75, 100]
}, },
}, },
{% endif %}
}, },
grid: { grid: {
padding: { padding: {
@@ -66,26 +63,14 @@
}, },
xaxis: { xaxis: {
type: "datetime", type: "datetime",
labels: {
datetimeUTC: false
},
}, },
stroke: { stroke: {
width: 2, width: 1.5,
curve: 'smooth',
}, },
series: [{ series: [{
name: "Hits", name: "{{unit|default:'Sessions'}}",
type: 'area', data: {{data|safe}}
color: "#ddd6fe", }]
data: {{data.hits|safe}}
}, {
name: "Sessions",
type: 'line',
color: "#805AD5",
data: {{data.sessions|safe}}
}],
labels: {{data.labels|safe}}
}; };
var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions); var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions);
triggerMatchesChart.render(); triggerMatchesChart.render();

View File

@@ -94,7 +94,7 @@
{% endwith %} {% endwith %}
</div> </div>
<div class="card overflow-visible ~neutral !low py-0 mb-6"> <div class="card overflow-visible ~neutral !low 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 %} {% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %}
</div> </div>
{% endif %} {% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
@@ -109,20 +109,8 @@
<tbody> <tbody>
{% for location in stats.locations %} {% for location in stats.locations %}
<tr> <tr>
<td class="truncate w-full max-w-0 relative"> <td class="truncate w-full max-w-0">{{location.location|default:"Unknown"|urldisplay}}</td>
{% include 'dashboard/includes/bar.html' with count=location.count max=stats.locations.0.count total=stats.hit_count %} <td class="rf">{{location.count|intcomma}}</td>
<div class="relative flex items-center">
{{location.location|default:"Unknown"|urldisplay}}
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{location.count|intcomma}}
<span class="text-xs rf" style="min-width: 48px">
({{location.count|percent:stats.hit_count}})
</span>
</div>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@@ -132,10 +120,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card ~neutral !low force-limited-height py-2 overflow-y-hidden">
<p class="text-sm font-semibold mx-2 p-2 border-b mb-2">Sessions by Geography</p>
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
</div>
<div class="card ~neutral !low limited-height py-2"> <div class="card ~neutral !low limited-height py-2">
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
@@ -147,20 +131,32 @@
<tbody> <tbody>
{% for referrer in stats.referrers %} {% for referrer in stats.referrers %}
<tr> <tr>
<td class="truncate w-full max-w-0 relative"> <td class="truncate w-full max-w-0">{{referrer.referrer|default:"Direct"|urldisplay}}</td>
{% include 'dashboard/includes/bar.html' with count=referrer.count max=stats.referrers.0.count total=stats.session_count %} <td class="rf">{{referrer.count|intcomma}}</td>
<div class="relative flex items-center"> </tr>
{{referrer.referrer|default:"Direct"|urldisplay}} {% empty %}
</div> <tr>
</td> <td><span class="text-gray-600">No data yet...</span></td>
<td> </tr>
<div class="flex justify-end items-center"> {% endfor %}
{{referrer.count|intcomma}} </tbody>
<span class="text-xs rf" style="min-width: 48px"> </table>
({{referrer.count|percent:stats.session_count}})
</span>
</div> </div>
<div class="card ~neutral !low limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
<th>Country</th>
<th class="rf">Sessions</th>
</tr>
</thead>
<tbody>
{% for country in stats.countries %}
<tr>
<td class="truncate w-full max-w-0" title="{{country.country|country_name}}">
<span class="{{country.country|flag_class}}"></span> {{country.country|country_name}}
</td> </td>
<td class="rf">{{country.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@@ -181,20 +177,10 @@
<tbody> <tbody>
{% for os in stats.operating_systems %} {% for os in stats.operating_systems %}
<tr> <tr>
<td class="flex items-center truncate w-full max-w-0 relative" title="{{os.os|default:'Unknown'}}"> <td class="flex items-center truncate w-full max-w-0" title="{{os.os|default:'Unknown'}}">
{% include 'dashboard/includes/bar.html' with count=os.count max=stats.operating_systems.0.count total=stats.session_count %}
<div class="relative flex items-center">
{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span> {{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{os.count|intcomma}}
<span class="text-xs rf" style="min-width: 48px">
({{os.count|percent:stats.session_count}})
</span>
</div>
</td> </td>
<td class="rf">{{os.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@@ -215,21 +201,9 @@
<tbody> <tbody>
{% for browser in stats.browsers %} {% for browser in stats.browsers %}
<tr> <tr>
<td class="flex items-center truncate w-full max-w-0 relative" title="{{browser.browser|default:'Unknown'}}"> <td class="flex items-center truncate w-full max-w-0" title="{{browser.browser|default:'Unknown'}}">
{% include 'dashboard/includes/bar.html' with count=browser.count max=stats.browsers.0.count total=stats.session_count %} {{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span></td>
</div> <td class="rf">{{browser.count|intcomma}}</td>
<div class="relative flex items-center">
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{browser.count|intcomma}}
<span class="text-xs rf" style="min-width: 48px">
({{browser.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@@ -250,20 +224,8 @@
<tbody> <tbody>
{% for device_type in stats.device_types %} {% for device_type in stats.device_types %}
<tr> <tr>
<td class="truncate w-full max-w-0 relative"> <td class="truncate w-full max-w-0">{{device_type.device_type|default:"Unknown"|title}}</td>
{% include 'dashboard/includes/bar.html' with count=device_type.count max=stats.device_types.0.count total=stats.session_count %} <td class="rf">{{device_type.count|intcomma}}</td>
<div class="relative flex items-center">
{{device_type.device_type|default:"Unknown"|title}}
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{device_type.count|intcomma}}
<span class="text-xs rf" style="min-width: 48px">
({{device_type.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
@@ -274,7 +236,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="card ~neutral !low py-2"> <div class="card ~neutral !low limited-height py-2">
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8 md:h-12"> <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 <a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more

View File

@@ -43,14 +43,6 @@ def country_name(isocode):
return "Unknown" return "Unknown"
@register.filter
def datamap_id(isocode):
try:
return pycountry.countries.get(alpha_2=isocode).alpha_3
except:
return "UNKNOWN"
@register.simple_tag @register.simple_tag
def relative_stat_tone( def relative_stat_tone(
start, start,
@@ -194,7 +186,6 @@ def urldisplay(url):
else: else:
return url return url
class ContextualURLNode(template.Node): class ContextualURLNode(template.Node):
"""Extension of the Django URLNode to support including contextual parameters in URL outputs. In other words, URLs generated will keep the start and end date parameters.""" """Extension of the Django URLNode to support including contextual parameters in URL outputs. In other words, URLs generated will keep the start and end date parameters."""
@@ -214,13 +205,9 @@ class ContextualURLNode(template.Node):
url_parts = list(urlparse(url)) url_parts = list(urlparse(url))
query = dict(urllib.parse.parse_qsl(url_parts[4])) query = dict(urllib.parse.parse_qsl(url_parts[4]))
query.update( query.update({
{ param: context.request.GET.get(param) for param in self.CONTEXT_PARAMS if param in context.request.GET and param not in query
param: context.request.GET.get(param) })
for param in self.CONTEXT_PARAMS
if param in context.request.GET and param not in query
}
)
url_parts[4] = urllib.parse.urlencode(query) url_parts[4] = urllib.parse.urlencode(query)
@@ -238,38 +225,6 @@ def contextual_url(*args, **kwargs):
urlnode = url_tag(*args, **kwargs) urlnode = url_tag(*args, **kwargs)
return ContextualURLNode(urlnode) return ContextualURLNode(urlnode)
@register.filter @register.filter
def location_url(session): def location_url(session):
return settings.LOCATION_URL.replace("$LATITUDE", str(session.latitude)).replace( return settings.LOCATION_URL.replace("$LATITUDE", str(session.latitude)).replace("$LONGITUDE", str(session.longitude))
"$LONGITUDE", str(session.longitude)
)
@register.filter
def percent(value, total):
if total == 0:
return "N/A"
percent = value / total
if percent < 0.001:
return "<0.1%"
return f'{percent:.1%}'
@register.simple_tag
def bar_width(count, max, total):
if total == 0 or max == 0:
return "0"
if settings.USE_RELATIVE_MAX_IN_BAR_VISUALIZATION:
percent = count / max
else:
percent = count / total
if percent < 0.001:
return "0"
return f'{percent:.1%}'

View File

@@ -25,7 +25,7 @@ from .mixins import DateRangeMixin
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView): class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
model = Service model = Service
template_name = "dashboard/pages/dashboard.html" template_name = "dashboard/pages/dashboard.html"
paginate_by = settings.DASHBOARD_PAGE_SIZE paginate_by = 5
def get_queryset(self): def get_queryset(self):
return Service.objects.filter( return Service.objects.filter(

View File

@@ -18,7 +18,7 @@ import urllib.parse as urlparse
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
# Increment on new releases # Increment on new releases
VERSION = "v0.10.0" VERSION = "v0.8.2"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -310,13 +310,7 @@ NPM_FILE_PATTERNS = {
"stimulus": [os.path.join("dist", "stimulus.umd.js")], "stimulus": [os.path.join("dist", "stimulus.umd.js")],
"inter-ui": [os.path.join("Inter (web)", "*")], "inter-ui": [os.path.join("Inter (web)", "*")],
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")], "@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
"datamaps": [os.path.join("dist", "datamaps.world.min.js")], "flag-icon-css": [os.path.join("css", "flag-icon.min.css"), os.path.join("flags", "*")],
"d3": ["d3.min.js"],
"topojson": [os.path.join("build", "topojson.min.js")],
"flag-icon-css": [
os.path.join("css", "flag-icon.min.css"),
os.path.join("flags", "*"),
],
} }
# Shynet # Shynet
@@ -352,14 +346,4 @@ BLOCK_ALL_IPS = os.getenv("BLOCK_ALL_IPS", "False") == "True"
AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True" AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True"
# What location url should be linked to in the frontend? # What location url should be linked to in the frontend?
LOCATION_URL = os.getenv( LOCATION_URL = os.getenv("LOCATION_URL", "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE")
"LOCATION_URL", "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE"
)
# How many services should be displayed on dashboard page?
DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
# Should background bars be scaled to full width?
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
)