Skip to content

Commit

Permalink
Adds CLI + fixes static assets
Browse files Browse the repository at this point in the history
We can now install and get all static assets. Also adds CLI for
admin/user work -- TODO -- split it out and do dynamic imports...
  • Loading branch information
elijahbenizzy committed Feb 23, 2024
1 parent 944e09d commit db09a37
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 21 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,6 @@ cython_debug/
#.idea/
#
_build

# Symlink gnerated in build
burr/tracking/server/build
Empty file added burr/cli/__init__.py
Empty file.
124 changes: 124 additions & 0 deletions burr/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import os
import shutil
import subprocess
import threading
import time
import webbrowser
from contextlib import contextmanager

import click
import requests
from loguru import logger


def _command(command: str) -> str:
"""Runs a simple command"""
logger.info(f"Running command: {command}")
if isinstance(command, str):
command = command.split(" ")
try:
output = subprocess.check_output(command, stderr=subprocess.PIPE, shell=False)
return output.decode().strip() # If command is successful, print the output.
except subprocess.CalledProcessError as e:
print(e.stdout.decode())
print(e.stderr.decode())
raise e


def _get_git_root() -> str:
return _command("git rev-parse --show-toplevel")


def open_when_ready(check_url: str, open_url: str):
while True:
try:
response = requests.get(check_url)
if response.status_code == 200:
webbrowser.open(open_url)
return
else:
pass
except requests.exceptions.RequestException:
pass
time.sleep(1)


@contextmanager
def cd(path):
old_dir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old_dir)


@click.group()
def cli():
pass


def _build_ui():
cmd = "npm run build --prefix telemetry/ui"
_command(cmd)
# create a symlink so we can get packages inside it...
cmd = "ln -s telemetry/ui/build burr/tracking/server/build"
_command(cmd)


@cli.command()
def build_ui():
git_root = _get_git_root()
with cd(git_root):
_build_ui()


@cli.command()
@click.option("--port", default=7241, help="Port to run the server on")
@click.option("--dev-mode", is_flag=True, help="Run the server in development mode")
@click.option("--no-open", is_flag=True, help="Run the server without opening it")
def run_server(port: int, dev_mode: bool, no_open: bool):
# TODO: Implement server running logic here
# Example: Start a web server, configure ports, etc.
logger.info(f"Starting server on port {port}")
cmd = f"uvicorn burr.tracking.server.run:app --port {port}"
if dev_mode:
cmd += " --reload"
if not no_open:
thread = threading.Thread(
target=open_when_ready,
kwargs={
"open_url": (open_url := f"http://localhost:{port}"),
"check_url": f"{open_url}/ready",
},
daemon=True,
)
thread.start()
_command(cmd)


@cli.command(help="Publishes the package to a repository")
@click.option("--prod", is_flag=True, help="Publish to pypi (rather than test pypi)")
@click.option("--no-wipe-dist", is_flag=True, help="Wipe the dist/ directory before building")
def build_and_publish(prod: bool, no_wipe_dist: bool):
git_root = _get_git_root()
with cd(git_root):
logger.info("Building UI -- this may take a bit...")
# _build_ui()
logger.info("Built UI!")
if not no_wipe_dist:
logger.info("Wiping dist/ directory for a clean publish.")
shutil.rmtree("dist", ignore_errors=True)
_command("python3 -m build")
repository = "pypi" if prod else "testpypi"
_command(f"python3 -m twine upload --repository {repository} dist/*")
logger.info(f"Published to {repository}! 🎉")


# quick trick to expose every subcommand as a variable
# will create a command called `cli_{command}` for every command we have
for key, command in cli.commands.items():
globals()[f'cli_{key.replace("-", "_")}'] = command

if __name__ == "__main__":
cli()
31 changes: 31 additions & 0 deletions burr/tracking/server/run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import os
from importlib.resources import files
from typing import Sequence

import uvicorn
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from starlette.templating import Jinja2Templates

