1
0
forked from clan/clan-core

integrate static assets into webui command

This commit is contained in:
Jörg Thalheim 2023-08-25 17:08:39 +02:00
parent 79b02dffab
commit 9ae14a4f7d
11 changed files with 149 additions and 28 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
.direnv
result*
pkgs/clan-cli/clan_cli/nixpkgs
pkgs/clan-cli/clan_cli/webui/assets
# python
__pycache__

View File

@ -23,6 +23,18 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--no-open", action="store_true", help="Don't open the browser", default=False
)
parser.add_argument(
"--dev", action="store_true", help="Run in development mode", default=False
)
parser.add_argument(
"--dev-port",
type=int,
default=3000,
help="Port to listen on for the dev server",
)
parser.add_argument(
"--dev-host", type=str, default="localhost", help="Host to listen on"
)
parser.add_argument(
"--reload", action="store_true", help="Don't reload on changes", default=False
)

View File

@ -1,7 +1,10 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from .assets import asset_path
from .config import settings
from .routers import health, machines, root
@ -10,14 +13,18 @@ def setup_app() -> FastAPI:
app.include_router(health.router)
app.include_router(machines.router)
app.include_router(root.router)
# TODO make this configurable
app.add_middleware(
CORSMiddleware,
allow_origins="http://localhost:3000",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if settings.env.is_development():
# TODO make this configurable
app.add_middleware(
CORSMiddleware,
allow_origins="http://${settings.dev_host}:${settings.dev_port}",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
else:
app.mount("/static", StaticFiles(directory=asset_path()), name="static")
for route in app.routes:
if isinstance(route, APIRoute):

View File

@ -0,0 +1,7 @@
import functools
from pathlib import Path
@functools.cache
def asset_path() -> Path:
return Path(__file__).parent / "assets"

View File

@ -0,0 +1,38 @@
# config.py
import logging
import os
from enum import Enum
from pydantic import BaseSettings
logger = logging.getLogger(__name__)
class EnvType(Enum):
production = "production"
development = "development"
@staticmethod
def from_environment() -> "EnvType":
t = os.environ.get("CLAN_WEBUI_ENV", "production")
try:
return EnvType[t]
except KeyError:
logger.warning(f"Invalid environment type: {t}, fallback to production")
return EnvType.production
def is_production(self) -> bool:
return self == EnvType.production
def is_development(self) -> bool:
return self == EnvType.development
class Settings(BaseSettings):
env: EnvType = EnvType.from_environment()
dev_port: int = int(os.environ.get("CLAN_WEBUI_DEV_PORT", 3000))
dev_host: str = os.environ.get("CLAN_WEBUI_DEV_HOST", "localhost")
# global instance
settings = Settings()

View File

@ -1,9 +1,28 @@
import os
from mimetypes import guess_type
from pathlib import Path
from fastapi import APIRouter, Response
from ..assets import asset_path
router = APIRouter()
@router.get("/")
async def root() -> Response:
body = "<html><body><h1>Welcome</h1></body></html>"
return Response(content=body, media_type="text/html")
@router.get("/{path_name:path}")
async def root(path_name: str) -> Response:
if path_name == "":
path_name = "index.html"
filename = Path(os.path.normpath((asset_path() / path_name)))
if not filename.is_relative_to(asset_path()):
# prevent directory traversal
return Response(status_code=403)
if not filename.is_file():
print(filename)
print(asset_path())
return Response(status_code=404)
content_type, _ = guess_type(filename)
return Response(filename.read_bytes(), media_type=content_type)

View File

@ -1,12 +1,20 @@
import argparse
import logging
import os
import subprocess
import time
import urllib.request
import webbrowser
from contextlib import ExitStack, contextmanager
from pathlib import Path
from threading import Thread
from typing import Iterator
# XXX: can we dynamically load this using nix develop?
from uvicorn import run
logger = logging.getLogger(__name__)
def defer_open_browser(base_url: str) -> None:
for i in range(5):
@ -18,15 +26,41 @@ def defer_open_browser(base_url: str) -> None:
webbrowser.open(base_url)
@contextmanager
def spawn_node_dev_server() -> Iterator[None]:
logger.info("Starting node dev server...")
path = Path(__file__).parent.parent.parent.parent / "ui"
with subprocess.Popen(
["direnv", "exec", path, "npm", "run", "dev"],
cwd=path,
) as proc:
try:
yield
finally:
proc.terminate()
def start_server(args: argparse.Namespace) -> None:
if not args.no_open:
Thread(
target=defer_open_browser, args=(f"http://[{args.host}]:{args.port}",)
).start()
run(
"clan_cli.webui.app:app",
host=args.host,
port=args.port,
log_level=args.log_level,
reload=args.reload,
)
with ExitStack() as stack:
if args.dev:
os.environ["CLAN_WEBUI_ENV"] = "development"
os.environ["CLAN_WEBUI_DEV_PORT"] = str(args.dev_port)
os.environ["CLAN_WEBUI_DEV_HOST"] = args.dev_host
stack.enter_context(spawn_node_dev_server())
open_url = f"http://{args.dev_host}:{args.dev_port}"
else:
os.environ["CLAN_WEBUI_ENV"] = "production"
open_url = f"http://[{args.host}]:{args.port}"
if not args.no_open:
Thread(target=defer_open_browser, args=(open_url,)).start()
run(
"clan_cli.webui.app:app",
host=args.host,
port=args.port,
log_level=args.log_level,
reload=args.reload,
)

View File

@ -19,6 +19,7 @@
, zerotierone
, rsync
, pkgs
, ui-assets
}:
let
# This provides dummy options for testing clan config and prevents it from
@ -49,6 +50,7 @@ let
rm $out/clan_cli/config/jsonschema
cp -r ${self + /lib/jsonschema} $out/clan_cli/config/jsonschema
ln -s ${nixpkgs} $out/clan_cli/nixpkgs
ln -s ${ui-assets} $out/clan_cli/webui/assets
'';
nixpkgs = runCommand "nixpkgs" { } ''
mkdir -p $out/unfree

View File

@ -2,12 +2,12 @@
perSystem = { self', pkgs, ... }: {
devShells.clan-cli = pkgs.callPackage ./shell.nix {
inherit self;
inherit (self'.packages) clan-cli;
inherit (self'.packages) clan-cli ui-assets;
};
packages = {
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
inherit self;
zerotierone = self'.packages.zerotierone;
inherit (self'.packages) ui-assets zerotierone;
};
clan-openapi = self'.packages.clan-cli.clan-openapi;
default = self'.packages.clan-cli;

View File

@ -12,7 +12,7 @@ scripts = { clan = "clan_cli:main" }
exclude = ["clan_cli.nixpkgs*"]
[tool.setuptools.package-data]
clan_cli = ["config/jsonschema/*"]
clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"]
[tool.pytest.ini_options]
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail"

View File

@ -1,4 +1,4 @@
{ self, clan-cli, pkgs }:
{ self, clan-cli, pkgs, ui-assets }:
let
pythonWithDeps = pkgs.python3.withPackages (
ps:
@ -26,8 +26,9 @@ pkgs.mkShell {
shellHook = ''
tmp_path=$(realpath ./.direnv)
rm -f clan_cli/nixpkgs
rm -f clan_cli/nixpkgs clan_cli/assets
ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs
ln -sf ${ui-assets} clan_cli/webui/assets
export PATH="$tmp_path/bin:${checkScript}/bin:$PATH"
export PYTHONPATH="$PYTHONPATH:$(pwd)"