From e3a03a9299cd9dbc054a46553dfaed88b784eef4 Mon Sep 17 00:00:00 2001 From: Uwe Seimet Date: Sun, 12 Nov 2023 08:35:24 +0100 Subject: [PATCH] Merge with develop --- .github/CODEOWNERS | 2 +- .github/ISSUE_TEMPLATE.md | 1 + .github/workflows/cpp.yml | 11 +- .github/workflows/web.yml | 5 - easyinstall.sh | 48 +- python/README.md | 41 +- python/common/requirements.txt | 4 + python/common/src/piscsi/file_cmds.py | 121 +-- python/common/src/piscsi/piscsi_cmds.py | 98 +- .../ctrlboard_menu_update_event_handler.py | 3 +- .../ctrlboard/src/ctrlboard_menu_builder.py | 6 +- python/oled/requirements.txt | 13 +- python/pyproject.toml | 2 +- python/web/requirements.txt | 13 +- python/web/src/return_code_mapper.py | 2 +- .../web/src/static/themes/classic/style.css | 13 + .../static/themes/modern/icons/cloud-off.svg | 1 + .../src/static/themes/modern/icons/cloud.svg | 1 + .../static/themes/modern/icons/command.svg | 1 + .../icons/{manual copy.svg => home.svg} | 2 +- .../src/static/themes/modern/icons/manual.svg | 1 - python/web/src/static/themes/modern/style.css | 58 +- python/web/src/templates/admin.html | 193 ++++ python/web/src/templates/base.html | 59 +- python/web/src/templates/deviceinfo.html | 46 +- python/web/src/templates/diskinfo.html | 2 +- python/web/src/templates/drives.html | 2 +- python/web/src/templates/index.html | 129 +-- python/web/src/templates/logs.html | 27 +- python/web/src/templates/manpage.html | 2 +- python/web/src/templates/upload.html | 26 +- .../translations/de/LC_MESSAGES/messages.po | 970 ++++++++++-------- .../translations/es/LC_MESSAGES/messages.po | 777 ++++++++------ .../translations/fr/LC_MESSAGES/messages.po | 840 ++++++++------- .../translations/sv/LC_MESSAGES/messages.po | 920 +++++++++-------- .../translations/zh/LC_MESSAGES/messages.po | 749 ++++++++------ python/web/src/web.py | 161 ++- python/web/src/web_utils.py | 58 +- python/web/tests/api/test_files.py | 8 +- 39 files changed, 3128 insertions(+), 2288 deletions(-) create mode 100644 python/web/src/static/themes/modern/icons/cloud-off.svg create mode 100644 python/web/src/static/themes/modern/icons/cloud.svg create mode 100644 python/web/src/static/themes/modern/icons/command.svg rename python/web/src/static/themes/modern/icons/{manual copy.svg => home.svg} (55%) delete mode 100644 python/web/src/static/themes/modern/icons/manual.svg create mode 100644 python/web/src/templates/admin.html diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 20e3089ed0..202a2fdfbb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @akuker @erichelgeson @rdmark +* @akuker @rdmark diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0dc35b8228..c828582eab 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,6 +4,7 @@ - Which github revision of software: - Which board version: - Which computer is the PiSCSI connected to: +- Which OS you are using (output of 'lsb_release -a'): # Describe the issue diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 91e2ae5d1b..7c3eac8c51 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -1,20 +1,17 @@ -name: C++ Tests/Analysis +name: C++ Tests; Full Static Analysis on: workflow_dispatch: push: paths: - 'cpp/**' + - 'python/**' - '.github/workflows/cpp.yml' pull_request: paths: - 'cpp/**' + - 'python/**' - '.github/workflows/cpp.yml' - types: - - assigned - - opened - - synchronize - - reopened branches: - 'develop' - 'main' @@ -118,4 +115,4 @@ jobs: --define sonar.coverage.exclusions="cpp/**/test/**" --define sonar.cpd.exclusions="cpp/**/test/**" --define sonar.inclusions="cpp/**,python/**" - --define sonar.python.version=3.7,3.9 + --define sonar.python.version=3.9,3.11 diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index d22c4906fe..c2f0c0866f 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -14,11 +14,6 @@ on: - 'python/common/**' - '.github/workflows/web.yml' - 'easyinstall.sh' - types: - - assigned - - opened - - synchronize - - reopened branches: - 'develop' - 'main' diff --git a/easyinstall.sh b/easyinstall.sh index 7e1754fe8b..21b1d5a727 100755 --- a/easyinstall.sh +++ b/easyinstall.sh @@ -76,7 +76,7 @@ SECRET_FILE="$HOME/.config/piscsi/secret" FILE_SHARE_PATH="$HOME/shared_files" FILE_SHARE_NAME="Pi File Server" -APT_PACKAGES_COMMON="build-essential git protobuf-compiler bridge-utils" +APT_PACKAGES_COMMON="build-essential git protobuf-compiler bridge-utils ca-certificates" APT_PACKAGES_BACKEND="libspdlog-dev libpcap-dev libprotobuf-dev protobuf-compiler libgmock-dev clang" APT_PACKAGES_PYTHON="python3 python3-dev python3-pip python3-venv python3-setuptools python3-wheel libev-dev libevdev2" APT_PACKAGES_WEB="nginx-light genisoimage man2html hfsutils dosfstools kpartx unzip unar disktype gettext" @@ -111,7 +111,15 @@ function sudoCheck() { function deleteFile() { if sudo test -f "$1/$2"; then sudo rm "$1/$2" || exit 1 - echo "Deleted $1/$2" + echo "Deleted file $1/$2" + fi +} + +# Delete dir if it exists +function deleteDir() { + if sudo test -d "$1"; then + sudo rm -rf "$1" || exit 1 + echo "Deleted directory $1" fi } @@ -229,6 +237,9 @@ function installPiscsiWebInterface() { deleteFile "$SSL_CERTS_PATH" "rascsi-web.crt" deleteFile "$SSL_KEYS_PATH" "rascsi-web.key" + # Deleting previous venv dir, if one exists, to avoid the common issue of broken python dependencies + deleteDir "$WEB_INSTALL_PATH/venv" + if [ -f "$SSL_CERTS_PATH/piscsi-web.crt" ]; then echo "SSL certificate $SSL_CERTS_PATH/piscsi-web.crt already exists." else @@ -937,6 +948,7 @@ function installSamba() { # Installs and configures Webmin function installWebmin() { WEBMIN_PATH="/usr/share/webmin" + WEBMIN_MODULE_CONFIG="/etc/webmin/netatalk2/config" WEBMIN_MODULE_VERSION="1.0" if [ -d "$WEBMIN_PATH" ]; then @@ -953,17 +965,27 @@ function installWebmin() { echo echo "Installing packages..." - sudo apt-get install curl --no-install-recommends --assume-yes /dev/null || true wget -O netatalk2-wbm.tgz "https://github.com/Netatalk/netatalk-webmin/releases/download/netatalk2-$WEBMIN_MODULE_VERSION/netatalk2-wbm-$WEBMIN_MODULE_VERSION.tgz" requirements.txt +``` + +## Static analysis and formatting + +The CI workflow is set up to check code formatting with `black`, +and linting with `flake8`. If non-conformant code is found, the CI job +will fail. + +Before checking in new code, install the development packages and run +these two tools locally. + +``` +pip install -r web/requirements-dev.txt +``` + +Note that `black` only works correctly if you run it in the root of the +`python/` dir: + +``` +cd python +black . +``` + +Optionally: It is recommended to run pylint against new code to protect against bugs and keep the code readable and maintainable. The local pylint configuration lives in .pylintrc. In order for pylint to recognize venv libraries, the pylint-venv package is required. diff --git a/python/common/requirements.txt b/python/common/requirements.txt index 90a99b04c4..658ef8fee9 100644 --- a/python/common/requirements.txt +++ b/python/common/requirements.txt @@ -1,3 +1,7 @@ +certifi==2023.7.22 +charset-normalizer==3.3.2 +idna==3.4 protobuf==3.19.5 requests==2.31.0 +urllib3==2.0.7 vcgencmd==0.1.1 diff --git a/python/common/src/piscsi/file_cmds.py b/python/common/src/piscsi/file_cmds.py index a64298ae51..08666f750c 100644 --- a/python/common/src/piscsi/file_cmds.py +++ b/python/common/src/piscsi/file_cmds.py @@ -5,7 +5,6 @@ import logging import asyncio from os import walk, path -from functools import lru_cache from pathlib import PurePath, Path from zipfile import ZipFile, is_zipfile from subprocess import run, Popen, PIPE, CalledProcessError, TimeoutExpired @@ -17,23 +16,22 @@ import requests -import piscsi_interface_pb2 as proto from piscsi.common_settings import ( CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, - ARCHIVE_FILE_SUFFIXES, RESERVATIONS, SHELL_ERROR, ) from piscsi.piscsi_cmds import PiscsiCmds from piscsi.return_codes import ReturnCodes -from piscsi.socket_cmds import SocketCmds from util import unarchiver FILE_READ_ERROR = "Unhandled exception when reading file: %s" FILE_WRITE_ERROR = "Unhandled exception when writing to file: %s" URL_SAFE = "/:?&" +# Common file sharing protocol meta data dirs to filter out from target upload dirs +EXCLUDED_DIRS = ["Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder"] class FileCmds: @@ -41,18 +39,8 @@ class FileCmds: class for methods reading from and writing to the file system """ - def __init__(self, sock_cmd: SocketCmds, piscsi: PiscsiCmds, token=None, locale=None): - self.sock_cmd = sock_cmd + def __init__(self, piscsi: PiscsiCmds): self.piscsi = piscsi - self.token = token - self.locale = locale - - def send_pb_command(self, command): - if logging.getLogger().isEnabledFor(logging.DEBUG): - # TODO: Uncouple/move to common dependency - logging.debug(self.piscsi.format_pb_command(command)) - - return self.sock_cmd.send_pb_command(command.SerializeToString()) # noinspection PyMethodMayBeStatic def list_config_files(self): @@ -74,89 +62,18 @@ def list_subdirs(self, directory): Returns a (list) of (str) subdir_list. """ subdir_list = [] - # Filter out file sharing meta data dirs - excluded_dirs = ("Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder") for root, dirs, _files in walk(directory, topdown=True): # Strip out dirs that begin with . - dirs[:] = [d for d in dirs if not d[0] == "."] + dirs[:] = [d for d in dirs if d[0] != "."] for dir in dirs: - if dir not in excluded_dirs: + if dir not in EXCLUDED_DIRS: dirpath = path.join(root, dir) - subdir_list.append(dirpath.replace(directory, "", 1)) + # Remove the section of the path up until the first subdir + subdir_list.append(dirpath.replace(directory + "/", "", 1)) subdir_list.sort() return subdir_list - def list_images(self): - """ - Sends a IMAGE_FILES_INFO command to the server - Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files - - """ - command = proto.PbCommand() - command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO - command.params["token"] = self.token - command.params["locale"] = self.locale - - data = self.send_pb_command(command) - result = proto.PbResult() - result.ParseFromString(data) - - server_info = self.piscsi.get_server_info() - files = [] - for file in result.image_files_info.image_files: - prop_file_path = Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}" - # Add properties meta data for the image, if matching prop file is found - if prop_file_path.exists(): - process = self.read_drive_properties(prop_file_path) - prop = process["conf"] - else: - prop = False - - archive_contents = [] - if PurePath(file.name).suffix.lower()[1:] in ARCHIVE_FILE_SUFFIXES: - try: - archive_info = self._get_archive_info( - f"{server_info['image_dir']}/{file.name}", - _cache_extra_key=file.size, - ) - - properties_files = [ - x["path"] - for x in archive_info["members"] - if x["path"].endswith(PROPERTIES_SUFFIX) - ] - - for member in archive_info["members"]: - if member["is_dir"] or member["is_resource_fork"]: - continue - - if PurePath(member["path"]).suffix.lower()[1:] == PROPERTIES_SUFFIX: - member["is_properties_file"] = True - elif f"{member['path']}.{PROPERTIES_SUFFIX}" in properties_files: - member[ - "related_properties_file" - ] = f"{member['path']}.{PROPERTIES_SUFFIX}" - - archive_contents.append(member) - except (unarchiver.LsarCommandError, unarchiver.LsarOutputError): - pass - - size_mb = "{:,.1f}".format(file.size / 1024 / 1024) - dtype = proto.PbDeviceType.Name(file.type) - files.append( - { - "name": file.name, - "size": file.size, - "size_mb": size_mb, - "detected_type": dtype, - "prop": prop, - "archive_contents": archive_contents, - } - ) - - return {"status": result.status, "msg": result.msg, "files": files} - # noinspection PyMethodMayBeStatic def delete_file(self, file_path): """ @@ -572,12 +489,12 @@ def download_file_to_iso(self, url, *iso_args): iso_filename = Path(server_info["image_dir"]) / f"{file_name}.iso" with TemporaryDirectory() as tmp_dir: - req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_dir, file_name) + tmp_full_path = Path(tmp_dir) / file_name + req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_full_path) logging.info("Downloaded %s to %s", file_name, tmp_dir) if not req_proc["status"]: return {"status": False, "msg": req_proc["msg"]} - tmp_full_path = Path(tmp_dir) / file_name if is_zipfile(tmp_full_path): if "XtraStuf.mac" in str(ZipFile(str(tmp_full_path)).namelist()): logging.info( @@ -649,9 +566,9 @@ def generate_iso(self, iso_file, target_path, *iso_args): } # noinspection PyMethodMayBeStatic - def download_to_dir(self, url, save_dir, file_name): + def download_to_dir(self, url, target_path): """ - Takes (str) url, (str) save_dir, (str) file_name + Takes (str) url, (Path) target_path Returns (dict) with (bool) status and (str) msg """ logging.info("Making a request to download %s", url) @@ -664,7 +581,7 @@ def download_to_dir(self, url, save_dir, file_name): ) as req: req.raise_for_status() try: - with open(f"{save_dir}/{file_name}", "wb") as download: + with open(str(target_path), "wb") as download: for chunk in req.iter_content(chunk_size=8192): download.write(chunk) except FileNotFoundError as error: @@ -677,7 +594,7 @@ def download_to_dir(self, url, save_dir, file_name): logging.info("Response content-type: %s", req.headers["content-type"]) logging.info("Response status code: %s", req.status_code) - parameters = {"file_name": file_name, "save_dir": save_dir} + parameters = {"target_path": str(target_path)} return { "status": True, "return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS, @@ -892,15 +809,3 @@ async def run_async(self, program, args): logging.info("stderr: %s", stderr) return {"returncode": proc.returncode, "stdout": stdout, "stderr": stderr} - - # noinspection PyMethodMayBeStatic - @lru_cache(maxsize=32) - def _get_archive_info(self, file_path, **kwargs): - """ - Cached wrapper method to improve performance, e.g. on index screen - """ - try: - return unarchiver.inspect_archive(file_path) - except (unarchiver.LsarCommandError, unarchiver.LsarOutputError) as error: - logging.error(str(error)) - raise diff --git a/python/common/src/piscsi/piscsi_cmds.py b/python/common/src/piscsi/piscsi_cmds.py index 2f7b16fcbd..408f8b768b 100644 --- a/python/common/src/piscsi/piscsi_cmds.py +++ b/python/common/src/piscsi/piscsi_cmds.py @@ -2,10 +2,21 @@ Module for commands sent to the PiSCSI backend service. """ +import logging +from pathlib import PurePath, Path +from functools import lru_cache + import piscsi_interface_pb2 as proto from piscsi.return_codes import ReturnCodes from piscsi.socket_cmds import SocketCmds -import logging + +from piscsi.common_settings import ( + CFG_DIR, + PROPERTIES_SUFFIX, + ARCHIVE_FILE_SUFFIXES, +) + +from util import unarchiver class PiscsiCmds: @@ -24,6 +35,79 @@ def send_pb_command(self, command): return self.sock_cmd.send_pb_command(command.SerializeToString()) + def list_images(self): + """ + Sends a IMAGE_FILES_INFO command to the server + Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files + """ + from piscsi.file_cmds import FileCmds + + self.file_cmd = FileCmds(piscsi=self) + + command = proto.PbCommand() + command.operation = proto.PbOperation.DEFAULT_IMAGE_FILES_INFO + command.params["token"] = self.token + command.params["locale"] = self.locale + + data = self.send_pb_command(command) + result = proto.PbResult() + result.ParseFromString(data) + + server_info = self.get_server_info() + files = [] + for file in result.image_files_info.image_files: + prop_file_path = Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}" + # Add properties meta data for the image, if matching prop file is found + if prop_file_path.exists(): + process = self.file_cmd.read_drive_properties(prop_file_path) + prop = process["conf"] + else: + prop = False + + archive_contents = [] + if PurePath(file.name).suffix.lower()[1:] in ARCHIVE_FILE_SUFFIXES: + try: + archive_info = self._get_archive_info( + f"{server_info['image_dir']}/{file.name}", + _cache_extra_key=file.size, + ) + + properties_files = [ + x["path"] + for x in archive_info["members"] + if x["path"].endswith(PROPERTIES_SUFFIX) + ] + + for member in archive_info["members"]: + if member["is_dir"] or member["is_resource_fork"]: + continue + + if PurePath(member["path"]).suffix.lower()[1:] == PROPERTIES_SUFFIX: + member["is_properties_file"] = True + elif f"{member['path']}.{PROPERTIES_SUFFIX}" in properties_files: + member[ + "related_properties_file" + ] = f"{member['path']}.{PROPERTIES_SUFFIX}" + + archive_contents.append(member) + except (unarchiver.LsarCommandError, unarchiver.LsarOutputError): + pass + + size_mb = "{:,.1f}".format(file.size / 1024 / 1024) + dtype = proto.PbDeviceType.Name(file.type) + files.append( + { + "name": file.name, + "size": file.size, + "size_mb": size_mb, + "detected_type": dtype, + "prop": prop, + "archive_contents": archive_contents, + } + ) + + return {"status": result.status, "msg": result.msg, "files": files} + def get_server_info(self): """ Sends a SERVER_INFO command to the server. @@ -521,3 +605,15 @@ def format_pb_command(self, command): message += f", device: {formatted_device}" return message + + # noinspection PyMethodMayBeStatic + @lru_cache(maxsize=32) + def _get_archive_info(self, file_path, **kwargs): + """ + Cached wrapper method to improve performance, e.g. on index screen + """ + try: + return unarchiver.inspect_archive(file_path) + except (unarchiver.LsarCommandError, unarchiver.LsarOutputError) as error: + logging.error(str(error)) + raise diff --git a/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_menu_update_event_handler.py b/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_menu_update_event_handler.py index 6b48d9f7c1..768e996f4f 100644 --- a/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_menu_update_event_handler.py +++ b/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_menu_update_event_handler.py @@ -107,8 +107,7 @@ def route_rotary_button_handler(self, info_object): except AttributeError: log = logging.getLogger(__name__) log.error( - "Handler function [%s] not found or returned an error. Skipping.", - str(handler_function_name), + "Handler function not found or returned an error. Skipping.", ) # noinspection PyUnusedLocal diff --git a/python/ctrlboard/src/ctrlboard_menu_builder.py b/python/ctrlboard/src/ctrlboard_menu_builder.py index 25075f1def..3f322131ff 100644 --- a/python/ctrlboard/src/ctrlboard_menu_builder.py +++ b/python/ctrlboard/src/ctrlboard_menu_builder.py @@ -8,7 +8,7 @@ class CtrlBoardMenuBuilder(MenuBuilder): - """Class fgor building the control board UI specific menus""" + """Class for building the control board UI specific menus""" SCSI_ID_MENU = "scsi_id_menu" ACTION_MENU = "action_menu" @@ -48,7 +48,7 @@ def build(self, name: str, context_object=None) -> Menu: return self.create_device_info_menu(context_object) log = logging.getLogger(__name__) - log.warning("Provided menu name [%s] cannot be built!", name) + log.error("Provided menu name cannot be built!") return self.create_scsi_id_list_menu(context_object) @@ -142,7 +142,7 @@ def create_action_menu(self, context_object=None): def create_images_menu(self, context_object=None): """Creates a sub menu showing all the available images""" menu = Menu(CtrlBoardMenuBuilder.IMAGES_MENU) - images_info = self.file_cmd.list_images() + images_info = self.piscsi_cmd.list_images() menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN}) images = images_info["files"] sorted_images = sorted(images, key=lambda d: d["name"]) diff --git a/python/oled/requirements.txt b/python/oled/requirements.txt index 8ea5d44c2b..7d240e485f 100644 --- a/python/oled/requirements.txt +++ b/python/oled/requirements.txt @@ -1,4 +1,15 @@ +Adafruit-Blinka==8.24.0 +adafruit-circuitpython-busdevice==5.2.6 +adafruit-circuitpython-framebuf==1.6.4 +adafruit-circuitpython-requests==2.0.2 adafruit-circuitpython-ssd1306==2.12.11 +adafruit-circuitpython-typing==1.9.5 +Adafruit-PlatformDetect==3.53.0 +Adafruit-PureIO==1.1.11 Pillow==10.0.1 protobuf==3.20.2 -unidecode==1.3.6 +pyftdi==0.55.0 +pyserial==3.5 +pyusb==1.2.1 +typing_extensions==4.8.0 +Unidecode==1.3.6 diff --git a/python/pyproject.toml b/python/pyproject.toml index aea91ba3a0..0ede3f4658 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,4 +1,4 @@ [tool.black] line-length = 100 -target-version = ['py37', 'py38', 'py39'] +target-version = ['py39', 'py310', 'py311'] extend-exclude = ".*_pb2.py" diff --git a/python/web/requirements.txt b/python/web/requirements.txt index a11d5f408a..88e3f57d75 100644 --- a/python/web/requirements.txt +++ b/python/web/requirements.txt @@ -1,10 +1,17 @@ +Babel==2.13.1 bjoern==3.2.2 -Flask==2.3.3 +blinker==1.6.3 +charset-normalizer==2.1.1 +click==8.1.7 +Flask==3.0.0 +flask-babel==4.0.0 +itsdangerous==2.1.2 Jinja2==3.1.2 +MarkupSafe==2.1.3 protobuf==3.20.2 +pytz==2023.3.post1 requests==2.31.0 simplepam==0.1.5 -flask_babel==2.0.0 ua-parser==0.16.1 vcgencmd==0.1.1 -werkzeug==2.3.7 +Werkzeug==3.0.1 diff --git a/python/web/src/return_code_mapper.py b/python/web/src/return_code_mapper.py index 95025d9b1b..dd5e43b95a 100644 --- a/python/web/src/return_code_mapper.py +++ b/python/web/src/return_code_mapper.py @@ -23,7 +23,7 @@ class ReturnCodeMapper: ReturnCodes.DOWNLOADFILETOISO_SUCCESS: _("Created CD-ROM ISO image with arguments \"%(value)s\""), ReturnCodes.DOWNLOADTODIR_SUCCESS: - _("%(file_name)s downloaded to %(save_dir)s"), + _("Downloaded file to %(target_path)s"), ReturnCodes.WRITEFILE_SUCCESS: _("File created: %(target_path)s"), ReturnCodes.WRITEFILE_COULD_NOT_WRITE: diff --git a/python/web/src/static/themes/classic/style.css b/python/web/src/static/themes/classic/style.css index 3c8b754a69..d77de45a06 100644 --- a/python/web/src/static/themes/classic/style.css +++ b/python/web/src/static/themes/classic/style.css @@ -21,6 +21,11 @@ td { margin: none; } +th { + color: white; + background-color: black; +} + h1 { color: white; font-size: 20px; @@ -97,6 +102,10 @@ div.flash div.info { background-color: #0d6efd; } +div.flash > div a { + display: none; +} + td.inactive { text-align: center; background-color: tan; @@ -203,3 +212,7 @@ div.throttle-notice > div a { div.throttle-notice > div a:hover { text-decoration: underline; } + +label.hidden { + display: none; +} diff --git a/python/web/src/static/themes/modern/icons/cloud-off.svg b/python/web/src/static/themes/modern/icons/cloud-off.svg new file mode 100644 index 0000000000..b53410adfa --- /dev/null +++ b/python/web/src/static/themes/modern/icons/cloud-off.svg @@ -0,0 +1 @@ + diff --git a/python/web/src/static/themes/modern/icons/cloud.svg b/python/web/src/static/themes/modern/icons/cloud.svg new file mode 100644 index 0000000000..448e1485bb --- /dev/null +++ b/python/web/src/static/themes/modern/icons/cloud.svg @@ -0,0 +1 @@ + diff --git a/python/web/src/static/themes/modern/icons/command.svg b/python/web/src/static/themes/modern/icons/command.svg new file mode 100644 index 0000000000..e4d7559593 --- /dev/null +++ b/python/web/src/static/themes/modern/icons/command.svg @@ -0,0 +1 @@ + diff --git a/python/web/src/static/themes/modern/icons/manual copy.svg b/python/web/src/static/themes/modern/icons/home.svg similarity index 55% rename from python/web/src/static/themes/modern/icons/manual copy.svg rename to python/web/src/static/themes/modern/icons/home.svg index 12ffcbc46c..7bb31b23dc 100644 --- a/python/web/src/static/themes/modern/icons/manual copy.svg +++ b/python/web/src/static/themes/modern/icons/home.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/python/web/src/static/themes/modern/icons/manual.svg b/python/web/src/static/themes/modern/icons/manual.svg deleted file mode 100644 index 12ffcbc46c..0000000000 --- a/python/web/src/static/themes/modern/icons/manual.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/web/src/static/themes/modern/style.css b/python/web/src/static/themes/modern/style.css index a21a91ed96..e057da98cf 100644 --- a/python/web/src/static/themes/modern/style.css +++ b/python/web/src/static/themes/modern/style.css @@ -62,6 +62,10 @@ div.notice { color: #fff; } +label.hidden { + display: none; +} + /* ------------------------------------------------------------------------------ Tables @@ -419,8 +423,8 @@ div.header div.authentication-disabled a { color: #fff; } - div.header div.login-status.logged-in a { - background: var(--danger) no-repeat right 0.5rem center; + div.header div.login-status.logged-in span.log-out-button a { + background: var(--primary) no-repeat right 0.5rem center; background-image: url("icons/log-out.svg"); background-size: var(--icon-size); border-radius: var(--border-radius); @@ -430,6 +434,17 @@ div.header div.authentication-disabled a { color: #fff; } + div.header div.login-status.logged-in span.admin-button a { + background: var(--secondary) no-repeat right 0.5rem center; + background-image: url("icons/command.svg"); + background-size: var(--icon-size); + border-radius: var(--border-radius); + padding: 0.25rem 2.25rem 0.25rem 0.75rem; + display: inline-block; + text-decoration: none; + color: #fff; + } + div.header div.login-status.logged-in span.logged-in-as-text { margin-right: 1rem; } @@ -530,7 +545,7 @@ div.flash > div { } div.flash > div a { - display: inline-block !important; + display: inline-block; padding: 0.25rem 0.75rem; margin-left: auto; color: #fff; @@ -935,23 +950,25 @@ section#system div.power-control { /* ------------------------------------------------------------------------------ - Index > Section: Manual + Admin > Section: Services ------------------------------------------------------------------------------ */ -section#manual { - margin: 2rem 0 1rem; +section#services ul.service-list { + list-style: none; + padding-left: 0; } -section#manual a { - margin: auto; - display: block; +section#services li.service-item { + margin-bottom: 0.5em; padding: 0.25rem 0 0.25rem 2rem; - background: url("icons/manual.svg") no-repeat left center; - font-weight: bold; } -section#manual a p { - margin: 0; +section#services li.enabled { + background: url("icons/cloud.svg") no-repeat left center; +} + +section#services li.disabled { + background: url("icons/cloud-off.svg") no-repeat left center; } /* @@ -1034,3 +1051,18 @@ body.page-manpage div.content p.home { margin-top: 2rem; font-weight: bold; } + +/* + ------------------------------------------------------------------------------ + Base > Back + ------------------------------------------------------------------------------ + */ +a.back { + padding: 0.25rem 0 0.25rem 2rem; + font-weight: bold; + background: url("icons/home.svg") no-repeat left center; +} + +a.back span.separator { + display: none; +} diff --git a/python/web/src/templates/admin.html b/python/web/src/templates/admin.html new file mode 100644 index 0000000000..c28d41a470 --- /dev/null +++ b/python/web/src/templates/admin.html @@ -0,0 +1,193 @@ +{% extends "base.html" %} +{% block content %} + +
+
+ + {{ _("Logging") }} + +
    +
  • {{ _("The current dropdown selection indicates the active log level.") }}
  • +
