35 Commits

Author SHA1 Message Date
Natan Keddem 3b13ca89bd update to nicegui 1.4.2 2023-11-15 18:38:14 -05:00
Natan Keddem 6e7ebd4c25 make the http pipe dialog wider 2023-11-14 20:33:59 -05:00
Natan Keddem b87a3d79b5 change drawer styling 2023-11-13 22:20:55 -05:00
Natan Keddem f2119ea762 change default load timer 2023-11-13 22:19:38 -05:00
Natan Keddem 6acef6c0a5 refactor page element style 2023-11-13 22:15:36 -05:00
Natan Keddem 1b1cccdc4d change to new shade of orange 2023-11-13 22:14:23 -05:00
Natan Keddem 8808f66f5e reduce result terminal rows 2023-11-13 22:13:53 -05:00
Natan Keddem d3199fa0ad remove notify from zfs queries 2023-11-13 22:13:28 -05:00
Natan Keddem 489c0607b6 added expansion function to drawer 2023-11-12 21:21:26 -05:00
Natan Keddem 9120fcf64f fixed renaming of host 2023-11-12 21:21:26 -05:00
Natan Keddem 40eb701e9b cleanup drawer 2023-11-12 21:21:26 -05:00
Natan Keddem 4c67e050d1 moved defaults to main 2023-11-12 21:21:26 -05:00
Natan Keddem cf32b1c885 modify color theme 2023-11-12 21:21:26 -05:00
Natan Keddem 3c745b9746 fixed zfs data race condition bug 2023-11-12 21:21:26 -05:00
Natan Keddem 50590d924f added notification for auto manage mode 2023-11-12 21:21:26 -05:00
Natan Keddem b6753be56d added default host selection 2023-11-12 21:21:26 -05:00
Natan Keddem f4722af3db added content min width and remove settings 2023-11-12 21:21:22 -05:00
Natan Keddem cdbf77e039 cleanup content 2023-11-12 20:56:33 -05:00
Natan Keddem 1462de5acd added return_code injection for Cli 2023-11-09 20:18:45 -05:00
Natan Keddem 5a7bd1b61d added http pipe processing to Tab 2023-11-09 20:18:04 -05:00
Natan Keddem 5635b4ee42 added pipe enable and processing to automation 2023-11-09 20:16:38 -05:00
Natan Keddem 5b09b53d85 added http pipe setup to history 2023-11-09 20:15:21 -05:00
Natan Keddem 40d4155672 added remove to manage tasks and optimize 2023-11-09 20:14:48 -05:00
Natan Keddem f8c1f56ec1 cleanup manage 2023-11-09 20:13:48 -05:00
Natan Keddem 3614800715 added pipe enables to Automation 2023-11-09 20:13:10 -05:00
Natan Keddem 1d883a91d4 restructure Result 2023-11-09 20:12:50 -05:00
Natan Keddem 6e0f19db26 added custom json_editor 2023-11-09 20:12:29 -05:00
Natan Keddem 22ff08a09b restructure defaults and css loading 2023-11-09 20:08:32 -05:00
Natan Keddem 03c7bfcfaf cleanup elements 2023-11-09 20:05:03 -05:00
Natan Keddem 157c8c7deb checkbox no longer full width 2023-11-09 20:04:17 -05:00
Natan Keddem 10dd8d1d67 workspace update 2023-11-09 20:03:01 -05:00
Natan Keddem e2f36062d6 change static mount to files 2023-11-07 20:38:02 -05:00
Natan Keddem 4183313467 enable auto mode 2023-11-05 20:32:58 -05:00
Natan Keddem df24ffe39f fixed confirm for browse 2023-11-05 20:00:03 -05:00
Natan Keddem 6f625b029b cleanup ssh 2023-11-05 19:59:34 -05:00
19 changed files with 494 additions and 118 deletions
+3 -2
View File
@@ -13,7 +13,7 @@
"request": "launch",
"program": "main.py",
"console": "integratedTerminal",
"justMyCode": true
"justMyCode": false
},
{
"name": "Python: Current File",
@@ -33,6 +33,7 @@
"black-formatter.args": [
"-l",
"180"
]
],
"editor.suggest.showStatusBar": true
}
}
+13 -10
View File
@@ -1,9 +1,7 @@
from nicegui import app, ui
import re
from datetime import datetime
import asyncio
from nicegui import ui
from bale import elements as el
import bale.logo as logo
from bale.tabs import Tab
from bale.tabs.manage import Manage
from bale.tabs.history import History
from bale.tabs.automation import Automation
@@ -30,6 +28,7 @@ class Content:
def build(self):
self._header = ui.header(bordered=True).classes("bg-dark q-pt-sm q-pb-xs")
self._header.tailwind.border_color(f"[{el.orange}]").min_width("[920px]")
self._header.visible = False
with self._header:
with ui.row().classes("w-full h-12 justify-between items-center"):
@@ -38,7 +37,6 @@ class Content:
self._tab["manage"] = ui.tab(name="Manage").classes("text-secondary")
self._tab["automation"] = ui.tab(name="Automation").classes("text-secondary")
self._tab["history"] = ui.tab(name="History").classes("text-secondary")
self._tab["settings"] = ui.tab(name="Settings").classes("text-secondary")
with ui.row().classes("items-center"):
self._spinner = el.Spinner()
self._host_display = ui.label().classes("text-secondary text-h4")
@@ -46,6 +44,13 @@ class Content:
self._tab_panels = (
ui.tab_panels(self._tabs, value="Manage", on_change=lambda e: self._tab_changed(e), animated=False).classes("w-full h-full").bind_visibility_from(self._header)
)
ui.timer(1, self.select_default, once=True)
async def select_default(self):
tab = Tab(spinner=None)
default = tab.common.get("default", "")
if default != "":
await self.host_selected(default)
async def _tab_changed(self, e):
if e.value == "Manage":
@@ -55,14 +60,12 @@ class Content:
def _build_tab_panels(self):
with self._tab_panels:
with ui.tab_panel(self._tab["manage"]).style("height: calc(100vh - 131px)"):
with el.ContentTabPanel(self._tab["manage"]):
self._manage = Manage(spinner=self._spinner, host=self._host)
with ui.tab_panel(self._tab["automation"]).style("height: calc(100vh - 131px)"):
with el.ContentTabPanel(self._tab["automation"]):
self._automation = Automation(spinner=self._spinner, host=self._host)
with ui.tab_panel(self._tab["history"]).style("height: calc(100vh - 131px)"):
with el.ContentTabPanel(self._tab["history"]):
self._history = History(spinner=self._spinner, host=self._host)
with ui.tab_panel(self._tab["settings"]).style("height: calc(100vh - 131px)"):
ui.label("settings tab")
async def host_selected(self, name):
self._host = name
+26 -7
View File
@@ -22,8 +22,20 @@ class Drawer(object):
self._selection_mode = None
def build(self):
with ui.left_drawer(top_corner=True, bordered=True).props("width=200").classes("q-pt-sm q-pb-xs"):
with el.WColumn():
def toggle_drawer():
if chevron._props["icon"] == "chevron_left":
content.visible = False
drawer.props("width=0")
chevron.props("icon=chevron_right")
chevron.style("top: 16vh").style("right: -24px").style("height: 16vh")
else:
content.visible = True
drawer.props("width=200")
chevron.props("icon=chevron_left")
chevron.style("top: 16vh").style("right: -12px").style("height: 16vh")
with ui.left_drawer(top_corner=True).props("width=226 behavior=desktop bordered").classes("q-pa-none") as drawer:
with ui.column().classes("h-full w-full q-py-xs q-px-md") as content:
self._header_row = el.WRow().classes("justify-between")
self._header_row.tailwind().height("12")
with self._header_row:
@@ -50,12 +62,17 @@ class Drawer(object):
on_select=lambda e: self._selected(e),
)
.on("rowClick", self._clicked, [[], ["name"], None])
.props("hide-header hide-pagination hide-selected-banner dense flat bordered binary-state-sort")
.classes("w-full text-secondary")
.props("dense flat bordered binary-state-sort hide-header hide-pagination hide-selected-bannerhide-no-data")
)
self._table.tailwind.width("full")
self._table.visible = False
for name in ssh.get_hosts("data"):
self._add_host_to_table(name)
chevron = ui.button(icon="chevron_left", color=None, on_click=toggle_drawer).props("padding=0px")
chevron.classes("absolute")
chevron.style("top: 16vh").style("right: -12px").style("background-color: #0E1210 !important").style("height: 16vh")
chevron.tailwind.border_color("[#E97451]")
chevron.props(f"color=primary text-color=accent")
def _add_host_to_table(self, name):
if len(name) > 0:
@@ -70,9 +87,7 @@ class Drawer(object):
save = None
async def send_key():
s = ssh.Ssh(
"data", host=host_input.value, hostname=hostname_input.value, username=username_input.value, password=password_input.value
)
s = ssh.Ssh("data", host=host_input.value, hostname=hostname_input.value, username=username_input.value, password=password_input.value)
result = await s.send_key()
if result.stdout.strip() != "":
el.notify(result.stdout.strip(), multi_line=True, type="positive")
@@ -107,6 +122,10 @@ class Drawer(object):
result = await host_dialog
if result == "save":
if name != "" and name != host_input.value:
default = Tab(spinner=None).common.get("default", "")
if default == name:
Tab(spinner=None).common["default"] = ""
ssh.Ssh(path="data", host=name).remove()
for row in self._table.rows:
if name == row["name"]:
self._table.remove_rows(row)
+60 -15
View File
@@ -1,6 +1,7 @@
from typing import Any, Callable, Dict, List, Literal, Optional, Union
from nicegui import ui, app, Tailwind
from nicegui.elements.spinner import SpinnerTypes
from nicegui.elements.tabs import Tab
from nicegui.tailwind_types.height import Height
from nicegui.tailwind_types.width import Width
from nicegui.elements.mixins.validation_element import ValidationElement
@@ -10,14 +11,48 @@ import logging
logger = logging.getLogger(__name__)
orange = "#f59e0b"
dark = "#171717"
ui.card.default_style("max-width: none")
ui.card.default_props("flat bordered")
ui.input.default_props("outlined dense hide-bottom-space")
ui.button.default_props("outline dense")
ui.select.default_props("outlined dense dense-options")
ui.checkbox.default_props("dense")
orange = "#E97451"
dark = "#0E1210"
def load_element_css():
ui.add_head_html(
f"""
<style>
.bale-colors,
.q-table--dark,
.q-table--dark .q-table__bottom,
.q-table--dark td,
.q-table--dark th,
.q-table--dark thead,
.q-table--dark tr,
.q-table__card--dark,
body.body--dark .q-drawer,
body.body--dark .q-footer,
body.body--dark .q-header {{
color: {orange} !important;
border-color: {orange} !important;
}}
.full-size-stepper,
.full-size-stepper .q-stepper__content,
.full-size-stepper .q-stepper__step-content,
.full-size-stepper .q-stepper__step-inner {{
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}}
.multi-line-notification {{
white-space: pre-line;
}}
.q-drawer--bordered{{
border-color: {orange} !important;
}}
</style>
"""
)
ui.add_head_html('<link href="static/jse-theme-dark.css" rel="stylesheet">')
class ErrorAggregator:
@@ -136,9 +171,7 @@ class DSelect(ui.select):
multiple: bool = False,
clearable: bool = False,
) -> None:
super().__init__(
options, label=label, value=value, on_change=on_change, with_input=with_input, multiple=multiple, clearable=clearable
)
super().__init__(options, label=label, value=value, on_change=on_change, with_input=with_input, multiple=multiple, clearable=clearable)
self.tailwind.width("full")
if multiple is True:
self.props("use-chips")
@@ -156,9 +189,7 @@ class FSelect(ui.select):
multiple: bool = False,
clearable: bool = False,
) -> None:
super().__init__(
options, label=label, value=value, on_change=on_change, with_input=with_input, multiple=multiple, clearable=clearable
)
super().__init__(options, label=label, value=value, on_change=on_change, with_input=with_input, multiple=multiple, clearable=clearable)
self.tailwind.width("64")
@@ -179,7 +210,7 @@ class DButton(ui.button):
class DCheckbox(ui.checkbox):
def __init__(self, text: str = "", *, value: bool = False, on_change: Callable[..., Any] | None = None) -> None:
super().__init__(text, value=value, on_change=on_change)
self.tailwind.width("full").text_color("secondary")
self.tailwind.text_color("secondary")
class IButton(ui.button):
@@ -264,3 +295,17 @@ def notify(
)
else:
ui.notify(message=message, position="bottom-left", type=type)
class JsonEditor(ui.json_editor):
def __init__(self, properties: Dict, *, on_select: Optional[Callable] = None, on_change: Optional[Callable] = None) -> None:
super().__init__(properties, on_select=on_select, on_change=on_change)
self.classes("jse-theme-dark")
self.tailwind.height("[360px]").width("full")
class ContentTabPanel(ui.tab_panel):
def __init__(self, name: Tab | str) -> None:
super().__init__(name)
self.style("height: calc(100vh - 131px)")
self.tailwind.min_width("[920px]")
+3 -4
View File
@@ -4,7 +4,7 @@ from asyncio.subprocess import Process, PIPE
import contextlib
import shlex
from datetime import datetime
from nicegui import app, ui
from nicegui import ui
from bale.result import Result
import logging
@@ -12,7 +12,6 @@ logger = logging.getLogger(__name__)
def load_terminal_css():
app.add_static_files("/static", "static")
ui.add_head_html('<link href="static/xterm.css" rel="stylesheet">')
@@ -111,7 +110,7 @@ class Cli:
finally:
self._terminate.clear()
self._busy = False
return Result(command=command, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=terminated)
return Result(command=command, return_code=process.returncode, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=terminated)
async def shell(self, command: str) -> Result:
self._busy = True
@@ -133,7 +132,7 @@ class Cli:
raise e
finally:
self._busy = False
return Result(command=command, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=False)
return Result(command=command, return_code=process.returncode, stdout_lines=self.stdout.copy(), stderr_lines=self.stderr.copy(), terminated=False)
def clear_buffers(self):
self.prefix_line = ""
-1
View File
@@ -41,7 +41,6 @@ class Ssh(Cli):
self.key_path: str = f"{self._path}/id_rsa"
self._base_cmd: str = ""
self._full_cmd: str = ""
# self._cli = Cli(seperator=seperator)
self._config_path: str = f"{self._path}/config"
self._config: Dict[str, Dict[str, str]] = {}
self.read_config()
+10 -6
View File
@@ -95,16 +95,17 @@ class Zfs:
def is_query_ready_to_execute(self, query: str, timeout: int):
now = datetime.now()
if query in self._last_run_time:
if query in self._last_run_time and query in self._last_data:
if (now - self._last_run_time[query]).total_seconds() > timeout:
self._last_run_time[query] = now
return True
else:
return False
else:
self._last_run_time[query] = now
return True
def set_query_time(self, query: str):
self._last_run_time[query] = datetime.now()
async def add_filesystem_prop(self, filesystem: str, prop: str, value: str) -> Result:
result = await self.execute(f"zfs set {prop}={value} {filesystem}")
return result
@@ -138,7 +139,7 @@ class Zfs:
else:
with_holds = [snapshot]
if len(with_holds) > 0:
result = await self.execute(f"zfs holds -H -r {with_holds}")
result = await self.execute(f"zfs holds -H -r {with_holds}", notify=False)
tags: Dict[str, list[str]] = {}
for line in result.stdout_lines:
matches = re.match("^(?P<filesystem>[^@]+)@(?P<name>[^\t]+)\t(?P<tag>[^\t]+)\t(?P<creation>[^\n]+)", line)
@@ -161,6 +162,7 @@ class Zfs:
else:
data = []
result = Result(data=data, cached=True)
self.set_query_time(query)
return result
async def find_files_in_snapshots(self, filesystem: str, pattern: str) -> Result:
@@ -189,7 +191,7 @@ class Zfs:
async def filesystems(self) -> Result:
query = "filesystems"
if self.is_query_ready_to_execute(query, 60):
result = await self.execute("zfs list -Hp -t filesystem -o name,used,avail,refer,mountpoint")
result = await self.execute("zfs list -Hp -t filesystem -o name,used,avail,refer,mountpoint", notify=False)
filesystems = dict()
for line in result.stdout_lines:
matches = re.match(
@@ -204,13 +206,14 @@ class Zfs:
result.data = self._last_data[query]
else:
result = Result(data=self._last_data[query], cached=True)
self.set_query_time(query)
return result
@property
async def snapshots(self) -> Result:
query = "snapshots"
if self.is_query_ready_to_execute(query, 60):
result = await self.execute("zfs list -Hp -t snapshot -o name,used,creation,userrefs")
result = await self.execute("zfs list -Hp -t snapshot -o name,used,creation,userrefs", notify=False)
snapshots = dict()
for line in result.stdout_lines:
matches = re.match("^(?P<filesystem>[^@]+)@(?P<name>[^\t]+)\t(?P<used_bytes>[^\t]+)\t(?P<creation>[^\t]+)\t(?P<userrefs>[^\n]+)", line)
@@ -225,6 +228,7 @@ class Zfs:
result.data = self._last_data[query]
else:
result = Result(data=self._last_data[query], cached=True)
self.set_query_time(query)
return result
+2 -2
View File
@@ -6,14 +6,14 @@ logger = logging.getLogger(__name__)
# https://www.svgviewer.dev/s/130897/turtle
logo = """
<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 42 42">
<path fill="orange" d="m26.001 26.422-3.594 2.075v7.209c3.739-.449 7.072-2.704 9.135-6.086l-5.541-3.199zm-16.749-9.67a15.142 15.142 0 0 0 0 10.339l5.359-3.094v-4.151c-4.116-2.375-2.764-1.595-5.359-3.094zm1.219-2.529c2.578 1.488 2.013 1.161 5.54 3.199l3.594-2.075v-7.21c-3.62.435-7.014 2.603-9.135 6.086zm5.54 12.199-5.541 3.2c2.018 3.308 5.321 5.627 9.136 6.085v-7.209l-3.594-2.075zm4.994-8.65-3.594 2.075v4.151l3.594 2.075 3.594-2.075v-4.151zm1.4-9.634v7.209l3.594 2.075 5.54-3.199c-2.02-3.321-5.339-5.629-9.134-6.086zm10.354 8.615L27.4 19.847v4.151l5.359 3.094a15.142 15.142 0 0 0 0-10.339zM21.006 0c-2.752 0-5.178 1.862-5.178 6.211a14.479 14.479 0 0 1 10.357-.001c0-4.223-2.316-6.21-5.179-6.21zm11.157 10.374c2.031 2.285 3.392 5.104 3.976 8.106l.012.066c6.414-3.641 1.456-11.252-3.988-8.173zm3.918 15.272-.01.05c-.637 2.968-2.045 5.774-4.15 8.037 5.27 3.144 10.511-4.294 4.16-8.088zM5.86 18.547c.626-3.007 1.827-5.752 3.989-8.172C4.471 7.322-.615 14.871 5.86 18.547zm.071 7.099c-6.313 3.771-1.195 11.283 4.161 8.088-2.23-2.384-3.761-5.682-4.161-8.088zm12.513 12.715 1.29 2.822c.498 1.09 2.049 1.088 2.546 0l1.29-2.822a14.496 14.496 0 0 1-5.126 0z"/>
<path fill="#E97451" d="m26.001 26.422-3.594 2.075v7.209c3.739-.449 7.072-2.704 9.135-6.086l-5.541-3.199zm-16.749-9.67a15.142 15.142 0 0 0 0 10.339l5.359-3.094v-4.151c-4.116-2.375-2.764-1.595-5.359-3.094zm1.219-2.529c2.578 1.488 2.013 1.161 5.54 3.199l3.594-2.075v-7.21c-3.62.435-7.014 2.603-9.135 6.086zm5.54 12.199-5.541 3.2c2.018 3.308 5.321 5.627 9.136 6.085v-7.209l-3.594-2.075zm4.994-8.65-3.594 2.075v4.151l3.594 2.075 3.594-2.075v-4.151zm1.4-9.634v7.209l3.594 2.075 5.54-3.199c-2.02-3.321-5.339-5.629-9.134-6.086zm10.354 8.615L27.4 19.847v4.151l5.359 3.094a15.142 15.142 0 0 0 0-10.339zM21.006 0c-2.752 0-5.178 1.862-5.178 6.211a14.479 14.479 0 0 1 10.357-.001c0-4.223-2.316-6.21-5.179-6.21zm11.157 10.374c2.031 2.285 3.392 5.104 3.976 8.106l.012.066c6.414-3.641 1.456-11.252-3.988-8.173zm3.918 15.272-.01.05c-.637 2.968-2.045 5.774-4.15 8.037 5.27 3.144 10.511-4.294 4.16-8.088zM5.86 18.547c.626-3.007 1.827-5.752 3.989-8.172C4.471 7.322-.615 14.871 5.86 18.547zm.071 7.099c-6.313 3.771-1.195 11.283 4.161 8.088-2.23-2.384-3.761-5.682-4.161-8.088zm12.513 12.715 1.29 2.822c.498 1.09 2.049 1.088 2.546 0l1.29-2.822a14.496 14.496 0 0 1-5.126 0z"/>
</svg>"""
# favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABINJREFUaEPVWj1oVEEQnr0IF87WWKTwp1OjjSA2EdMYNHhgcWvAUjvFwkZLtbSyEO20FOJLIVwwok1EGxFsNNHKaAoLtfXIgbkn3/PmHCe7b9/L7al5TSDv7cx88/PN7O4ZKvBMT0/vXVtbqxtjjhDRWJqmo8aYaoGlhT9J07RtjPlMRItpmj4fGhpqzszMvAsJMHkfWGuPEdElIjoREjSg9/NEdDNJkqc++U4A9Xq9Vq1Wbxljzg7IsFJi0zS91263LzabzZZeuA6AtfYAEd0nov2ltAz+47dEdCZJkjdS1R8AusY/JqLRGPZsqXQyMT86lRjiIAM1clyC6AFA2gwPD7+M4fmJHR/p/MFXPaO/trbSwsouSt7viwHk7erq6mFOpx6ARqNxN0bO2z1LZPcsOg0FiDuvD/UNAjUxOzt7DoIyAF22edKv5JHad7o9+cgrBpEAgMVvI/2qwvpJsBMDgNa+qVKnjsvK5P1YrFSaT5JkyqBJdTqdpRguQd4DBB54+0urlv0d2/aVEB08sdIIsiqVyj7TaDQuG2NuxAAAQ/G4UgQAttd+0XikFKI0Ta8Ya22TiE7GAPAPZMwBwAci2h1LOTw9seNTljLMOGAmPEvfRqJ5v2vvMlJotchgFkoBvEcNyDS6/mIi03N1fKH3/zwmCunQTs4GQGttmud9bZirEGXxsizkuQsAv9dspPsHgF57cTQjgbwnCEB6jwWdfmh7Ml3vuVDzADBTXXgylcmCo66NP+uxlX7vA5ELwMXrMIpZxGd8UQA6miF9LhC5AHRYZVog1wHA94RSSK5DsaM/4NHpGOobpQBI7zPbFJl7fJFyDXnaMaHOnQtAzzYy99mDLiBaaZFvZEQkYNRIXiEHi5jzUoYSChB2KRhGIuUwMvsUMhDdD3it7BuIrEytDRWx9DLGAC7eB6eSzEiAWljZ6TSYjYIMHygZGchjRsobSTSQYAT0Ap1Wrjx2cbre0Lj2DbLGcslfvCwNwDcyc67msRMb6Ns3/BUAvh1XDAAhxindB1wLBgmgSNH2XQO+FCkSAf7Gl0J/BYBWDmZyUaqcTF0TKLMU7+Dg2RDnbziFeMzVNBraoMO4kVordw8shzhulIgyb0dDbBRkIU4ZOdvAMJ5dWAFqg//vO//xfaMdxNGDHK2ndA3gmAQKZKORQlw1oVMG38Ao3tjzqOw77EKjHMg4rYc5mefaM0WnUb1x0X0m1BtK7QdkFEKHWEUBAHjeHiPETKWm0SIbEI5EUQDSeFePCTFTcFPv2u+GlAJEEQCy87pqScpwsRFv6nOPVVyzvBbsKtI8AK6+oB1VcKxYLnywxVTn42cN1AUgdMwe0uGIwlzUo0Xf/gHMAkChI5JQ09Lvs6PFmIe7ZQ3o9/vscBdCrLVRjtelQbJpcVPq12C1/tfxehcArlP7vuCQKaQvOkJ0uAFwvy84sDjWFRMbIlkldLZT1vh1V0wQEPOSD/Jkp47sffclXzeVcEcc9Zo14hUrTPRfs3IYN/VFN4PY1D81kAW1aX/soVnhf/65zU8sHJ18FN1uYgAAAABJRU5ErkJggg=="
favicon = """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 48 48" style="enable-background:new -3 -3 48 48" xml:space="preserve" width="42" height="42">
<rect x="-3" y="-3" height="48" width="48" rx="3" ry="3"/>
<path fill="orange" d="m26.001 26.422-3.594 2.075v7.209c3.739-.449 7.072-2.704 9.135-6.086l-5.541-3.199zm-16.749-9.67a15.142 15.142 0 0 0 0 10.339l5.359-3.094v-4.151c-4.116-2.375-2.764-1.595-5.359-3.094zm1.219-2.529c2.578 1.488 2.013 1.161 5.54 3.199l3.594-2.075v-7.21c-3.62.435-7.014 2.603-9.135 6.086zm5.541 12.199-5.541 3.2c2.018 3.308 5.321 5.627 9.136 6.085v-7.209l-3.594-2.075zm4.994-8.651-3.594 2.075v4.151l3.594 2.075 3.595-2.075v-4.151Zm1.4-9.634v7.209L26 17.421l5.54-3.199c-2.02-3.321-5.339-5.629-9.134-6.086zm10.354 8.615-5.359 3.094v4.151l5.359 3.094a15.142 15.142 0 0 0 0-10.339zM21.006 0c-2.752 0-5.178 1.862-5.178 6.211a14.479 14.479 0 0 1 10.357-.001c0-4.223-2.316-6.21-5.179-6.21zm11.158 10.374c2.031 2.285 3.392 5.104 3.976 8.106l.012.066c6.414-3.641 1.456-11.252-3.988-8.173zm3.918 15.272-.01.05c-.637 2.968-2.045 5.774-4.15 8.037 5.27 3.144 10.511-4.294 4.16-8.088zM5.86 18.547c.626-3.007 1.827-5.752 3.989-8.172C4.471 7.322-.615 14.871 5.86 18.547zm.071 7.099c-6.313 3.771-1.195 11.283 4.161 8.088-2.23-2.384-3.761-5.682-4.161-8.088zm12.512 12.715 1.29 2.822c.498 1.09 2.049 1.088 2.546 0l1.29-2.822a14.496 14.496 0 0 1-5.126 0z"/></svg>
<path fill="#E97451" d="m26.001 26.422-3.594 2.075v7.209c3.739-.449 7.072-2.704 9.135-6.086l-5.541-3.199zm-16.749-9.67a15.142 15.142 0 0 0 0 10.339l5.359-3.094v-4.151c-4.116-2.375-2.764-1.595-5.359-3.094zm1.219-2.529c2.578 1.488 2.013 1.161 5.54 3.199l3.594-2.075v-7.21c-3.62.435-7.014 2.603-9.135 6.086zm5.541 12.199-5.541 3.2c2.018 3.308 5.321 5.627 9.136 6.085v-7.209l-3.594-2.075zm4.994-8.651-3.594 2.075v4.151l3.594 2.075 3.595-2.075v-4.151Zm1.4-9.634v7.209L26 17.421l5.54-3.199c-2.02-3.321-5.339-5.629-9.134-6.086zm10.354 8.615-5.359 3.094v4.151l5.359 3.094a15.142 15.142 0 0 0 0-10.339zM21.006 0c-2.752 0-5.178 1.862-5.178 6.211a14.479 14.479 0 0 1 10.357-.001c0-4.223-2.316-6.21-5.179-6.21zm11.158 10.374c2.031 2.285 3.392 5.104 3.976 8.106l.012.066c6.414-3.641 1.456-11.252-3.988-8.173zm3.918 15.272-.01.05c-.637 2.968-2.045 5.774-4.15 8.037 5.27 3.144 10.511-4.294 4.16-8.088zM5.86 18.547c.626-3.007 1.827-5.752 3.989-8.172C4.471 7.322-.615 14.871 5.86 18.547zm.071 7.099c-6.313 3.771-1.195 11.283 4.161 8.088-2.23-2.384-3.761-5.682-4.161-8.088zm12.512 12.715 1.29 2.822c.498 1.09 2.049 1.088 2.546 0l1.29-2.822a14.496 14.496 0 0 1-5.126 0z"/></svg>
"""
+9 -25
View File
@@ -1,4 +1,4 @@
from nicegui import ui
from nicegui import app, ui
from bale import elements as el
from bale.drawer import Drawer
from bale.content import Content
@@ -10,35 +10,19 @@ logger = logging.getLogger(__name__)
def build():
@ui.page("/")
def page():
ui.add_head_html(
"""
<style>
.full-size-stepper,
.full-size-stepper .q-stepper__content,
.full-size-stepper .q-stepper__step-content,
.full-size-stepper .q-stepper__step-inner {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.multi-line-notification {
white-space: pre-line;
}
</style>
"""
)
def page() -> None:
app.add_static_files("/static", "static")
el.load_element_css()
cli.load_terminal_css()
ui.colors(
primary=el.orange,
secondary=el.orange,
accent="#d946ef",
accent=el.orange,
dark=el.dark,
positive="#21ba45",
negative="#c10015",
info="#31ccec",
warning="#f2c037",
positive="#21BA45",
negative="#C10015",
info="#5C8984",
warning="#F2C037",
)
column = ui.column()
content = Content()
+11 -4
View File
@@ -1,4 +1,4 @@
from typing import Any, List
from typing import Any, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
from copy import deepcopy
@@ -9,17 +9,20 @@ import time
class Result:
name: str = ""
command: str = ""
return_code: int = 0
return_code: Optional[int] = 0
stdout_lines: List[str] = field(default_factory=list)
stderr_lines: List[str] = field(default_factory=list)
terminated: bool = False
data: Any = None
failed: bool = False
trace: str = ""
cached: bool = False
status: str = "success"
timestamp: float = field(default_factory=time.time)
@property
def failed(self) -> bool:
return False if self.status == "success" else True
@property
def date(self) -> str:
return datetime.fromtimestamp(self.timestamp).strftime("%Y/%m/%d")
@@ -36,8 +39,13 @@ class Result:
def stderr(self) -> str:
return "".join(self.stderr_lines)
@property
def properties(self) -> List:
return list(self.to_dict().keys())
def to_dict(self):
d = deepcopy(self.__dict__)
d["failed"] = self.failed
d["date"] = self.date
d["time"] = self.time
d["stdout"] = self.stdout
@@ -52,7 +60,6 @@ class Result:
self.stderr_lines = d["stderr_lines"]
self.terminated = d["terminated"]
self.data = d["data"]
self.failed = d["failed"]
self.trace = d["trace"]
self.cached = d["cached"]
self.status = d["status"]
+2
View File
@@ -19,6 +19,8 @@ class Automation:
triggers: Dict[str, str]
options: Union[Dict[str, Any], None] = None
timestamp: float = field(default_factory=time.time)
pipe_success: bool
pipe_error: bool
def to_dict(self) -> Dict[str, Any]:
return self.__dict__
+68 -4
View File
@@ -1,9 +1,12 @@
from typing import Any, Dict, List, Union
from dataclasses import dataclass, field
import string
import asyncio
from datetime import datetime
import time
from nicegui import ui
import json
import httpx
from nicegui import app, ui
from bale.interfaces.zfs import Ssh
from bale import elements as el
from bale.result import Result
@@ -21,6 +24,10 @@ class Task:
timestamp: float = field(default_factory=time.time)
class PipeTemplate(string.Template):
delimiter = ""
class SelectionConfirm:
def __init__(self, container, label) -> None:
self._container = container
@@ -98,7 +105,7 @@ class Tab:
ui.label(f"Time: {result.time}").classes("text-secondary")
with el.Card() as card:
with el.WColumn():
terminal = cli.Terminal(options={"rows": 20, "cols": 120, "convertEol": True})
terminal = cli.Terminal(options={"rows": 18, "cols": 120, "convertEol": True})
for line in result.stdout_lines:
terminal.call_terminal_method("write", line)
for line in result.stderr_lines:
@@ -115,11 +122,14 @@ class Tab:
self._history.pop(0)
self._history.append(r)
def _add_task(self, action: str, command: str, hosts: Union[List[str], None] = None) -> None:
def _add_task(self, action: str, command: str, hosts: Union[List[str], None] = None) -> List[Task]:
if hosts is None:
hosts = [self.host]
tasks = []
for host in hosts:
self._tasks.append(Task(action=action, command=command, host=host, status="pending"))
tasks.append(Task(action=action, command=command, host=host, status="pending"))
self._tasks.extend(tasks)
return tasks
def _remove_task(self, timestamp: str):
for task in self._tasks:
@@ -143,6 +153,48 @@ class Tab:
self._grid.options["rowSelection"] = row_selection
self._grid.update()
def get_pipe(self, pipe):
if pipe not in self.pipes:
self.pipes[pipe] = {}
return self.pipes[pipe]
def get_pipe_status(self, pipe, status):
if status not in self.get_pipe(pipe):
self.get_pipe(pipe)[status] = {}
return self.get_pipe(pipe)[status]
def process_pipe_data(self, result: Result, data: Any):
template = PipeTemplate(json.dumps(data))
json_string = template.safe_substitute(
name=result.name,
command=result.command,
return_code=result.return_code,
stdout_lines=result.stdout_lines,
stderr_lines=result.stderr_lines,
terminated=result.terminated,
data=result.data,
trace=result.trace,
cached=result.cached,
status=result.status,
timestamp=result.timestamp,
failed=result.failed,
date=result.date,
time=result.time,
stdout=result.stdout,
stderr=result.stderr,
)
json_string = json_string.replace("\n", r"\n").replace("\b", r"\b").replace("\f", r"\f").replace("\r", r"\r").replace("\t", r"\t")
return json.loads(json_string)
def pipe_result(self, result: Result):
http = self.get_pipe("http")
if http.get("enable", False) is True:
status = "success" if result.status == "success" else "error"
url = http[status]["url"]
data = self.process_pipe_data(result=result, data=http[status]["data"])
headers = http[status]["headers"]
httpx.post(url=url, json=data, headers=headers)
@property
def zfs(self) -> Ssh:
return self._zfs[self.host]
@@ -150,3 +202,15 @@ class Tab:
@property
def _zfs_hosts(self) -> List[str]:
return list(self._zfs.keys())
@property
def common(self) -> Dict[str, Any]:
if "common" not in app.storage.general:
app.storage.general["common"] = {}
return app.storage.general["common"]
@property
def pipes(self) -> Dict[str, Any]:
if "pipes" not in self.common:
self.common["pipes"] = {}
return self.common["pipes"]
+23 -1
View File
@@ -51,6 +51,11 @@ async def automation_job(**kwargs) -> None:
if job_handlers[d.id].is_busy is False:
result = await job_handlers[d.id].execute(command.safe_substitute(host=d.host))
result.name = d.host
result.status = "success" if result.return_code == 0 else "error"
if d.pipe_success is True and result.status == "success":
tab.pipe_result(result=result)
if d.pipe_error is True and result.status != "success":
tab.pipe_result(result=result)
tab.add_history(result=result)
else:
logger.warning("Job Skipped!")
@@ -60,6 +65,10 @@ async def automation_job(**kwargs) -> None:
if job_handlers[d.id].is_busy is False:
result = await job_handlers[d.id].execute(command.safe_substitute(host=d.host))
result.name = d.host
if d.pipe_success is True and result.status == "success":
tab.pipe_result(result=result)
if d.pipe_error is True and result.status != "success":
tab.pipe_result(result=result)
tab.add_history(result=result)
else:
logger.warning("Job Skipped!")
@@ -69,6 +78,10 @@ async def automation_job(**kwargs) -> None:
if job_handlers[d.id].is_busy is False:
result = await job_handlers[d.id].execute(command.safe_substitute(host=d.host))
result.name = d.host
if d.pipe_success is True and result.status == "success":
tab.pipe_result(result=result)
if d.pipe_error is True and result.status != "success":
tab.pipe_result(result=result)
tab.add_history(result=result)
else:
logger.warning("Job Skipped!")
@@ -699,7 +712,7 @@ class Automation(Tab):
with ui.dialog() as automation_dialog, el.Card():
with el.DBody(height="[90vh]", width="fit"):
with ui.stepper().props("flat").classes("full-size-stepper") as self.stepper:
with ui.stepper() as self.stepper:
with ui.step("Schedule Setup"):
with el.WColumn().classes("col justify-between"):
with ui.row().classes("col") as row:
@@ -707,6 +720,9 @@ class Automation(Tab):
with ui.column() as col:
col.tailwind.height("full").width("[420px]")
self.auto_name = el.DInput(label="Name", value=" ", validation=validate_name)
with el.WRow():
self.pipe_success = el.DCheckbox("Pipe Success", value=self.job_data.get("pipe_success", False))
self.pipe_error = el.DCheckbox("Pipe Error", value=self.job_data.get("pipe_error", False))
self.schedule_em = el.ErrorAggregator(self.auto_name)
if name != "":
self.app = el.DInput(label="Application", value=self.job_data["app"]).props("readonly")
@@ -778,6 +794,8 @@ class Automation(Tab):
target_path=self.target_path.value,
target_paths=self.target_path.options,
filesystems=self.fs,
pipe_success=self.pipe_success.value,
pipe_error=self.pipe_error.value,
)
self.scheduler.scheduler.add_job(
automation_job,
@@ -803,6 +821,8 @@ class Automation(Tab):
command=self.command.value,
schedule_mode=self.schedule_mode.value,
triggers=self.picked_triggers,
pipe_success=self.pipe_success.value,
pipe_error=self.pipe_error.value,
)
self.scheduler.scheduler.add_job(
automation_job,
@@ -823,6 +843,8 @@ class Automation(Tab):
command=self.command.value,
schedule_mode=self.schedule_mode.value,
triggers=self.picked_triggers,
pipe_success=self.pipe_success.value,
pipe_error=self.pipe_error.value,
)
self.scheduler.scheduler.add_job(
automation_job,
+60 -1
View File
@@ -1,6 +1,7 @@
from datetime import datetime
import json
from . import SelectionConfirm, Tab
from nicegui import ui
from nicegui import ui, events
from bale import elements as el
from bale.result import Result
from bale.interfaces import zfs
@@ -23,6 +24,7 @@ class History(Tab):
with el.WRow().classes("justify-between").bind_visibility_from(self._confirm, "visible", value=False):
with ui.row().classes("items-center"):
el.SmButton(text="Remove", on_click=self._remove_history)
el.SmButton(text="HTTP Pipe", on_click=self._setup_http_pipe)
with ui.row().classes("items-center"):
el.SmButton(text="Refresh", on_click=lambda _: self._grid.update())
self._grid = ui.aggrid(
@@ -77,3 +79,60 @@ class History(Tab):
self._history.remove(row)
self._grid.update()
self._set_selection()
async def _setup_http_pipe(self):
http = {}
def set_url(status: str, url: str) -> None:
http[status]["url"] = url
def set_content(status: str, e: events.JsonEditorChangeEventArguments) -> None:
http[status]["data"] = e.content["json"]["data"]
http[status]["headers"] = e.content["json"]["headers"]
def show_controls(status):
if status not in http:
http[status] = {}
url = el.DInput(label="URL", on_change=lambda e: set_url(status, e.value))
rps = Result().properties
sv = []
for rp in rps:
sv.append(f"{{{rp}}}")
properties = {"content": {"json": {"data": {}, "headers": {}, "Special Values": sv}}}
editor = el.JsonEditor(properties=properties, on_change=lambda e: set_content(status, e))
url.value = self.get_pipe_status("http", status).get("url", "https://www.ntfy.sh/")
editor.properties["content"]["json"]["data"] = self.get_pipe_status("http", status).get(
"data",
{
"topic": "mytopic",
"tags": ["turtle"],
"title": "Successful Automation Run for {name}",
"message": "{stdout}",
},
)
editor.properties["content"]["json"]["headers"] = self.get_pipe_status("http", status).get("headers", {"Authorization": "Bearer tk_..."})
editor.update()
http[status]["data"] = editor.properties["content"]["json"]["data"]
http[status]["headers"] = editor.properties["content"]["json"]["headers"]
with ui.dialog() as host_dialog, el.Card():
with el.DBody(height="[90vh]", width="[720px]"):
with ui.stepper() as stepper:
with ui.step("General"):
with el.WColumn().classes("col justify-start"):
enable = el.DCheckbox("Enable")
enable.value = self.get_pipe("http").get("enable", False)
el.LgButton("NEXT", on_click=lambda _: stepper.next())
with ui.step("On Success"):
with el.WColumn().classes("col justify-start"):
show_controls(status="success")
el.LgButton("NEXT", on_click=lambda _: stepper.next())
with ui.step("On Error"):
with el.WColumn().classes("col justify-start"):
show_controls(status="error")
el.DButton("SAVE", on_click=lambda: host_dialog.submit("save"))
result = await host_dialog
if result == "save":
http["enable"] = enable.value
self.pipes["http"] = http
+79 -32
View File
@@ -13,6 +13,21 @@ logger = logging.getLogger(__name__)
class Manage(Tab):
def _build(self):
def set_auto(value: bool) -> None:
self.common.update({"auto": value})
if value is True:
el.notify("Automatic task handling enabled.", type="info")
else:
el.notify("Automatic task handling disabled.", type="info")
def set_default(value) -> None:
if value is True:
self.common.update({"default": self.host})
el.notify(f"Default host is now {self.host}.", type="info")
else:
self.common.update({"default": ""})
el.notify("Default host is now unset.", type="info")
with el.WColumn() as col:
col.tailwind.height("full")
self._confirm = el.WRow()
@@ -27,8 +42,11 @@ class Manage(Tab):
el.SmButton(text="Browse", on_click=self._browse)
el.SmButton(text="Find", on_click=self._find)
with ui.row().classes("items-center"):
self._auto = ui.checkbox("Auto")
self._auto.props(f"left-label keep-color color=primary")
self._default = ui.checkbox("Default", value=True if self.common.get("default", "") == self.host else False, on_change=lambda e: set_default(e.value))
self._default.props("left-label keep-color color=primary")
self._default.tailwind.text_color("primary")
self._auto = ui.checkbox("Auto", value=self.common.get("auto", False), on_change=lambda e: set_auto(e.value))
self._auto.props("left-label keep-color color=primary")
self._auto.tailwind.text_color("primary")
el.SmButton(text="Tasks", on_click=self._display_tasks)
el.SmButton(text="Refresh", on_click=self.display_snapshots)
@@ -81,7 +99,7 @@ class Manage(Tab):
self._spinner.visible = False
async def _browse(self) -> None:
self._set_selection(mode="multiple")
self._set_selection(mode="single")
result = await SelectionConfirm(container=self._confirm, label=">BROWSE<")
if result == "confirm":
rows = await self._grid.get_selected_rows()
@@ -111,11 +129,15 @@ class Manage(Tab):
result = await dialog
if result == "create":
for filesystem in filesystems.value:
self._add_task(
tasks = self._add_task(
"create",
zfs.SnapshotCreate(name=f"{filesystem}@{name.value}", recursive=recursive.value).command,
hosts=zfs_hosts.value,
)
if self._auto.value is True:
for task in tasks:
await self._run_task(task=task, spinner=self._spinner)
await self.display_snapshots()
async def _destroy_snapshot(self):
with ui.dialog() as dialog, el.Card():
@@ -132,11 +154,15 @@ class Manage(Tab):
if result == "destroy":
rows = await self._grid.get_selected_rows()
for row in rows:
self._add_task(
tasks = self._add_task(
"destroy",
zfs.SnapshotDestroy(name=f"{row['filesystem']}@{row['name']}", recursive=recursive.value).command,
hosts=zfs_hosts.value,
)
if self._auto.value is True:
for task in tasks:
await self._run_task(task=task, spinner=self._spinner)
await self.display_snapshots()
self._set_selection()
async def _rename_snapshot(self):
@@ -163,11 +189,15 @@ class Manage(Tab):
if mode.value == "replace":
rename = row["name"].replace(original.value, replace.value)
if row["name"] != rename:
self._add_task(
tasks = self._add_task(
"rename",
zfs.SnapshotRename(name=f"{row['filesystem']}@{row['name']}", new_name=rename, recursive=recursive.value).command,
hosts=zfs_hosts.value,
)
if self._auto.value is True:
for task in tasks:
await self._run_task(task=task, spinner=self._spinner)
await self.display_snapshots()
else:
el.notify(f"Skipping rename of {row['filesystem']}@{row['name']}!")
self._set_selection()
@@ -188,7 +218,7 @@ class Manage(Tab):
if result == "hold":
rows = await self._grid.get_selected_rows()
for row in rows:
self._add_task(
tasks = self._add_task(
"hold",
zfs.SnapshotHold(
name=f"{row['filesystem']}@{row['name']}",
@@ -197,6 +227,10 @@ class Manage(Tab):
).command,
hosts=zfs_hosts.value,
)
if self._auto.value is True:
for task in tasks:
await self._run_task(task=task, spinner=self._spinner)
await self.display_snapshots()
self._set_selection()
async def _release_snapshot(self):
@@ -227,7 +261,7 @@ class Manage(Tab):
if len(tags.value) > 0:
for tag in tags.value:
for row in rows:
self._add_task(
tasks = self._add_task(
"release",
zfs.SnapshotRelease(
name=f"{row['filesystem']}@{row['name']}",
@@ -236,35 +270,40 @@ class Manage(Tab):
).command,
hosts=zfs_hosts.value,
)
if self._auto.value is True:
for task in tasks:
await self._run_task(task=task, spinner=self._spinner)
await self.display_snapshots()
self._set_selection()
async def _display_tasks(self):
def update_status(timestamp, status, result=None):
for row in grid.options["rowData"]:
if timestamp == row.timestamp:
row.status = status
if result is not None:
row.result = deepcopy(result)
self.add_history(deepcopy(result))
grid.update()
return row
def _update_task_status(self, timestamp, status, result=None):
for task in self._tasks:
if timestamp == task.timestamp:
task.status = status
if result is not None:
task.result = deepcopy(result)
self.add_history(deepcopy(result))
return task
async def _run_task(self, task: Task, spinner: el.Spinner):
spinner.visible = True
if task.status == "pending":
self._update_task_status(task.timestamp, "running")
result = await self.zfs.execute(task.command)
if result.stdout == "" and result.stderr == "":
status = "success"
else:
status = "error"
self._update_task_status(task.timestamp, status, result)
spinner.visible = False
async def _display_tasks(self):
async def apply():
spinner.visible = True
rows = await grid.get_selected_rows()
for row in rows:
task = Task(**row)
if task.status == "pending":
update_status(task.timestamp, "running")
result = await self.zfs.execute(task.command)
if result.stdout == "" and result.stderr == "":
status = "success"
result.failed = False
else:
status = "error"
result.failed = True
update_status(task.timestamp, status, result)
spinner.visible = False
await self._run_task(task=task, spinner=spinner)
grid.update()
async def dry_run():
spinner.visible = True
@@ -280,7 +319,15 @@ class Manage(Tab):
for grow in grid.options["rowData"]:
if row["command"] == grow.command:
grow.status = "pending"
grid.update()
grid.update()
async def remove():
rows = await grid.get_selected_rows()
for row in rows:
for task in self._tasks:
if row["timestamp"] == task.timestamp:
self._tasks.remove(task)
grid.update()
async def display_result(e):
if e.args["data"]["result"] is not None:
@@ -344,7 +391,7 @@ class Manage(Tab):
el.DButton("Apply", on_click=apply)
el.DButton("Dry Run", on_click=dry_run)
el.DButton("Reset", on_click=reset)
el.DButton("Remove", on_click=lambda: dialog.submit("finish"))
el.DButton("Remove", on_click=remove)
el.DButton("Exit", on_click=lambda: dialog.submit("exit"))
el.Spinner(master=spinner)
-3
View File
@@ -1,3 +0,0 @@
import logging
logger = logging.getLogger(__name__)
+10
View File
@@ -8,6 +8,16 @@ if not os.path.exists("data"):
os.makedirs("data")
os.environ.setdefault("NICEGUI_STORAGE_PATH", "data")
from nicegui import ui
ui.card.default_style("max-width: none")
ui.card.default_props("flat bordered")
ui.input.default_props("outlined dense hide-bottom-space")
ui.button.default_props("outline dense")
ui.select.default_props("outlined dense dense-options")
ui.checkbox.default_props("dense")
ui.stepper.default_props("flat")
ui.stepper.default_classes("full-size-stepper")
from bale import page, logo, scheduler
+1 -1
View File
@@ -2,7 +2,7 @@ APScheduler==3.10.4
SQLAlchemy==2.0.22
cron-descriptor==1.4.0
cron-validator==1.0.8
nicegui==1.4.1
nicegui==1.4.2
zfs-autobackup==3.2
netifaces==0.11.0
asyncssh==2.14.0
+114
View File
@@ -0,0 +1,114 @@
.jse-theme-dark {
--jse-theme: dark;
/* over all fonts, sizes, and colors */
--jse-theme-color: #E48257;
--jse-theme-color-highlight: #E48257;
--jse-background-color: #1e1e1e;
--jse-text-color: #d4d4d4;
/* main, menu, modal */
--jse-main-border: 1px solid #4f4f4f;
--jse-menu-color: #fff;
--jse-modal-background: #2f2f2f;
--jse-modal-overlay-background: rgba(0, 0, 0, 0.5);
--jse-modal-code-background: #2f2f2f;
/* tooltip in text mode */
--jse-tooltip-color: var(--jse-text-color);
--jse-tooltip-background: #4b4b4b;
--jse-tooltip-border: 1px solid #737373;
--jse-tooltip-action-button-color: inherit;
--jse-tooltip-action-button-background: #737373;
/* panels: navigation bar, gutter, search box */
--jse-panel-background: #333333;
--jse-panel-background-border: 1px solid #464646;
--jse-panel-color: var(--jse-text-color);
--jse-panel-color-readonly: #737373;
--jse-panel-border: 1px solid #3c3c3c;
--jse-panel-button-color-highlight: #e5e5e5;
--jse-panel-button-background-highlight: #464646;
/* navigation-bar */
--jse-navigation-bar-background: #656565;
--jse-navigation-bar-background-highlight: #7e7e7e;
--jse-navigation-bar-dropdown-color: var(--jse-text-color);
/* context menu */
--jse-context-menu-background: #4b4b4b;
--jse-context-menu-background-highlight: #595959;
--jse-context-menu-separator-color: #595959;
--jse-context-menu-color: var(--jse-text-color);
--jse-context-menu-pointer-background: #737373;
--jse-context-menu-pointer-background-highlight: #818181;
--jse-context-menu-pointer-color: var(--jse-context-menu-color);
/* contents: json key and values */
--jse-key-color: #9cdcfe;
--jse-value-color: var(--jse-text-color);
--jse-value-color-number: #b5cea8;
--jse-value-color-boolean: #569cd6;
--jse-value-color-null: #569cd6;
--jse-value-color-string: #ce9178;
--jse-value-color-url: #ce9178;
--jse-delimiter-color: #949494;
--jse-edit-outline: 2px solid var(--jse-text-color);
/* contents: selected or hovered */
--jse-selection-background-color: #464646;
--jse-selection-background-inactive-color: #333333;
--jse-hover-background-color: #343434;
--jse-active-line-background-color: rgba(255, 255, 255, 0.06);
--jse-search-match-background-color: #343434;
/* contents: section of collapsed items in an array */
--jse-collapsed-items-background-color: #333333;
--jse-collapsed-items-selected-background-color: #565656;
--jse-collapsed-items-link-color: #b2b2b2;
--jse-collapsed-items-link-color-highlight: #ec8477;
/* contents: highlighting of search results */
--jse-search-match-color: #724c27;
--jse-search-match-outline: 1px solid #966535;
--jse-search-match-active-color: #9f6c39;
--jse-search-match-active-outline: 1px solid #bb7f43;
/* contents: inline tags inside the JSON document */
--jse-tag-background: #444444;
--jse-tag-color: #bdbdbd;
/* contents: table */
--jse-table-header-background: #333333;
--jse-table-header-background-highlight: #424242;
--jse-table-row-odd-background: rgba(255, 255, 255, 0.1);
/* controls in modals: inputs, buttons, and `a` */
--jse-input-background: #3d3d3d;
--jse-input-border: var(--jse-main-border);
--jse-button-background: #808080;
--jse-button-background-highlight: #7a7a7a;
--jse-button-color: #e0e0e0;
--jse-button-secondary-background: #494949;
--jse-button-secondary-background-highlight: #5d5d5d;
--jse-button-secondary-background-disabled: #9d9d9d;
--jse-button-secondary-color: var(--jse-text-color);
--jse-a-color: #E48257;
--jse-a-color-highlight: #E48257;
/* svelte-select */
--background: #3d3d3d;
--border: 1px solid #4f4f4f;
--list-background: #3d3d3d;
--item-hover-bg: #505050;
--multi-item-bg: #5b5b5b;
--input-color: #d4d4d4;
--multi-clear-bg: #8a8a8a;
--multi-item-clear-icon-color: #d4d4d4;
--multi-item-outline: 1px solid #696969;
--list-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.4);
/* color picker */
--jse-color-picker-background: #656565;
--jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px;
}