forked from clan/clan-core
integrate static assets into webui command
This commit is contained in:
parent
79b02dffab
commit
9ae14a4f7d
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.direnv
|
||||
result*
|
||||
pkgs/clan-cli/clan_cli/nixpkgs
|
||||
pkgs/clan-cli/clan_cli/webui/assets
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
7
pkgs/clan-cli/clan_cli/webui/assets.py
Normal file
7
pkgs/clan-cli/clan_cli/webui/assets.py
Normal file
@ -0,0 +1,7 @@
|
||||
import functools
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@functools.cache
|
||||
def asset_path() -> Path:
|
||||
return Path(__file__).parent / "assets"
|
38
pkgs/clan-cli/clan_cli/webui/config.py
Normal file
38
pkgs/clan-cli/clan_cli/webui/config.py
Normal 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()
|
@ -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)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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)"
|
||||
|
Loading…
Reference in New Issue
Block a user