+
+ +
+
+ + + + + +
+
+ +
+
+ + + +
+
+
+ +
+ +
+
+ + {{ _("Appearance") }} + +
    +
  • {{ _("Theme and language are auto-detected for your user agent. Here you can change the default.") }}
  • +
  • {{ _("The System Name is the \"pretty\" hostname if set, with a fallback to the regular hostname.") }}
  • +
+
+ +
+
+ {{ _("The current theme is \"%(theme)s\".", theme=current_theme) }} + {% if current_theme == "classic" %} + {{ _('Switch to the %(theme)s theme', theme="modern") }} + {% else %} + {{ _('Switch to the %(theme)s theme', theme="classic") }} + {% endif %} +
+ +
+ + + +
+
+ +
+
+ + + +
+
+ + +
+
+
+ +
+ +
+
+ + {{ _("Companion Services") }} + +
    +
  • {{ _("If you want to add a service, run the easyinstall.sh script and choose the one to install.") }}
  • +
  • {{ _("In order to manage the services in the Web UI, you may install Webmin as well.") }}
  • +
+
+ +
+ +
+ +
+
+ + {{ _("System Operations") }} + +
    +
  • {{ _("IMPORTANT: Always shut down the system before turning off the power. Failing to do so may lead to data loss.") }}
  • +
+
+ +
+
+ +
+
+ +
+
+
+ +
+{% endblock content %} diff --git a/python/web/src/templates/base.html b/python/web/src/templates/base.html index 38cbe09ba1..0c555f72ce 100644 --- a/python/web/src/templates/base.html +++ b/python/web/src/templates/base.html @@ -32,7 +32,9 @@
{{ _("Logged in as %(username)s", username=env["username"]) }} - - {{ _("Log Out") }} + {{ _("Log Out") }} + - + {{ _("Settings") }}
{% else %}
@@ -93,7 +95,7 @@

{% else %}
{{ message }}
{% endif %} - +

{% endfor %} {% endif %} @@ -103,56 +105,17 @@

{{ content_class }} {% block content %}{% endblock content %} + {% if not is_root_page %} + + {% endif %}