from burr.tracking.server import backend, schema
from burr.tracking.server.schema import ApplicationLogs
Expand Down Expand Up @@ -43,3 +48,29 @@ async def get_application_logs(request: Request, project_id: str, app_id: str) -
:return: A list of steps with all associated step data
"""
return await backend.get_application_logs(request, project_id=project_id, app_id=app_id)


@app.get("/api/v0/ready")
async def ready() -> bool:
return True


BASE_ASSET_DIRECTORY = str(files("burr").joinpath("tracking/server/build"))

templates = Jinja2Templates(directory=BASE_ASSET_DIRECTORY)
app.mount("/static", StaticFiles(directory=os.path.join(BASE_ASSET_DIRECTORY, "static")), "/static")
# public assets in create react app don't get put under build/static, we need to route them over
app.mount("/public", StaticFiles(directory=BASE_ASSET_DIRECTORY, html=True), "/public")


@app.get("/{rest_of_path:path}")
async def react_app(req: Request, rest_of_path: str):
"""Quick trick to server the react app
Thanks to https://github.com/hop-along-polly/fastapi-webapp-react for the example/demo
"""
return templates.TemplateResponse("index.html", {"request": req})


if __name__ == "__main__":
port = int(os.getenv("PORT", 8000)) # Default to 8000 if no PORT environment variable is set
uvicorn.run(app, host="0.0.0.0", port=port)
1 change: 0 additions & 1 deletion burr/tracking/server/run.sh

This file was deleted.

36 changes: 33 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,53 @@ tracking-client = [
]

tracking-server = [
"click",
"fastapi",
"uvicorn",
"pydantic",
"fastapi-pagination",
"aiofiles"
"aiofiles",
"requests",
"jinja2",
]

tracking = [
"burr[tracking-client]",
"burr[tracking-server]"
"burr[tracking-server]",
"loguru",
]

# just install everything for developers
developer = [
"burr[streamlit]",
"burr[graphviz]",
"burr[tracking]",
"burr[tests]",
"burr[documentation]",
"loguru", # I love loguru TBH, but not for any prod stuff
"build",
"twine",
]

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
include = ["burr"]
include = ["burr", "burr.*"]

# we need to ensure this is there...
[tool.setuptools.package-data]
burr = [
"burr/tracking/server/build/**/*",
]

[project.urls]
Homepage = "https://github.com/dagworks-inc/burr"
Documentation = "https://github.com/dagworks-inc/burr"
Repository = "https://github.com/dagworks-inc/burr"
"Bug Tracker" = "https://github.com/dagworks-inc/burr"

[project.scripts]
server = "burr.cli.__main__:cli_run_server"
publish = "burr.cli.__main__:cli_build_and_publish"
build-ui = "burr.cli.__main__:cli_build_ui"
11 changes: 8 additions & 3 deletions telemetry/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ import { ProjectList } from './components/routes/ProjectList';
import { AppList } from './components/routes/AppList';
import { AppView } from './components/routes/app/AppView';
import { QueryClient, QueryClientProvider } from 'react-query';
import { OpenAPI } from './api';
import { AppContainer } from './components/nav/appcontainer';

// TODO -- unhardcode
OpenAPI.BASE = 'http://localhost:7241';
/**
* Basic application. We have an AppContainer -- this has a breadcrumb and a sidebar.
* We refer to route paths to gather parameters, as its simple to wire through. We may
* want to consider passing parameters in to avoid overly coupled dependencies/ensure
* more reusable top-level components.
*
* Note that you can run this in one of two modes:
* 1. As an asset served by the backend
* 2. Standalone, using npm run
*
* npm run will proxy to port 7241, versus the asset which will
* hit the backend (on the same port/server as the FE, its just a static route).
*
* @returns A rendered application object
*/
const App = () => {
Expand Down
32 changes: 18 additions & 14 deletions telemetry/ui/src/components/nav/appcontainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-900/80" />
</Transition.Child>

Expand All @@ -113,7 +114,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full">
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<Transition.Child
as={Fragment}
Expand All @@ -122,12 +124,14 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
leaveTo="opacity-0"
>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
className="-m-2.5 p-2.5"
onClick={() => setSmallSidebarOpen(false)}>
onClick={() => setSmallSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
Expand All @@ -136,11 +140,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2 py-2">
<div className="flex h-16 shrink-0 items-center">
<img
className="h-10 w-auto"
src={process.env.PUBLIC_URL + '/logo.png'}
alt="Burr"
/>
<img className="h-10 w-auto" src={'/logo.png'} alt="Burr" />
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
Expand All @@ -159,7 +159,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
? 'text-gray-700 hover:text-dwdarkblue'
: 'text-gray-700 hover:text-dwdarkblue hover:bg-gray-50',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}>
)}
>
<item.icon
className={classNames(
item.current
Expand Down Expand Up @@ -188,11 +189,12 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
<div
className={`hidden ${
sidebarOpen ? 'h-screen lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col' : ''
}`}>
}`}
>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 py-2">
<div className="flex h-16 shrink-0 items-center">
<img className="h-12 w-auto" src={process.env.PUBLIC_URL + '/logo.png'} alt="Burr" />
<img className="h-12 w-auto" src={'/public/logo.png'} alt="Burr" />
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-7">
Expand All @@ -211,7 +213,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
? 'text-gray-700 hover:text-dwdarkblue'
: 'text-gray-700 hover:text-dwdarkblue hover:bg-gray-50',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold'
)}>
)}
>
<item.icon
className={classNames(
item.current
Expand Down Expand Up @@ -239,7 +242,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
!sidebarOpen
? 'lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-8 lg:flex-col justify-end lg:py-2 lg:px-1'
: ''
}`}>
}`}
>
<ToggleOpenButton open={sidebarOpen} toggleSidebar={toggleSidebar} />
</div>

Expand Down

0 comments on commit db09a37

Please sign in to comment.