Implement more UI elements

This commit is contained in:
Mark Veidemanis 2022-07-21 13:51:12 +01:00
parent 2c62c343b8
commit b2a1c42f3d
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
9 changed files with 403 additions and 4 deletions

View File

@ -19,6 +19,12 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from core.api.views.threshold import (
ThresholdChans,
ThresholdInfoModal,
ThresholdOnline,
ThresholdUsers,
)
from core.ui.views.drilldown import Drilldown from core.ui.views.drilldown import Drilldown
from core.views import Billing, Cancel, Home, Order, Portal, Signup from core.views import Billing, Cancel, Home, Order, Portal, Signup
from core.views.callbacks import Callback from core.views.callbacks import Callback
@ -44,5 +50,9 @@ urlpatterns = [
path("accounts/signup/", Signup.as_view(), name="signup"), path("accounts/signup/", Signup.as_view(), name="signup"),
path("ui/drilldown/", Drilldown.as_view(), name="drilldown"), path("ui/drilldown/", Drilldown.as_view(), name="drilldown"),
path("parts/search/", Search.as_view(), name="search"), path("parts/search/", Search.as_view(), name="search"),
path("modal/info/", ThresholdInfoModal.as_view(), name="modal_info"),
path("api/chans/", ThresholdChans.as_view(), name="chans"),
path("api/users/", ThresholdUsers.as_view(), name="users"),
path("api/online/", ThresholdOnline.as_view(), name="online"),
path("api/search/", APISearch.as_view(), name="api_search"), path("api/search/", APISearch.as_view(), name="api_search"),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

106
core/api/views/threshold.py Normal file
View File

@ -0,0 +1,106 @@
import logging
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render
from rest_framework.parsers import FormParser
from rest_framework.views import APIView
from core.lib.threshold import annotate_online, get_chans, get_users
logger = logging.getLogger(__name__)
class ThresholdChans(LoginRequiredMixin, APIView):
parser_classes = [FormParser]
plan_name = "drilldown"
def post(self, request):
if not request.user.has_plan(self.plan_name):
return JsonResponse({"success": False})
if "net" not in request.data:
return JsonResponse({"success": False})
if "query" not in request.data:
return JsonResponse({"success": False})
net = request.data["net"]
query = request.data["query"]
channels = get_chans(net, [query])
if not channels:
return HttpResponse("")
channels_human = ", ".join(channels)
return HttpResponse(channels_human)
class ThresholdUsers(LoginRequiredMixin, APIView):
parser_classes = [FormParser]
plan_name = "drilldown"
def post(self, request):
if not request.user.has_plan(self.plan_name):
return JsonResponse({"success": False})
if "net" not in request.data:
return JsonResponse({"success": False})
if "query" not in request.data:
return JsonResponse({"success": False})
net = request.data["net"]
query = request.data["query"]
users = get_users(net, [query])
if not users:
return HttpResponse("")
users_human = ", ".join(users)
return HttpResponse(users_human)
class ThresholdOnline(LoginRequiredMixin, APIView):
parser_classes = [FormParser]
plan_name = "drilldown"
def post(self, request):
if not request.user.has_plan(self.plan_name):
return JsonResponse({"success": False})
if "net" not in request.data:
return JsonResponse({"success": False})
if "query" not in request.data:
return JsonResponse({"success": False})
net = request.data["net"]
query = request.data["query"]
online_info = annotate_online(net, query)
return JsonResponse(online_info)
class ThresholdInfoModal(LoginRequiredMixin, APIView):
parser_classes = [FormParser]
plan_name = "drilldown"
template_name = "modals/info.html"
def post(self, request):
if not request.user.has_plan(self.plan_name):
return JsonResponse({"success": False})
if "net" not in request.data:
return JsonResponse({"success": False})
if "nick" not in request.data:
return JsonResponse({"success": False})
if "channel" not in request.data:
return JsonResponse({"success": False})
net = request.data["net"]
nick = request.data["nick"]
channel = request.data["channel"]
channels = get_chans(net, [nick])
users = get_users(net, [channel])
if channels:
inter_users = get_users(net, channels)
else:
inter_users = []
if users:
inter_chans = get_chans(net, users)
else:
inter_chans = []
context = {
"nick": nick,
"channel": channel,
"chans": channels,
"users": users,
"inter_chans": inter_chans,
"inter_users": inter_users,
}
return render(request, self.template_name, context)

76
core/lib/threshold.py Normal file
View File

@ -0,0 +1,76 @@
import logging
from json import dumps
import requests
from django.conf import settings
from requests.exceptions import JSONDecodeError
logger = logging.getLogger(__name__)
def escape(obj):
chars = ["[", "]", "^", "-", "*", "?"]
if isinstance(obj, str):
obj = obj.replace("\\", "\\\\")
for i in chars:
obj = obj.replace(i, "\\" + i)
elif isinstance(obj, list):
for i in obj:
i = escape(i)
elif isinstance(obj, dict):
for key in obj:
obj[key] = escape(obj[key])
return obj
def threshold_request(url, data):
headers = {
"ApiKey": settings.THRESHOLD_API_KEY,
"Token": settings.THRESHOLD_API_TOKEN,
}
for key in data:
data[key] = escape(data[key])
r = requests.post(
f"{settings.THRESHOLD_ENDPOINT}/{url}/", data=dumps(data), headers=headers
)
if not r.headers.get("Counter") == settings.THRESHOLD_API_COUNTER:
logger.error(
(
f"Threshold API counter mismatch: "
f"{r.headers.get('Counter')} != "
f"{settings.THRESHOLD_API_COUNTER}"
)
)
return False
try:
response = r.json()
except JSONDecodeError:
logging.error(f"Invalid JSON response: {r.text}")
return response
def get_chans(net, query):
url = "chans"
payload = {"net": net, "query": query}
channels = threshold_request(url, payload)
if not channels:
return []
return channels["chans"]
def get_users(net, query):
url = "users"
payload = {"net": net, "query": query}
users = threshold_request(url, payload)
if not users:
return []
return users["users"]
def annotate_online(net, query):
url = "online"
payload = {"net": net, "query": query}
online_info = threshold_request(url, payload)
if not online_info:
return {}
return online_info

View File

@ -9,6 +9,7 @@
<title>Pathogen - {{ request.path_info }}</title> <title>Pathogen - {{ request.path_info }}</title>
<link rel="shortcut icon" href="{% static 'favicon.ico' %}"> <link rel="shortcut icon" href="{% static 'favicon.ico' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@creativebulma/bulma-tooltip@1.2.0/dist/bulma-tooltip.min.css">
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css" /> <link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css" />
<script src="https://unpkg.com/htmx.org@1.8.0" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script> <script src="https://unpkg.com/htmx.org@1.8.0" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
<script> <script>

View File

@ -0,0 +1,110 @@
<script>
var modal = document.querySelector('.modal'); // assuming you have only 1
var html = document.querySelector('html');
modal.querySelector('.modal-background').addEventListener('click', function(e) {
e.preventDefault();
modal.classList.remove('is-active');
html.classList.remove('is-clipped');
});
modal.querySelector('.modal-close').addEventListener('click', function(e) {
e.preventDefault();
modal.classList.remove('is-active');
html.classList.remove('is-clipped');
});
var TABS = [...document.querySelectorAll('#tabs li')];
var CONTENT = [...document.querySelectorAll('#tab-content div')];
var ACTIVE_CLASS = 'is-active';
initTabs();
</script>
<style>
#tab-content div {
display: none;
}
#tab-content div.is-active {
display: block;
}
</style>
<div class="modal is-active is-clipped">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<div class="tabs is-toggle is-fullwidth" id="tabs">
<ul>
<li class="is-active" data-tab="1">
<a>
<span class="icon is-small"><i class="fa-solid fa-user"></i></span>
<span>Channels</span>
</a>
</li>
<li data-tab="2">
<a>
<span class="icon is-small"><i class="fa-solid fa-hashtag"></i></span>
<span>Users</span>
</a>
</li>
<li data-tab="3">
<a>
<span class="icon is-small"><i class="fa-solid fa-people"></i></span>
<span>Intersection</span>
</a>
</li>
<li data-tab="4">
<a>
<span class="icon is-small"><i class="fa-solid fa-hashtag"></i></span>
<span>Intersection</span>
</a>
</li>
</ul>
</div>
<div id="tab-content">
<div class="is-active" data-content="1">
<h4 class="subtitle is-4">Channels {{ nick }} is on</h4>
{% for channel in chans %}
<a class="panel-block">
<span class="panel-icon">
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
</span>
{{ channel }}
</a>
{% endfor %}
</div>
<div data-content="2">
<h4 class="subtitle is-4">Users on {{ channel }}</h4>
{% for user in users %}
<a class="panel-block">
<span class="panel-icon">
<i class="fa-solid fa-user" aria-hidden="true"></i>
</span>
{{ user }}
</a>
{% endfor %}
</div>
<div data-content="3">
<h4 class="subtitle is-4">Users sharing channels with {{ nick }}</h4>
{% for user in inter_users %}
<a class="panel-block">
<span class="panel-icon">
<i class="fa-solid fa-user" aria-hidden="true"></i>
</span>
{{ user }}
</a>
{% endfor %}
</div>
<div data-content="4">
<h4 class="subtitle is-4">Channels sharing users with {{ channel }}</h4>
{% for channel in inter_chans %}
<a class="panel-block">
<span class="panel-icon">
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
</span>
{{ channel }}
</a>
{% endfor %}
</div>
</div>
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
</div>

