integrate static assets into webui command
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.direnv
|
.direnv
|
||||||
result*
|
result*
|
||||||
pkgs/clan-cli/clan_cli/nixpkgs
|
pkgs/clan-cli/clan_cli/nixpkgs
|
||||||
|
pkgs/clan-cli/clan_cli/webui/assets
|
||||||
|
|
||||||
# python
|
# python
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|||||||
@@ -23,6 +23,18 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-open", action="store_true", help="Don't open the browser", default=False
|
"--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(
|
parser.add_argument(
|
||||||
"--reload", action="store_true", help="Don't reload on changes", default=False
|
"--reload", action="store_true", help="Don't reload on changes", default=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.routing import APIRoute
|
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
|
from .routers import health, machines, root
|
||||||
|
|
||||||
|
|
||||||
@@ -10,14 +13,18 @@ def setup_app() -> FastAPI:
|
|||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
app.include_router(machines.router)
|
app.include_router(machines.router)
|
||||||
app.include_router(root.router)
|
app.include_router(root.router)
|
||||||
# TODO make this configurable
|
|
||||||
app.add_middleware(
|
if settings.env.is_development():
|
||||||
CORSMiddleware,
|
# TODO make this configurable
|
||||||
allow_origins="http://localhost:3000",
|
app.add_middleware(
|
||||||
allow_credentials=True,
|
CORSMiddleware,
|
||||||
allow_methods=["*"],
|
allow_origins="http://${settings.dev_host}:${settings.dev_port}",
|
||||||
allow_headers=["*"],
|
allow_credentials=True,
|
||||||
)
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
app.mount("/static", StaticFiles(directory=asset_path()), name="static")
|
||||||
|
|
||||||
for route in app.routes:
|
for route in app.routes:
|
||||||
if isinstance(route, APIRoute):
|
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 fastapi import APIRouter, Response
|
||||||
|
|
||||||
|
from ..assets import asset_path
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/{path_name:path}")
|
||||||
async def root() -> Response:
|
async def root(path_name: str) -> Response:
|
||||||
body = "<html><body><h1>Welcome</h1></body></html>"
|
if path_name == "":
|
||||||
return Response(content=body, media_type="text/html")
|
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 argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
from contextlib import ExitStack, contextmanager
|
||||||
|
from pathlib import Path
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
# XXX: can we dynamically load this using nix develop?
|
# XXX: can we dynamically load this using nix develop?
|
||||||
from uvicorn import run
|
from uvicorn import run
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def defer_open_browser(base_url: str) -> None:
|
def defer_open_browser(base_url: str) -> None:
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
@@ -18,15 +26,41 @@ def defer_open_browser(base_url: str) -> None:
|
|||||||
webbrowser.open(base_url)
|
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:
|
def start_server(args: argparse.Namespace) -> None:
|
||||||
if not args.no_open:
|
with ExitStack() as stack:
|
||||||
Thread(
|
if args.dev:
|
||||||
target=defer_open_browser, args=(f"http://[{args.host}]:{args.port}",)
|
os.environ["CLAN_WEBUI_ENV"] = "development"
|
||||||
).start()
|
os.environ["CLAN_WEBUI_DEV_PORT"] = str(args.dev_port)
|
||||||
run(
|
os.environ["CLAN_WEBUI_DEV_HOST"] = args.dev_host
|
||||||
"clan_cli.webui.app:app",
|
|
||||||
host=args.host,
|
stack.enter_context(spawn_node_dev_server())
|
||||||
port=args.port,
|
|
||||||
log_level=args.log_level,
|
open_url = f"http://{args.dev_host}:{args.dev_port}"
|
||||||
reload=args.reload,
|
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
|
, zerotierone
|
||||||
, rsync
|
, rsync
|
||||||
, pkgs
|
, pkgs
|
||||||
|
, ui-assets
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# This provides dummy options for testing clan config and prevents it from
|
# This provides dummy options for testing clan config and prevents it from
|
||||||
@@ -49,6 +50,7 @@ let
|
|||||||
rm $out/clan_cli/config/jsonschema
|
rm $out/clan_cli/config/jsonschema
|
||||||
cp -r ${self + /lib/jsonschema} $out/clan_cli/config/jsonschema
|
cp -r ${self + /lib/jsonschema} $out/clan_cli/config/jsonschema
|
||||||
ln -s ${nixpkgs} $out/clan_cli/nixpkgs
|
ln -s ${nixpkgs} $out/clan_cli/nixpkgs
|
||||||
|
ln -s ${ui-assets} $out/clan_cli/webui/assets
|
||||||
'';
|
'';
|
||||||
nixpkgs = runCommand "nixpkgs" { } ''
|
nixpkgs = runCommand "nixpkgs" { } ''
|
||||||
mkdir -p $out/unfree
|
mkdir -p $out/unfree
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
perSystem = { self', pkgs, ... }: {
|
perSystem = { self', pkgs, ... }: {
|
||||||
devShells.clan-cli = pkgs.callPackage ./shell.nix {
|
devShells.clan-cli = pkgs.callPackage ./shell.nix {
|
||||||
inherit self;
|
inherit self;
|
||||||
inherit (self'.packages) clan-cli;
|
inherit (self'.packages) clan-cli ui-assets;
|
||||||
};
|
};
|
||||||
packages = {
|
packages = {
|
||||||
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||||
inherit self;
|
inherit self;
|
||||||
zerotierone = self'.packages.zerotierone;
|
inherit (self'.packages) ui-assets zerotierone;
|
||||||
};
|
};
|
||||||
clan-openapi = self'.packages.clan-cli.clan-openapi;
|
clan-openapi = self'.packages.clan-cli.clan-openapi;
|
||||||
default = self'.packages.clan-cli;
|
default = self'.packages.clan-cli;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ scripts = { clan = "clan_cli:main" }
|
|||||||
exclude = ["clan_cli.nixpkgs*"]
|
exclude = ["clan_cli.nixpkgs*"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
clan_cli = ["config/jsonschema/*"]
|
clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail"
|
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
|
let
|
||||||
pythonWithDeps = pkgs.python3.withPackages (
|
pythonWithDeps = pkgs.python3.withPackages (
|
||||||
ps:
|
ps:
|
||||||
@@ -26,8 +26,9 @@ pkgs.mkShell {
|
|||||||
shellHook = ''
|
shellHook = ''
|
||||||
tmp_path=$(realpath ./.direnv)
|
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 ${clan-cli.nixpkgs} clan_cli/nixpkgs
|
||||||
|
ln -sf ${ui-assets} clan_cli/webui/assets
|
||||||
|
|
||||||
export PATH="$tmp_path/bin:${checkScript}/bin:$PATH"
|
export PATH="$tmp_path/bin:${checkScript}/bin:$PATH"
|
||||||
export PYTHONPATH="$PYTHONPATH:$(pwd)"
|
export PYTHONPATH="$PYTHONPATH:$(pwd)"
|
||||||
|
|||||||
Reference in New Issue
Block a user