This commit is contained in:
Eugene Pankov 2021-06-16 23:49:44 +02:00
parent 15d1fe4a46
commit 0b55657200
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
17 changed files with 665 additions and 191 deletions

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys

149
poetry.lock generated
View File

@ -136,6 +136,14 @@ twisted = {version = ">=18.7", extras = ["tls"]}
[package.extras]
tests = ["hypothesis (==4.23)", "pytest (>=3.10,<4.0)", "pytest-asyncio (>=0.8,<1.0)"]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "django"
version = "3.2.3"
@ -256,6 +264,19 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "oauthlib"
version = "3.1.1"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
rsa = ["cryptography (>=3.0.0,<4)"]
signals = ["blinker (>=1.4.0)"]
signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "pyasn1"
version = "0.4.8"
@ -299,6 +320,20 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyjwt"
version = "2.1.0"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
crypto = ["cryptography (>=3.3.1,<4.0.0)"]
dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
[[package]]
name = "pyopenssl"
version = "20.0.1"
@ -315,6 +350,21 @@ six = ">=1.5.2"
docs = ["sphinx", "sphinx-rtd-theme"]
test = ["flaky", "pretend", "pytest (>=3.0.1)"]
[[package]]
name = "python3-openid"
version = "3.2.0"
description = "OpenID support for modern servers and consumers."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
defusedxml = "*"
[package.extras]
mysql = ["mysql-connector-python"]
postgresql = ["psycopg2"]
[[package]]
name = "pytz"
version = "2021.1"
@ -323,6 +373,33 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.15.1"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = "*"
[package.extras]
security = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "requests-oauthlib"
version = "1.3.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "service-identity"
version = "21.1.0"
@ -352,6 +429,42 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "social-auth-app-django"
version = "4.0.0"
description = "Python Social Authentication, Django integration."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
six = "*"
social-auth-core = ">=3.3.0"
[[package]]
name = "social-auth-core"
version = "4.1.0"
description = "Python social authentication made simple."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cryptography = ">=1.4"
defusedxml = ">=0.5.0rc1"
oauthlib = ">=1.0.3"
PyJWT = ">=2.0.0"
python3-openid = ">=3.0.10"
requests = ">=2.9.1"
requests-oauthlib = ">=0.6.1"
[package.extras]
all = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"]
allpy3 = ["python-jose (>=3.0.0)", "python3-saml (>=1.2.1)", "cryptography (>=2.1.1)"]
azuread = ["cryptography (>=2.1.1)"]
openidconnect = ["python-jose (>=3.0.0)"]
saml = ["python3-saml (>=1.2.1)"]
[[package]]
name = "sqlparse"
version = "0.4.1"
@ -464,7 +577,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "3922460ced69b5e33ce174cf478542b18bf7bc793d32e262fe46a4b7d9a2a047"
content-hash = "ddf1bd292c78d2b6919ec1a9eb9b76b48ff4d97e7a20a3a46783dcdbf5ec1163"
[metadata.files]
asgiref = [
@ -560,6 +673,10 @@ daphne = [
{file = "daphne-3.0.2-py3-none-any.whl", hash = "sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"},
{file = "daphne-3.0.2.tar.gz", hash = "sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f"},
]
defusedxml = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
django = [
{file = "Django-3.2.3-py3-none-any.whl", hash = "sha256:7e0a1393d18c16b503663752a8b6790880c5084412618990ce8a81cc908b4962"},
{file = "Django-3.2.3.tar.gz", hash = "sha256:13ac78dbfd189532cad8f383a27e58e18b3d33f80009ceb476d7fcbfc5dcebd8"},
@ -599,6 +716,10 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
oauthlib = [
{file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
{file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
@ -641,14 +762,31 @@ pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pyjwt = [
{file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"},
{file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"},
]
pyopenssl = [
{file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"},
{file = "pyOpenSSL-20.0.1.tar.gz", hash = "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51"},
]
python3-openid = [
{file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
{file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
requests = [
{file = "requests-2.15.1-py2.py3-none-any.whl", hash = "sha256:ff753b2196cd18b1bbeddc9dcd5c864056599f7a7d9a4fb5677e723efa2b7fb9"},
{file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"},
]
requests-oauthlib = [
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
service-identity = [
{file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"},
{file = "service_identity-21.1.0-py2.py3-none-any.whl", hash = "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"},
@ -657,6 +795,15 @@ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
social-auth-app-django = [
{file = "social-auth-app-django-4.0.0.tar.gz", hash = "sha256:2c69e57df0b30c9c1823519c5f1992cbe4f3f98fdc7d95c840e091a752708840"},
{file = "social_auth_app_django-4.0.0-py2-none-any.whl", hash = "sha256:df5212370bd250108987c4748419a1a1d0cec750878856c2644c36aaa0fd3e58"},
{file = "social_auth_app_django-4.0.0-py3-none-any.whl", hash = "sha256:567ad0e028311541d7dfed51d3bf2c60440a6fd236d5d4d06c5a618b3d6c57c5"},
]
social-auth-core = [
{file = "social-auth-core-4.1.0.tar.gz", hash = "sha256:5ab43b3b15dce5f059db69cc3082c216574739f0edbc98629c8c6e8769c67eb4"},
{file = "social_auth_core-4.1.0-py3-none-any.whl", hash = "sha256:983b53167ac56e7ba4909db555602a6e7a98c97ca47183bb222eb85ba627bf2b"},
]
sqlparse = [
{file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"},
{file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"},

View File

@ -11,6 +11,7 @@ django-rest-framework = "^0.1.0"
channels = "^3.0.3"
uvloop = "^0.15.2"
djangorestframework-dataclasses = "^0.9"
social-auth-app-django = "^4.0.0"
[tool.poetry.dev-dependencies]
flake8 = "^3.9.2"

View File

@ -5,6 +5,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
import { AppComponent } from './components/app.component'
import { MainComponent } from './components/main.component';
@NgModule({
imports: [
@ -17,6 +18,7 @@ import { AppComponent } from './components/app.component'
],
declarations: [
AppComponent,
MainComponent,
],
bootstrap: [AppComponent]
})

View File

@ -1,55 +1,2 @@
.sidebar
img.logo(src='{{_logo}}')
div(ngbDropdown, placement='bottom-right')
button.btn.btn-secondary(ngbDropdownToggle)
fa-icon([icon]='_cogIcon', [fixedWidth]='true')
.config-menu(ngbDropdownMenu)
.header(*ngIf='getActiveConfig()')
.dropdown-header Active config
.title {{getActiveConfig().modified_at}}
div(*ngIf='activeVersion')
div App version:
div(ngbDropdown)
button.btn.btn-secondary(ngbDropdownToggle) {{activeVersion.version}}
div(ngbDropdownMenu)
a(
*ngFor='let version of versions',
ngbDropdownItem,
[class.active]='version == activeVersion',
(click)='selectVersion(version)'
) {{version.version}}
.btn-toolbar
button.btn.btn-light.w-50((click)='duplicateConfig()')
fa-icon([icon]='_copyIcon', [fixedWidth]='true')
span Duplicate
button.btn.btn-light.w-50((click)='deleteConfig()')
fa-icon([icon]='_deleteIcon', [fixedWidth]='true')
span Delete
div(*ngIf='configs.length > 1')
.dropdown-header All configs
ng-container(*ngFor='let config of configs')
a(
*ngIf='config !== getActiveConfig()',
ngbDropdownItem,
(click)='selectConfig(config)'
) Config modified at {{config.modified_at}}
button.btn.btn-light.w-100((click)='createNewConfig()')
fa-icon([icon]='_addIcon', [fixedWidth]='true')
span New config
div(ngbDropdown, placement='bottom-right')
button.btn.btn-secondary(ngbDropdownToggle)
fa-icon([icon]='_userIcon', [fixedWidth]='true')
div(ngbDropdownMenu)
a(ngbDropdownItem, (click)='logout()') Logout
.terminal([hidden]='!activeVersion')
iframe(#iframe)
main(*ngIf='ready && user')
login(*ngIf='ready && !user')

View File

@ -1,41 +0,0 @@
:host {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
}
.sidebar {
width: 64px;
flex: none;
.logo {
width: 64px;
height: 64px;
}
}
.terminal {
flex: 1 1 0;
overflow: hidden;
position: relative;
}
iframe {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: none;
}
.config-menu {
.header {
border-bottom: 1px solid black;
}
}

View File

@ -1,9 +1,5 @@
import * as semverGT from 'semver/functions/gt'
import { Component, ElementRef, ViewChild } from '@angular/core'
import { Component } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { AppConnectorService } from '../services/appConnector.service'
import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons'
@Component({
selector: 'app',
@ -11,95 +7,18 @@ import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid-
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
_logo = require('../assets/logo.svg')
_cogIcon = faCog
_userIcon = faUser
_copyIcon = faCopy
_addIcon = faPlus
_deleteIcon = faTrash
configs: any[] = []
versions: any[] = []
activeVersion?: any
@ViewChild('iframe') iframe: ElementRef
user: any
ready = false
constructor (
private appConnector: AppConnectorService,
private http: HttpClient,
) {
window.addEventListener('message', event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
}
})
}
) { }
async ngAfterViewInit () {
this.configs = await this.http.get('/api/1/configs').toPromise()
this.versions = await this.http.get('/api/1/versions').toPromise()
this.versions.sort((a, b) => semverGT(a, b))
if (!this.configs.length) {
await this.createNewConfig()
async ngOnInit () {
const user = await this.http.get('/api/1/user').toPromise()
if (user.id) {
this.user = user
}
this.selectConfig(this.configs[0])
}
async createNewConfig () {
this.configs.push(await this.http.post('/api/1/configs', {
content: '{}',
last_used_with_version: this.versions[0].version,
}).toPromise())
}
async duplicateConfig () {
const copy = {...this.appConnector.config, pk: undefined}
this.configs.push(await this.http.post('/api/1/configs', copy).toPromise())
}
unloadApp () {
delete this.activeVersion
this.iframe.nativeElement.src = 'about:blank'
}
loadApp (version) {
this.iframe.nativeElement.src = `/terminal?${version.version}`
this.activeVersion = version
}
getActiveConfig () {
return this.appConnector.config
}
selectVersion (version: any) {
// TODO check config incompatibility
this.unloadApp()
setTimeout(() => {
this.loadApp(version)
})
}
async selectConfig (config: any) {
let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
if (!matchingVersion) {
// TODO ask to upgrade
matchingVersion = this.versions[0]
}
this.appConnector.config = config
const result = await this.http.patch(`/api/1/configs/${config.id}`, {
last_used_with_version: matchingVersion.version,
}).toPromise()
Object.assign(config, result)
this.selectVersion(matchingVersion)
}
async logout () {
await this.http.post('/api/1/auth/logout', null).toPromise()
this.ready = true
}
}

View File

@ -0,0 +1,57 @@
.sidebar
img.logo(src='{{_logo}}')
div(ngbDropdown, placement='bottom-right')
button.btn(ngbDropdownToggle)
fa-icon([icon]='_cogIcon', [fixedWidth]='true')
.config-menu(ngbDropdownMenu)
.header(*ngIf='getActiveConfig()')
.d-flex.align-items-center.py-2.px-4
.me-auto
label Active config
.title {{getActiveConfig().modified_at}}
button.btn.btn-semi.me-2((click)='duplicateConfig()')
fa-icon([icon]='_copyIcon', [fixedWidth]='true')
button.btn.btn-semi((click)='deleteConfig()')
fa-icon([icon]='_deleteIcon', [fixedWidth]='true')
.d-flex.align-items-center.py-2.px-4(*ngIf='activeVersion')
.me-auto App version:
div(ngbDropdown)
button.btn.btn-semi(ngbDropdownToggle) {{activeVersion.version}}
div(ngbDropdownMenu)
a(
*ngFor='let version of versions',
ngbDropdownItem,
[class.active]='version == activeVersion',
(click)='selectVersion(version)'
) {{version.version}}
div(*ngIf='configs.length > 1')
.dropdown-header All configs
ng-container(*ngFor='let config of configs')
a(
*ngIf='config !== getActiveConfig()',
ngbDropdownItem,
(click)='selectConfig(config)',
href='#'
) Config modified at {{config.modified_at}}
.p-3
button.btn.btn-semi.w-100((click)='createNewConfig()')
fa-icon([icon]='_addIcon', [fixedWidth]='true')
span New config
div(ngbDropdown, placement='bottom-right')
button.btn(ngbDropdownToggle)
fa-icon([icon]='_userIcon', [fixedWidth]='true')
div(ngbDropdownMenu)
a(ngbDropdownItem, (click)='logout()', href='#') Logout
.terminal([hidden]='!activeVersion')
iframe(#iframe)

View File

@ -0,0 +1,61 @@
@import "../theme/vars";
:host {
position: absolute;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
}
.sidebar {
width: 64px;
flex: none;
display: flex;
flex-direction: column;
align-items: stretch;
.logo {
width: 32px;
height: 32px;
align-self: center;
margin-top: 15px;
margin-bottom: 20px;
}
>button, >[ngbdropdown] > button {
width: 64px;
height: 64px;
background: transparent;
box-shadow: none;
&::after {
display: none;
}
}
}
.terminal {
flex: 1 1 0;
overflow: hidden;
position: relative;
}
iframe {
background: $body-bg;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: none;
}
.config-menu {
.header {
border-bottom: 1px solid black;
}
}

View File

@ -0,0 +1,106 @@
import * as semverGT from 'semver/functions/gt'
import { Component, ElementRef, ViewChild } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { AppConnectorService } from '../services/appConnector.service'
import { faCog, faUser, faCopy, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons'
@Component({
selector: 'main',
templateUrl: './main.component.pug',
styleUrls: ['./main.component.scss'],
})
export class MainComponent {
_logo = require('../assets/logo.svg')
_cogIcon = faCog
_userIcon = faUser
_copyIcon = faCopy
_addIcon = faPlus
_deleteIcon = faTrash
configs: any[] = []
versions: any[] = []
activeVersion?: any
@ViewChild('iframe') iframe: ElementRef
constructor (
private appConnector: AppConnectorService,
private http: HttpClient,
) {
window.addEventListener('message', event => {
if (event.data === 'request-connector') {
this.iframe.nativeElement.contentWindow['__connector__'] = this.appConnector
this.iframe.nativeElement.contentWindow.postMessage('connector-ready', '*')
}
})
}
async ngAfterViewInit () {
this.configs = await this.http.get('/api/1/configs').toPromise()
this.versions = await this.http.get('/api/1/versions').toPromise()
this.versions.sort((a, b) => semverGT(a, b))
if (!this.configs.length) {
await this.createNewConfig()
}
this.selectConfig(this.configs[0])
}
async createNewConfig () {
this.configs.push(await this.http.post('/api/1/configs', {
content: '{}',
last_used_with_version: this.versions[0].version,
}).toPromise())
}
async duplicateConfig () {
const copy = {...this.appConnector.config, pk: undefined}
this.configs.push(await this.http.post('/api/1/configs', copy).toPromise())
}
unloadApp () {
delete this.activeVersion
this.iframe.nativeElement.src = 'about:blank'
}
loadApp (version) {
this.iframe.nativeElement.src = `/terminal?${version.version}`
this.activeVersion = version
}
getActiveConfig () {
return this.appConnector.config
}
selectVersion (version: any) {
// TODO check config incompatibility
this.unloadApp()
setTimeout(() => {
this.loadApp(version)
})
}
async selectConfig (config: any) {
let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
if (!matchingVersion) {
// TODO ask to upgrade
matchingVersion = this.versions[0]
}
this.appConnector.config = config
const result = await this.http.patch(`/api/1/configs/${config.id}`, {
last_used_with_version: matchingVersion.version,
}).toPromise()
Object.assign(config, result)
this.selectVersion(matchingVersion)
}
async logout () {
await this.http.post('/api/1/auth/logout', null).toPromise()
location.href = '/'
}
}

View File

@ -6,4 +6,4 @@ body {
overscroll-behavior: none;
}
@import "node_modules/bootstrap/scss/bootstrap";
@import "./theme/index.scss"

64
src/theme/index.scss Normal file
View File

@ -0,0 +1,64 @@
@import "vars";
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/utilities";
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
@import "~bootstrap/scss/type";
// @import "~bootstrap/scss/images";
@import "~bootstrap/scss/containers";
@import "~bootstrap/scss/grid";
// @import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
// @import "~bootstrap/scss/nav";
// @import "~bootstrap/scss/navbar";
// @import "~bootstrap/scss/card";
// @import "~bootstrap/scss/accordion";
// @import "~bootstrap/scss/breadcrum/b";
// @import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
// @import "~bootstrap/scss/alert";
// @import "~bootstrap/scss/progress";
@import "~bootstrap/scss/list-group";
// @import "~bootstrap/scss/close";
// @import "~bootstrap/scss/toasts";
// @import "~bootstrap/scss/modal";
// @import "~bootstrap/scss/tooltip";
// @import "~bootstrap/scss/popover";
// @import "~bootstrap/scss/carousel";
// @import "~bootstrap/scss/spinners";
// @import "~bootstrap/scss/offcanvas";
// Helpers
@import "~bootstrap/scss/helpers";
@import "~bootstrap/scss/utilities/api";
::-webkit-scrollbar-track
{
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
background-color: $gray-900;
}
::-webkit-scrollbar
{
height: 6px;
width: 6px;
background-color: #F5F5F5;
}
::-webkit-scrollbar-thumb
{
background-color: $gray-700;
}
.dropdown-menu {
box-shadow: $dropdown-box-shadow;
}

187
src/theme/vars.scss Normal file
View File

@ -0,0 +1,187 @@
$white: #fff;
$gray-100: #f8f9fa;
$gray-200: #e9ecef;
$gray-300: #dee2e6;
$gray-400: #ced4da;
$gray-500: #adb5bd;
$gray-600: #6c757d;
$gray-700: #495057;
$gray-800: #343a40;
$gray-900: #212529;
$black: #000;
$red: #d9534f !default;
$orange: #f0ad4e !default;
$yellow: #ffd500 !default;
$green: #5cb85c !default;
$blue: #0275d8 !default;
$teal: #5bc0de !default;
$pink: #ff5b77 !default;
$purple: #613d7c !default;
$semi: rgba(0,0,0, .5);
@import "~bootstrap/scss/functions";
$table-bg: rgba(255,255,255,.05);
$table-bg-hover: rgba(255,255,255,.1);
$table-border-color: rgba(255,255,255,.1);
$theme-colors: (
primary: $blue,
secondary: #38434e,
success: $green,
info: $blue,
warning: $orange,
danger: $red,
light: $gray-300,
dark: #0e151d,
rare: $purple,
semi: $semi
);
$body-color: #ccc;
$body-bg: #0c131b;
$font-family-sans-serif: "Source Sans Pro";
$font-family-monospace: "Source Code Pro";
$font-size-base: 14rem / 16;
$font-size-lg: 1.28rem;
$font-size-sm: .85rem;
$line-height-base: 1.6;
$border-radius: .4rem;
$border-radius-lg: .6rem;
$border-radius-sm: .2rem;
$box-shadow: 0 .5rem 1rem rgba($black, .5) !default;
// -----
$headings-color: #ced9e2;
$headings-font-weight: lighter;
$input-btn-padding-y: .3rem;
$input-btn-padding-x: .9rem;
$input-btn-line-height: 1.6;
$input-btn-line-height-sm: 1.8;
$input-btn-line-height-lg: 1.8;
$btn-focus-width: 1px;
$h4-font-size: 18px;
$link-color: $gray-400;
$link-hover-color: $white;
$link-hover-decoration: none;
$component-active-color: $white;
$component-active-bg: #2f3a42;
$list-group-bg: $table-bg;
$list-group-border-color: $table-border-color;
$list-group-item-padding-y: 0.8rem;
$list-group-item-padding-x: 1rem;
$list-group-hover-bg: $table-bg-hover;
$list-group-active-bg: rgba(255,255,255,.2);
$list-group-active-color: $component-active-color;
$list-group-active-border-color: translate;
$list-group-action-color: $body-color;
$list-group-action-hover-color: white;
$list-group-action-active-color: $component-active-color;
$list-group-action-active-bg: $list-group-active-bg;
$alert-padding-y: 0.9rem;
$alert-padding-x: 1.25rem;
$transition-base: all .15s ease-in-out;
$transition-fade: opacity .1s linear;
$transition-collapse: height .35s ease;
$btn-transition: all .15s ease-in-out;
$popover-bg: $body-bg;
$popover-body-color: $body-color;
$popover-header-bg: $table-bg-hover;
$popover-header-color: $headings-color;
$popover-arrow-color: $popover-bg;
$popover-max-width: 360px;
$btn-border-width: 2px;
$input-bg: #181e23;
$input-disabled-bg: #2e3235;
$input-color: #ddd;
$input-border-color: $input-bg;
$input-border-width: 2px;
$input-focus-bg: $input-bg;
$input-focus-border-color: rgba(171, 171, 171, 0.61);
$input-focus-color: $input-color;
$input-group-addon-color: $input-color;
$input-group-addon-bg: $input-bg;
$input-group-addon-border-color: transparent;
$input-group-btn-border-color: $input-bg;
$nav-tabs-border-radius: 0;
$nav-tabs-border-color: transparent;
$nav-tabs-border-width: 2px;
$nav-tabs-link-hover-border-color: transparent;
$nav-tabs-link-active-color: #eee;
$nav-tabs-link-active-bg: transparent;
$nav-tabs-link-active-border-color: #eee;
$navbar-padding-y: 0;
$navbar-padding-x: 0;
$dropdown-bg: $body-bg;
$dropdown-color: $body-color;
$dropdown-border-width: 1px;
$dropdown-header-color: $gray-500;
$dropdown-link-color: $body-color;
$dropdown-link-hover-color: #eee;
$dropdown-link-hover-bg: rgba(255,255,255,.04);
$dropdown-link-active-color: white;
$dropdown-link-active-bg: rgba(0, 0, 0, .2);
$dropdown-item-padding-y: 0.5rem;
$dropdown-item-padding-x: 1.5rem;
$code-color: $orange;
$code-bg: rgba(0, 0, 0, .25);
$code-padding-y: 3px;
$code-padding-x: 5px;
$pre-bg: $dropdown-bg;
$pre-color: $dropdown-link-color;
$badge-font-size: 0.75rem;
$badge-font-weight: bold;
$badge-padding-y: 4px;
$badge-padding-x: 6px;
$custom-control-indicator-size: 1.2rem;
$custom-control-indicator-bg: $body-bg;
$custom-control-indicator-border-color: lighten($body-bg, 25%);
$custom-control-indicator-checked-bg: theme-color("primary");
$custom-control-indicator-checked-color: $body-bg;
$custom-control-indicator-checked-border-color: transparent;
$custom-control-indicator-active-bg: rgba(255, 255, 0, 0.5);
$modal-content-bg: $body-bg;
$modal-content-border-color: $body-bg;
$modal-header-border-width: 0;
$modal-footer-border-color: #222;
$modal-footer-border-width: 1px;
$modal-content-border-width: 0;
$progress-bg: $table-bg;
$progress-height: 3px;

View File

@ -2,15 +2,16 @@ import os
from dataclasses import dataclass
from django.conf import settings
from django.contrib.auth import logout
from rest_framework import fields
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import ModelSerializer, Field
from rest_framework_dataclasses.serializers import DataclassSerializer
from .models import Config
from .models import Config, User
@dataclass
@ -61,6 +62,26 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet):
).data)
class UserSerializer(ModelSerializer):
id = fields.IntegerField()
class Meta:
model = User
fields = ('id', 'username', 'active_config')
read_only_fields = ('id', 'username')
class UserViewSet(RetrieveModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
def get_object(self):
if self.request.user.is_authenticated:
return self.request.user
return None
class LogoutView(APIView):
def post(self, request, format=None):
logout(request)
return Response(None)

View File

@ -12,6 +12,7 @@ router.register('api/1/versions', api.AppVersionViewSet, basename='app-versions'
urlpatterns = [
path('api/1/auth/logout', api.LogoutView.as_view()),
path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve'})),
path('', views.IndexView.as_view()),
path('terminal', views.TerminalView.as_view()),

View File

@ -39,6 +39,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'channels',
'rest_framework',
'social_django',
'terminus.app',
]

View File

@ -19,5 +19,6 @@ from .app.urls import urlpatterns
urlpatterns = [
path('', include(urlpatterns)),
path('', include('social_django.urls', namespace='social')),
path('admin/', admin.site.urls),
]