its a demo
This commit is contained in:
commit
13b87b97a3
4 changed files with 333 additions and 0 deletions
71
app.py
Normal file
71
app.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
import time
|
||||
import json
|
||||
from random import choice, random
|
||||
from flask import Flask, render_template
|
||||
from flask_sock import Sock
|
||||
|
||||
app = Flask(__name__)
|
||||
sock = Sock(app)
|
||||
|
||||
app.debug = True
|
||||
|
||||
@app.after_request
|
||||
def add_header(r):
|
||||
"""
|
||||
Add headers to both force latest IE rendering engine or Chrome Frame,
|
||||
and also to cache the rendered page for 10 minutes.
|
||||
"""
|
||||
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
r.headers["Pragma"] = "no-cache"
|
||||
r.headers["Expires"] = "0"
|
||||
r.headers['Cache-Control'] = 'public, max-age=0'
|
||||
return r
|
||||
|
||||
def strftime():
|
||||
return time.strftime('%H:%m:%S')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@sock.route('/time')
|
||||
def route_time(sock):
|
||||
while True:
|
||||
sock.send(strftime())
|
||||
time.sleep(1)
|
||||
|
||||
@sock.route('/logs')
|
||||
def route_logs(sock):
|
||||
id = 0;
|
||||
|
||||
sock.send(json.dumps({
|
||||
'id': 'Id',
|
||||
'time': 'Time',
|
||||
'value': 'Value',
|
||||
'message': 'Message'
|
||||
}))
|
||||
|
||||
with open('/usr/share/wordlists/wordlist') as wl:
|
||||
lines = wl.readlines()
|
||||
|
||||
while True:
|
||||
time.sleep(random() * 3)
|
||||
|
||||
message = '{} {} {}'.format(choice(lines), choice(lines), choice(lines))
|
||||
|
||||
sock.send(json.dumps({
|
||||
'id': id,
|
||||
'time': str(strftime()),
|
||||
'value': int(random() * 100),
|
||||
'message': message
|
||||
}))
|
||||
|
||||
id = id + 1
|
||||
|
||||
@sock.route('/ticker')
|
||||
def route_ticker(sock):
|
||||
tick = 0;
|
||||
while True:
|
||||
sock.send(tick);
|
||||
tick = tick + 1
|
||||
time.sleep(0.5)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Flask==3.0.0
|
||||
flask-sock==0.7.0
|
219
static/index.js
Normal file
219
static/index.js
Normal file
|
@ -0,0 +1,219 @@
|
|||
class WSElement extends HTMLElement {
|
||||
static #sockets = {};
|
||||
#delay = 1000;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// track `src` and `autoreconnect` attributes for later use
|
||||
// `autoreconnect` defaults to `true`, unless explicitly set to `false`
|
||||
this.src = this.getAttribute('src');
|
||||
this.autoreconnect = !this.hasAttribute('autoreconnect') || this.getAttribute('autoreconnect').toLowerCase() !== 'false';
|
||||
|
||||
this.#connect_socket(false);
|
||||
}
|
||||
|
||||
#connect_socket(recreate) {
|
||||
var ws;
|
||||
|
||||
// `recreate` is used to handle reconnects, removeing the old websocket object
|
||||
// and recreating a new one
|
||||
if(recreate && WSElement.#sockets[this.src]) {
|
||||
delete WSElement.#sockets[this.src];
|
||||
}
|
||||
|
||||
// a single WebSocket can be reused for multiple elements
|
||||
// check to see if one exists for this `src` else create
|
||||
// a new one.
|
||||
if(!(this.src in WSElement.#sockets)) {
|
||||
ws = WSElement.#newSocket(this.src);
|
||||
WSElement.#sockets[this.src] = ws;
|
||||
} else {
|
||||
ws = WSElement.#sockets[this.src];
|
||||
}
|
||||
|
||||
// setup event handlers for this WebSocket/Elemnent pair
|
||||
ws.addEventListener('error', this.#on_ws_error.bind(this));
|
||||
ws.addEventListener('message', this.#on_ws_message.bind(this));
|
||||
ws.addEventListener('open', this.#on_ws_open.bind(this));
|
||||
ws.addEventListener('close', this.#on_ws_close.bind(this));
|
||||
}
|
||||
|
||||
#on_ws_open(sock) {
|
||||
// set connected attribute for use in CSS
|
||||
// ```
|
||||
// ws-element[connected] {
|
||||
// color: green;
|
||||
// }
|
||||
// ws-element:not(connected) {
|
||||
// color: red;
|
||||
// }
|
||||
// ```
|
||||
this.setAttribute('connected', '');
|
||||
|
||||
// call this.open if defined by subclass
|
||||
if('open' in this) {
|
||||
this.open(sock);
|
||||
}
|
||||
}
|
||||
|
||||
#on_ws_message(sock) {
|
||||
// call this.message, which is always defined
|
||||
this.message(sock.data);
|
||||
}
|
||||
|
||||
#on_ws_close(sock) {
|
||||
this.removeAttribute('connected');
|
||||
|
||||
// clean up event handlers for this element
|
||||
sock.target.removeEventListener('error', this.#on_ws_error.bind(this));
|
||||
sock.target.removeEventListener('message', this.#on_ws_message.bind(this));
|
||||
sock.target.removeEventListener('open', this.#on_ws_open.bind(this));
|
||||
sock.target.removeEventListener('close', this.#on_ws_close.bind(this));
|
||||
|
||||
// call this.close if subclass has defined it
|
||||
if('close' in this) {
|
||||
this.close(sock);
|
||||
}
|
||||
|
||||
// handle autoreconnect and delay throttling
|
||||
if(this.autoreconnect) {
|
||||
setTimeout(this.#connect_socket.call(this, true), this.#delay);
|
||||
if(this.#delay < 10000) {
|
||||
this.#delay += 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#on_ws_error(sock) {
|
||||
// call this.error if subclass has defined it
|
||||
if('error' in this) {
|
||||
this.error(sock);
|
||||
}
|
||||
}
|
||||
|
||||
// default behavior on this.message is just update innerText
|
||||
message(data) {
|
||||
this.innerText = data;
|
||||
}
|
||||
|
||||
// static method for creating new sockets, which are tracked
|
||||
// on the static propert WSElement.#sockets
|
||||
static #newSocket(src) {
|
||||
var s = window.location.protocol.slice(-2) === 'p:' ? '' : 's';
|
||||
var host = window.location.host;
|
||||
var path = src[0] === '/' ? src.slice(1) : src;
|
||||
var wsurl = `ws${s}://${host}/${path}`;
|
||||
|
||||
return new WebSocket(wsurl);
|
||||
}
|
||||
}
|
||||
|
||||
// Example subclass to display a basic 'Time: 00:00:00'
|
||||
class WSTime extends WSElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// insert subelements for the label (Time:) and the value;
|
||||
this.insertAdjacentHTML('beforeend', '<label class="time-label">Time: </label><span class="time-value"></span>');
|
||||
// track the value element for updating later
|
||||
this.value = this.querySelector('span.time-value');
|
||||
}
|
||||
|
||||
// override this.message to update the innerText of the value span element
|
||||
message(data) {
|
||||
this.value.innerText = data;
|
||||
}
|
||||
}
|
||||
|
||||
// Example subclass to parse arbitrary json and build a table
|
||||
// with dynamically controlled limit
|
||||
class WSTable extends WSElement {
|
||||
// observe limit to dynamically update
|
||||
static observedAttributes = [ 'limit' ];
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// ignoreNext is to handle duplicate headers being sent on reconnect
|
||||
this.ignoreNext = false;
|
||||
|
||||
// insert and track table element
|
||||
this.insertAdjacentHTML('beforeend', '<table class="table"></table>');
|
||||
this.table = this.querySelector('table');
|
||||
|
||||
// track limit attribute, defaulting to no limit
|
||||
if(this.hasAttribute('limit')) {
|
||||
this.limit = parseInt(this.getAttribute('limit')) || -1;
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
// update the tracked limit, defaulting to none if no value provided
|
||||
if(oldValue === null) {
|
||||
this.limit = parseInt(newValue) || -1;
|
||||
}
|
||||
}
|
||||
|
||||
message(data) {
|
||||
// ignore one message, because the demo server is a bit dumb
|
||||
if(this.ignoreNext) {
|
||||
this.ignoreNext = false;
|
||||
return;
|
||||
}
|
||||
|
||||
data = JSON.parse(data);
|
||||
|
||||
// with autoheader set the first message will create a header row instead of body
|
||||
if(this.hasAttribute('autoheader') && !('header' in this)) {
|
||||
// createElement works just as well as insertAdjacentHTML for single, to be tracked elements
|
||||
this.thead = document.createElement('thead');
|
||||
this.header = document.createElement('tr');
|
||||
|
||||
this.thead.appendChild(this.header);
|
||||
this.table.appendChild(this.thead);
|
||||
|
||||
// add a `th` for each key/value pair in the json data
|
||||
for(let k in data) {
|
||||
this.header.insertAdjacentHTML('beforeend', `<th scope="col" class="${k}">${data[k]}</th>`);
|
||||
}
|
||||
|
||||
// work is done, this block will be ignored in future messages
|
||||
return;
|
||||
}
|
||||
|
||||
// create the body if it doesn't yet exists
|
||||
if(!('tbody' in this)) {
|
||||
this.tbody = document.createElement('tbody');
|
||||
this.table.appendChild(this.tbody);
|
||||
}
|
||||
|
||||
// create TD for each key/value pair in message data
|
||||
var inner = '';
|
||||
for(let k in data) {
|
||||
inner += `<td class="${k}">${data[k]}</td>`;
|
||||
}
|
||||
// instead the above TDs into a new TR
|
||||
this.tbody.insertAdjacentHTML('beforeend', `<tr>${inner}</tr>`);
|
||||
|
||||
// check limits are enforced
|
||||
if(this.limit > -1 && this.tbody.childElementCount > this.limit) {
|
||||
while(this.tbody.childElementCount > this.limit) {
|
||||
this.tbody.removeChild(this.tbody.childNodes[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ignore the next message if we're disconnected,
|
||||
// demo server always sends headers on connect
|
||||
close(sock) {
|
||||
this.ignoreNext = true;
|
||||
}
|
||||
}
|
||||
|
||||
// define custom elements
|
||||
customElements.define('ws-element', WSElement);
|
||||
customElements.define('ws-time', WSTime);
|
||||
customElements.define('ws-table', WSTable);
|
41
templates/index.html
Normal file
41
templates/index.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<style>
|
||||
ws-time[connected] {
|
||||
color: green;
|
||||
}
|
||||
|
||||
ws-time:not(connected) {
|
||||
color: red;
|
||||
}
|
||||
|
||||
ws-table tr {
|
||||
animation: fadein 1s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="module" src="{{ url_for('static', filename='index.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-xxl">
|
||||
<main class="bd-main">
|
||||
<ws-time src="/time"></ws-time>
|
||||
<div>
|
||||
<ws-element autoreconnect="false" src="/ticker"></ws-element>
|
||||
</div>
|
||||
<ws-table limit="10" autoheader src="/logs"></ws-table>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue