From cce8c81e29e96c7e707bac5f0f4e9841fc7efe08 Mon Sep 17 00:00:00 2001 From: "Gleb O. Ivaniczkij" Date: Thu, 15 Aug 2024 00:57:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20WebSocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nginx/main.conf | 7 + requirements.txt | 1 + static/scripts/vendor.js | 127 ++++++++++---- static/styles/dark-theme.css | 32 ++++ static/styles/light-theme.css | 32 ++++ static/styles/main.css | 309 +++------------------------------- templates/main.jinja2 | 59 ++++++- web/main.py | 51 ++++++ 8 files changed, 291 insertions(+), 327 deletions(-) create mode 100644 static/styles/dark-theme.css create mode 100644 static/styles/light-theme.css diff --git a/nginx/main.conf b/nginx/main.conf index 87f17e5..10075ed 100644 --- a/nginx/main.conf +++ b/nginx/main.conf @@ -17,6 +17,13 @@ server { proxy_pass http://cit.csasq.ru; } + location /ws { + include /opt/cit-is-bot/nginx/secure-headers.conf; + include ./conf.d/ws-proxy.conf; + + proxy_pass http://cit.csasq.ru; + } + location /static/ { include /opt/cit-is-bot/nginx/secure-headers.conf; include ./conf.d/gzip.conf; diff --git a/requirements.txt b/requirements.txt index 9c0d153..25cf5b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ psycopg[binary] pydantic redis uvicorn +websockets diff --git a/static/scripts/vendor.js b/static/scripts/vendor.js index 87d0da0..d2f7a12 100644 --- a/static/scripts/vendor.js +++ b/static/scripts/vendor.js @@ -1,42 +1,97 @@ -class Menu { - #application; - #root; - #items; +// class Menu { +// #application; +// #root; +// #items; +// +// constructor(application) { +// this.#application = application; +// this.#root = document.querySelector('#menu'); +// this.#items = this.#root.querySelectorAll('input[data-page]'); +// this.#items.forEach(item => { +// item.addEventListener('click', () => this.#application.open(item.dataset.page)); +// }); +// }; +// +// setActive(pageId) { +// this.#items.forEach(item => item.classList.remove('active')); +// this.#items.forEach(item => item.dataset.page === pageId ? item.classList.add('active') : null); +// }; +// } +// +// class Application { +// static DEFAULT_PAGE = 'dashboard'; +// +// #inputContainer; +// #outputContainer; +// #menu; +// #pageId; +// +// constructor() { +// this.#inputContainer = document.querySelector('#input'); +// this.#outputContainer = document.querySelector('#output'); +// this.#menu = new Menu(this); +// }; +// +// open(pageId) { +// this.#pageId = pageId; +// document.querySelectorAll('#input [data-page]').forEach(page => page.classList.remove('active')); +// document.querySelector(`#input [data-page="${pageId}"]`).classList.add('active'); +// document.querySelectorAll('#output [data-page]').forEach(page => page.classList.remove('active')); +// this.#menu.setActive(pageId); +// }; +// } - constructor(application) { - this.#application = application; - this.#root = document.querySelector('#menu'); - this.#items = this.#root.querySelectorAll('input[data-page]'); - this.#items.forEach(item => { - item.addEventListener('click', () => this.#application.open(item.dataset.page)); - }); - }; +// const mutableStateOf = { +// _value: null, +// constructor(value) { +// this._value = value +// }, +// get value() { +// return this._value +// }, +// set value(value) { +// this._value = value +// } +// } - setActive(pageId) { - this.#items.forEach(item => item.classList.remove('active')); - this.#items.forEach(item => item.dataset.page === pageId ? item.classList.add('active') : null); - }; +const connectWebSocket = (url) => { + const ws = new WebSocket(url) + + ws.onopen = () => { + + } + + ws.onmessage = (event) => { + const data = JSON.parse(event.data) + document.querySelectorAll(`#${data.id}`).forEach(element => element.toggleAttribute('selected', data.value)) + } + + ws.onclose = () => { + + } + + return ws } -class Application { - static DEFAULT_PAGE = 'dashboard'; +const wsUrl = new URL(location) +wsUrl.protocol = 'wss' +wsUrl.pathname = '/ws' +const ws = connectWebSocket(wsUrl) - #inputContainer; - #outputContainer; - #menu; - #pageId; - - constructor() { - this.#inputContainer = document.querySelector('#input'); - this.#outputContainer = document.querySelector('#output'); - this.#menu = new Menu(this); - }; - - open(pageId) { - this.#pageId = pageId; - document.querySelectorAll('#input [data-page]').forEach(page => page.classList.remove('active')); - document.querySelector(`#input [data-page="${pageId}"]`).classList.add('active'); - document.querySelectorAll('#output [data-page]').forEach(page => page.classList.remove('active')); - this.#menu.setActive(pageId); - }; +function getSwitch(id) { + const element = document.createElement('md-switch') + element.id = id + element.onclick = (event) => { + event.preventDefault() + const data = JSON.stringify({ + id: id, + value: !event.currentTarget.hasAttribute('selected'), + }) + ws.send(data) + } + return element } + +document.addEventListener('DOMContentLoaded', () => { + document.body.querySelector('main').append(getSwitch('qwerty')) +}) diff --git a/static/styles/dark-theme.css b/static/styles/dark-theme.css new file mode 100644 index 0000000..9c823da --- /dev/null +++ b/static/styles/dark-theme.css @@ -0,0 +1,32 @@ +:root { + --md-sys-color-primary: #D0BCFF; + --md-sys-color-on-primary: #381E72; + --md-sys-color-primary-container: #4F378B; + --md-sys-color-on-primary-container: #EADDFF; + --md-sys-color-secondary: #CCC2DC; + --md-sys-color-on-secondary: #332D41; + --md-sys-color-secondary-container: #4A4458; + --md-sys-color-on-secondary-container: #E8DEF8; + --md-sys-color-tertiary: #EFB8C8; + --md-sys-color-on-tertiary: #492532; + --md-sys-color-tertiary-container: #633B48; + --md-sys-color-on-tertiary-container: #FFD8E4; + --md-sys-color-error: #F2B8B5; + --md-sys-color-on-error: #601410; + --md-sys-color-error-container: #8C1D18; + --md-sys-color-on-error-container: #F9DEDC; + --md-sys-color-surface: #141218; + --md-sys-color-on-surface: #E6E0E9; + --md-sys-color-surface-variant: #49454F; + --md-sys-color-on-surface-variant: #CAC4D0; + --md-sys-color-surface-container-highest: #36343B; + --md-sys-color-surface-container-high: #2B2930; + --md-sys-color-surface-container: #211F26; + --md-sys-color-surface-container-low: #1D1B20; + --md-sys-color-surface-container-lowest: #0F0D13; + --md-sys-color-inverse-surface: #E6E0E9; + --md-sys-color-inverse-on-surface: #322F35; + --md-sys-color-surface-tint: #D0BCFF; + --md-sys-color-outline: #938F99; + --md-sys-color-outline-variant: #49454F; +} diff --git a/static/styles/light-theme.css b/static/styles/light-theme.css new file mode 100644 index 0000000..c607745 --- /dev/null +++ b/static/styles/light-theme.css @@ -0,0 +1,32 @@ +:root { + --md-sys-color-primary: #6750A4; + --md-sys-color-on-primary: #FFFFFF; + --md-sys-color-primary-container: #EADDFF; + --md-sys-color-on-primary-container: #4F378B; + --md-sys-color-secondary: #625B71; + --md-sys-color-on-secondary: #FFFFFF; + --md-sys-color-secondary-container: #E8DEF8; + --md-sys-color-on-secondary-container: #4A4458; + --md-sys-color-tertiary: #7D5260; + --md-sys-color-on-tertiary: #FFFFFF; + --md-sys-color-tertiary-container: #FFD8E4; + --md-sys-color-on-tertiary-container: #633B48; + --md-sys-color-error: #B3261E; + --md-sys-color-on-error: #FFFFFF; + --md-sys-color-error-container: #F9DEDC; + --md-sys-color-on-error-container: #8C1D18; + --md-sys-color-surface: #FEF7FF; + --md-sys-color-on-surface: #1D1B20; + --md-sys-color-surface-variant: #E7E0EC; + --md-sys-color-on-surface-variant: #49454F; + --md-sys-color-surface-container-highest: #E6E0E9; + --md-sys-color-surface-container-high: #ECE6F0; + --md-sys-color-surface-container: #F3EDF7; + --md-sys-color-surface-container-low: #F7F2FA; + --md-sys-color-surface-container-lowest: #FFFFFF; + --md-sys-color-inverse-surface: #322F35; + --md-sys-color-inverse-on-surface: #F5EFF7; + --md-sys-color-surface-tint: #6750A4; + --md-sys-color-outline: #79747E; + --md-sys-color-outline-variant: #CAC4D0; +} diff --git a/static/styles/main.css b/static/styles/main.css index 348a882..31423de 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -1,307 +1,48 @@ -@font-face { - font-family: 'Exo 2'; - font-style: normal; - font-weight: 100 900; - src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic-ext.woff2') format('woff2'); - unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; -} - -@font-face { - font-family: 'Exo 2'; - font-style: normal; - font-weight: 100 900; - src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic.woff2') format('woff2'); - unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; -} - -@font-face { - font-family: 'Exo 2'; - font-style: normal; - font-weight: 100 900; - src: url('https://cdn.csasq.ru/common/fonts/exo-2/vietnamese.woff2') format('woff2'); - unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; -} - -@font-face { - font-family: 'Exo 2'; - font-style: normal; - font-weight: 100 900; - src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin-ext.woff2') format('woff2'); - unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; -} - -@font-face { - font-family: 'Exo 2'; - font-style: normal; - font-weight: 100 900; - src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin.woff2') format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -:root { - --color-1: #2E436E; - --color-2: #1E1F22; - --color-3: #2B2D30; - --color-4: #43454A; - --color-5: #575757; - --color-6: #CED0D6; - --color-7: #569ACC; - --color-8: #3574F0; - --color-9: #528D58; - --color-10: #C94F4F; -} - -html, -input, -button, -textarea, -select { - font-family: 'Exo 2', sans-serif; -} - -html, -body { - height: 100%; - color: var(--color-6); - background-color: var(--color-2); - color-scheme: dark light; -} - body { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; display: flex; flex-direction: column; - margin: 0; -} - -input[type="text"], -input[type="number"] { - color: var(--color-6); -} - -a { - color: var(--color-7); - text-decoration: none; - outline: none; -} - -a:hover, -a:focus { - text-decoration: underline; -} - -.hidden { - display: none; -} - -#top-bar { - display: flex; justify-content: space-between; - align-items: center; - padding: .5rem; - background-color: var(--color-3); - border-bottom: solid 2px var(--color-2); + margin: 0; + background-color: var(--md-sys-color-surface-container-high); + scroll-behavior: smooth; } -#logo { - display: flex; - align-items: center; +main { + padding: 1rem; } -#icon { - width: 1.5rem; - height: 1.5rem; +main > * { + width: 100%; } -#title { - margin-left: .5rem; - font-size: 1.2rem; - font-weight: bold; -} - -#actions { - display: flex; - align-items: center; -} - -#actions button { - display: flex; - justify-content: center; - align-items: center; - padding: .4rem .6rem; - color: var(--color-6); - background: none; - border: none; - border-radius: .25rem; - outline: none; - font-size: 1rem; - cursor: pointer; -} - -#actions button:hover, -#actions button:focus { - background: var(--color-4); -} - -#actions .icon { - width: 1.2rem; - height: 1.2rem; -} - -#content { - position: sticky; - display: flex; - height: 100%; - overflow-y: auto; -} - -#menu { +nav { display: flex; flex-direction: column; - justify-content: stretch; - min-width: 12rem; - max-width: 16rem; - background-color: var(--color-3); + background-color: var(--md-sys-color-surface-container); } -#menu .header { - padding: .75rem; - font-weight: bold; -} - -#menu .item { +nav ul { display: flex; - align-items: stretch; -} - -#menu input[type="button"] { - padding: .5rem .75rem; - width: 100%; - color: var(--color-6); - background: none; - border: none; - outline: none; - white-space: pre-wrap; - text-align: start; - cursor: pointer; -} - -#menu input[type="button"]:hover, -#menu input[type="button"]:focus { - color: white; -} - -#menu input[type="button"].active { - color: white; -} - -#menu .marker { - width: 3px; - background: none; - border-radius: 3px; -} - -#menu input[type="button"].active + .marker { - background: var(--color-8); -} - -#main { - position: sticky; - display: flex; - width: 100%; - overflow: auto; -} - -#input { - flex: 1 1 auto; - padding: .75rem; - min-width: 24rem; - overflow: auto; -} - -#input [data-page]:not(.active), -#output [data-page]:not(.active) { - display: none; -} - -#main input { - width: 10rem; - background: none; - border: none; - outline: solid 1px var(--color-5); -} - -#main input:focus { - outline: solid 2px var(--color-8); -} - -#main input[type="button"] { - padding: .25rem 1.5rem; - width: 100%; - color: white; - border-radius: .25rem; - outline: none; - cursor: pointer; -} - -#main input[type="button"].primary { - padding: .25rem 1.5rem; - background: var(--color-8); - border: solid 1px var(--color-2); -} - -#main input[type="button"].secondary { - padding: .25rem 1.5rem; - background: none; - border: solid 1px var(--color-4); -} - -#main input[type="button"]:focus { - outline: solid 2px var(--color-8); -} - -#output { - flex: 1 1 auto; - padding: .75rem; - border-left: solid 1px var(--color-3); - overflow: auto; -} - -#bottom-bar { - display: flex; - justify-content: end; align-items: center; + gap: 1rem; + margin: 0; padding: .5rem; - background-color: var(--color-3); - border-top: solid .15rem var(--color-2); - font-size: .8rem; + list-style: none; } -table.properties th { - font-weight: normal; - text-align: start; -} - -table.properties th[colspan="2"] { - padding: 1rem 0 .5rem 0; - font-weight: bold; -} - -table.properties td:nth-child(2) { - padding-left: 1rem; -} - -table.properties td[colspan="2"] { - padding: .5rem 0 .5rem 0; -} - -table.properties td[colspan="2"] > div { +nav li { display: flex; + flex-direction: column; + width: 100%; + color: var(--md-sys-color-on-surface); + text-align: center; } -table.properties td[colspan="2"] > div > :not(:first-child) { - margin-left: .5rem; -} - -table.matrix input { - width: 3rem !important; +nav md-icon-button { + width: 100%; } diff --git a/templates/main.jinja2 b/templates/main.jinja2 index 8fecc97..4771643 100644 --- a/templates/main.jinja2 +++ b/templates/main.jinja2 @@ -8,6 +8,8 @@ Настройки + + - - + + + - +
+ + + + + +
+ diff --git a/web/main.py b/web/main.py index 51d994d..af6c018 100644 --- a/web/main.py +++ b/web/main.py @@ -1,12 +1,49 @@ +import asyncio import os from fastapi import FastAPI from fastapi.responses import HTMLResponse +from fastapi.websockets import WebSocket, WebSocketDisconnect from jinja2 import Environment, FileSystemLoader import config +class ConnectionManager: + connections: list[WebSocket] + + class StateError(Exception): + pass + + def __init__( + self, + ): + self.connections = [] + + async def connect( + self, + websocket: WebSocket, + ): + await websocket.accept() + self.connections.append(websocket) + + def disconnect( + self, + websocket: WebSocket, + ): + self.connections.remove(websocket) + + async def broadcast( + self, + data: dict, + ): + for connection in self.connections: + asyncio.ensure_future(connection.send_json(data)) + + +connection_manager = ConnectionManager() + + app = FastAPI( title=config.Main.app_name, ) @@ -33,3 +70,17 @@ async def _(): title='Дашборд', ), ) + + +@app.websocket( + path='/ws', +) +async def _( + websocket: WebSocket, +): + await connection_manager.connect(websocket) + try: + while True: + await connection_manager.broadcast(await websocket.receive_json()) + except WebSocketDisconnect: + connection_manager.disconnect(websocket)