View File

@ -2,6 +2,38 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
function initTabs() {
TABS.forEach((tab) => {
tab.addEventListener('click', (e) => {
let selected = tab.getAttribute('data-tab');
updateActiveTab(tab);
updateActiveContent(selected);
})
})
}
function updateActiveTab(selected) {
TABS.forEach((tab) => {
if (tab && tab.classList.contains(ACTIVE_CLASS)) {
tab.classList.remove(ACTIVE_CLASS);
}
});
selected.classList.add(ACTIVE_CLASS);
}
function updateActiveContent(selected) {
CONTENT.forEach((item) => {
if (item && item.classList.contains(ACTIVE_CLASS)) {
item.classList.remove(ACTIVE_CLASS);
}
let data = item.getAttribute('data-content');
if (data === selected) {
item.classList.add(ACTIVE_CLASS);
}
});
}
</script>
<div class="box"> <div class="box">
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
@ -10,7 +42,7 @@
<div class="field-body"> <div class="field-body">
<div class="field"> <div class="field">
<div class="control is-expanded has-icons-left"> <div class="control is-expanded has-icons-left">
<input name="query" class="input" type="text" placeholder="Query"> <input name="query" class="input" type="text" placeholder="msg: science AND nick: BillNye AND channel: #science">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="fas fa-magnifying-glass"></i> <i class="fas fa-magnifying-glass"></i>
</span> </span>

View File

@ -9,14 +9,21 @@
<script src="{% static 'chart.js' %}"></script> <script src="{% static 'chart.js' %}"></script>
</div> </div>
<div class="box"> <div class="box">
<span class="tag is-success">Online</span>
<span class="tag is-danger">Offline</span>
<span class="tag is-warning">Unknown</span>
IRC: <i class="fa-solid fa-hashtag" aria-hidden="true"></i>
Discord: <i class="fa-brands fa-discord" aria-hidden="true"></i>
<div class="table-container"> <div class="table-container">
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>src</th>
<th>TS</th> <th>TS</th>
<th>msg</th> <th>msg</th>
<th>host</th> <th>host</th>
<th>nick</th> <th>nick</th>
<th>actions</th>
<th>channel</th> <th>channel</th>
<th>net</th> <th>net</th>
</tr> </tr>
@ -25,10 +32,43 @@
<tbody> <tbody>
{% for item in results %} {% for item in results %}
<tr> <tr>
<td>{{ item.ts }}</td> <td>
{% if item.src == 'irc' %}
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
{% elif item.src == 'dis' %}
<i class="fa-brands fa-discord" aria-hidden="true"></i>
{% endif %}
</td>
<td>
<p>{{ item.date }}</p>
<p>{{ item.time }}</p>
</td>
<td>{{ item.msg }}</td> <td>{{ item.msg }}</td>
<td>{{ item.host }}</td> <td>{{ item.host }}</td>
<td>{{ item.nick }}</td> <td>
{% if item.online is True %}
<span class="tag is-success">{{ item.nick }}</span>
{% elif item.online is False %}
<span class="tag is-danger">{{ item.nick }}</span>
{% else %}
<span class="tag is-warning">{{ item.nick }}</span>
{% endif %}
</td>
<td>
{% if item.src == 'irc' %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-post="{% url 'modal_info' %}"
hx-vals='{"net": "{{ item.net }}", "nick": "{{ item.nick }}", "channel": "{{ item.channel }}"}'
hx-target="#modals-here"
hx-trigger="click"
class="btn btn-primary"
_="on htmx:afterOnLoad wait 10ms then add .show to #modal then add .show to #modal-backdrop">
Information
</button>
<div id="modals-here"></div>
{% endif %}
</td>
<td>{{ item.channel }}</td> <td>{{ item.channel }}</td>
<td>{{ item.net }}</td> <td>{{ item.net }}</td>
</tr> </tr>

View File

@ -7,6 +7,7 @@ from django.shortcuts import render
from django.views import View from django.views import View
from core.lib.opensearch import initialise_opensearch, run_main_query from core.lib.opensearch import initialise_opensearch, run_main_query
from core.lib.threshold import annotate_online
client = initialise_opensearch() client = initialise_opensearch()
@ -26,7 +27,30 @@ def query_results(request, post_params, api=False):
if "hits" in results.keys(): if "hits" in results.keys():
if "hits" in results["hits"]: if "hits" in results["hits"]:
for item in results["hits"]["hits"]: for item in results["hits"]["hits"]:
results_parsed.append(item["_source"]) element = item["_source"]
element["id"] = item["_id"]
ts = element["ts"]
ts_spl = ts.split("T")
date = ts_spl[0]
time = ts_spl[1]
element["date"] = date
element["time"] = time
results_parsed.append(element)
# Figure out items with net (not discord)
nets = set()
for x in results_parsed:
if "net" in x:
nets.add(x["net"])
# Annotate the online attribute from Threshold
for net in nets:
online_info = annotate_online(
net, [x["nick"] for x in results_parsed if x["src"] == "irc"]
)
for item in results_parsed:
if item["nick"] in online_info:
item["online"] = online_info[item["nick"]]
context = { context = {
"query": query, "query": query,
"results": results_parsed, "results": results_parsed,