Skip to content

Commit

Permalink
Feature/UI/new components (#19)
Browse files Browse the repository at this point in the history
* wip

* use metaclass to instantiate a blitzui

* fix text is finished trigger in gpt component

* fix lint and typing

* wip

* wip

* New heritence class version

* Add new mecanism in components

* Add new composant and Iframe

* Add ng variable

* Delete .zed/settings.json

* add some component and refacto

* clean

* continue adding component and remove js as much as possible

* clean

* make passing args and kwargs in component make them used in render

* clean

* use image components

* add doc

* update components

* fix ruff

* Update blitz/ui/components/buttons/base.py

---------

Co-authored-by: Maxime de Pachtere <[email protected]>
  • Loading branch information
pbrochar and mde-pach authored Mar 3, 2024
1 parent 10eac90 commit 04a4179
Show file tree
Hide file tree
Showing 71 changed files with 1,531 additions and 830 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,4 @@ database.db
.nicegui/
demo-blitz-app/
.idea/
.ruff_cache/
5 changes: 4 additions & 1 deletion blitz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import importlib.metadata

from .core import BlitzCore

__version__ = importlib.metadata.version("blitz")

__all__ = [
"BlitzCore",
]
__version__ = "0.1.0"
24 changes: 7 additions & 17 deletions blitz/api/logs.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from loguru import logger
import logging
import inspect
import logging
import sys
from typing import TYPE_CHECKING

from loguru import logger

from blitz.app import BlitzApp

if TYPE_CHECKING:
Expand All @@ -22,26 +23,15 @@ def emit(self, record: logging.LogRecord) -> None:
except ValueError:
level = record.levelno

if record.name in ("uvicorn.access",):
if record.args[2].startswith("/projects"): # type: ignore
record.name += ".ui"
elif record.args[2].startswith("/api"): # type: ignore
record.name += ".api"
elif record.args[2].startswith("/admin"): # type: ignore
record.name += ".admin"

# Find caller from where originated the logged message.
frame, depth = inspect.currentframe(), 0
while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
frame = frame.f_back
depth += 1
if record.name in ["uvicorn.access.ui", "uvicorn.access.admin"]:
pass
else:
logger.opt(
depth=depth,
exception=record.exc_info,
).log(level, record.getMessage())
logger.opt(
depth=depth,
exception=record.exc_info,
).log(level, record.getMessage())


def filter_logs(record: logging.LogRecord, blitz_app: BlitzApp) -> bool:
Expand Down
16 changes: 4 additions & 12 deletions blitz/cli/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ class BlitzProjectCreator:
DEMO_BLITZ_APP_DESCRIPTION = "This is a demo blitz app"
DEMO_BLITZ_APP_NAME = "Demo Blitz App"

def __init__(
self, name: str, description: str, file_format: str, demo: bool = False
) -> None:
def __init__(self, name: str, description: str, file_format: str, demo: bool = False) -> None:
self.name = name
self.description = description
self.file_format = file_format
Expand Down Expand Up @@ -95,9 +93,7 @@ def create_file_or_exit(self) -> None:
raise typer.Exit(code=1)

def print_success_message(self) -> None:
print(
f"\n[medium_purple1 bold]{self.name}[/medium_purple1 bold] created successfully !"
)
print(f"\n[medium_purple1 bold]{self.name}[/medium_purple1 bold] created successfully !")
print("To start your app, you can use:")
print(f" [bold medium_purple1]blitz start {self.path}[/bold medium_purple1]")

Expand Down Expand Up @@ -127,9 +123,7 @@ def _write_blitz_file(self) -> Path:
raise Exception()
match self.file_format:
case "json":
blitz_file_data = self.blitz_file.model_dump_json(
indent=4, by_alias=True, exclude_unset=True
)
blitz_file_data = self.blitz_file.model_dump_json(indent=4, by_alias=True, exclude_unset=True)
case "yaml":
blitz_file_data = yaml.dump(
self.blitz_file.model_dump(by_alias=True, exclude_unset=True),
Expand All @@ -151,9 +145,7 @@ def _print_file_error(error: Exception) -> None:

@staticmethod
def _print_directory_error(error: Exception) -> None:
print(
f"[red bold]Error[/red bold] while creating the blitz app in the file system: {error}"
)
print(f"[red bold]Error[/red bold] while creating the blitz app in the file system: {error}")


def create_blitz_app(
Expand Down
4 changes: 1 addition & 3 deletions blitz/ui/blitz_ui.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from functools import lru_cache
from pathlib import Path
from typing import Any

from blitz.app import BlitzApp
from blitz.core import BlitzCore
from blitz.settings import Settings, get_settings
from blitz.tools.erd import generate_mermaid_erd


# @lru_cache
# def get_erd(app: BlitzApp) -> str:
# return generate_mermaid_erd(app._base_resource_model.metadata)
Expand All @@ -30,7 +30,6 @@ def current_project(self) -> str | None:

@current_project.setter
def current_project(self, project: str) -> None:
print(project)
self._current_project = project

@property
Expand Down Expand Up @@ -95,7 +94,6 @@ def _get_preprompt(self) -> str:

def reset_preprompt(self) -> None:
self.preprompt = self._get_preprompt()
print(self.preprompt)


@lru_cache
Expand Down
5 changes: 5 additions & 0 deletions blitz/ui/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .base import BaseComponent as Component

__all__ = [
"Component",
]
File renamed without changes.
62 changes: 62 additions & 0 deletions blitz/ui/components/accordion/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from nicegui import ui
from typing import Callable
from blitz.ui.components.base import BaseComponent


class BaseExpansion(BaseComponent[ui.expansion]):
def __init__(
self,
text: str = "",
caption: str | None = None,
icon: str | None = None,
group: str | None = None,
value: bool = False,
on_value_change: Callable[..., None] | None = None,
props: str = "",
classes: str = "",
) -> None:
self.text = text
self.caption = caption
self.icon = icon
self.group = group
self.value = value
self.on_value_change = on_value_change
super().__init__(props=props, classes=classes)

def render(self) -> None:
self.ng = (
ui.expansion(
self.text,
caption=self.caption,
icon=self.icon,
group=self.group,
value=self.value,
on_value_change=self.on_value_change,
)
.classes(self.classes)
.props(self.props)
)


class BaseAccordion(BaseExpansion):
def __init__(
self,
text: str = "",
caption: str | None = None,
icon: str | None = None,
group: str | None = None,
is_open: bool = False,
on_change: Callable[..., None] | None = None,
props: str = "",
classes: str = "",
) -> None:
super().__init__(
text=text,
caption=caption,
icon=icon,
group=group,
value=is_open,
on_value_change=on_change,
props=props,
classes=classes,
)
152 changes: 147 additions & 5 deletions blitz/ui/components/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,150 @@
import time
from typing import Any, Generic, Protocol, Self, TypeVar, cast, overload

from nicegui import ui
from nicegui.element import Element
from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui


class BaseComponent:
def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None:
self.blitz_ui: BlitzUI = blitz_ui
self.current_project = blitz_ui.current_project
self.current_app = blitz_ui.current_app
class NiceGUIComponent(Protocol):
def __enter__(self) -> Any:
...

def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
...


V = TypeVar("V", bound=Any)


# Get the blitz_ui through a metaclass
class BaseComponentMeta(type):
def __new__(
cls,
name: str,
bases: tuple[type, ...],
namespace: dict[str, Any],
*,
reactive: bool = False,
render: bool = True,
) -> type:
blitz_ui = get_blitz_ui()
namespace["blitz_ui"] = blitz_ui
namespace["reactive"] = reactive
namespace["_render"] = render
return super().__new__(cls, name, bases, namespace)


class BaseComponent(Generic[V], metaclass=BaseComponentMeta):
def __init__(
self,
*args: Any,
props: str = "",
classes: str = "",
render: bool | None = None,
**kwargs: Any,
) -> None:
self._ng: Element
self.props = props
self.classes = classes
self.blitz_ui: BlitzUI
self.reactive: bool
self._render: bool
if render is not None:
self._render = render
self.current_project = self.blitz_ui.current_project
self.current_app = self.blitz_ui.current_app
self.kwargs = kwargs

self.blitz_ui = get_blitz_ui()
if self.reactive:
self.render = ui.refreshable(self.render) # type: ignore
if self._render:
self.render(*args, **kwargs)

@overload
def render(self) -> None:
...

@overload
def render(self, *args: Any, **kwargs: Any) -> None:
...

def render(self, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError

def __call__(self, *args: Any, **kwargs: Any) -> Any:
self.render(*args, **kwargs)

def refresh(self, *args: Any, **kwargs: Any) -> None:
if hasattr(self.render, "refresh"):
self.render.refresh(*args, **kwargs) # type: ignore

@property
def ng(self) -> Element:
return self._ng

@ng.setter
def ng(self, value: Element) -> None:
self._ng = value

@classmethod
def variant(
cls, name: str = "", *, props: str = "", classes: str = "", render: bool = True, **kwargs: Any
) -> type[Self]:
"""
Create a new type (class) based on the current component class with specified props and classes.
:param props: The properties to be predefined in the new class.
:param classes: The CSS classes to be predefined in the new class.
:return: A new type (class) that is a variant of the current class with predefined props and classes.
"""
if not name:
new_type_name = f'{cls.__name__}_{str(time.time()).replace(".","")}'
else:
new_type_name = f"{name}{cls.__name__}"

if hasattr(cls, "props"):
props = f"{getattr(cls, 'props')} {props}"
if hasattr(cls, "classes"):
classes = f"{getattr(cls, 'classes')} {classes}"

return type(
new_type_name,
(cls,),
{
"props": props,
"classes": classes,
"kwargs": kwargs,
},
render=render,
)

def __enter__(self) -> V:
if hasattr(self.ng, "__enter__"):
return cast(V, self.ng.__enter__())
raise NotImplementedError

def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
if hasattr(self.ng, "__exit__"):
self.ng.__exit__(exc_type, exc_value, traceback)
else:
raise NotImplementedError

def __new__(cls, *args: Any, **kwargs: Any) -> Self:
instance = super().__new__(cls)
for parent in cls.mro():
if hasattr(parent, "classes"):
setattr(instance, "classes", getattr(parent, "classes"))
if hasattr(parent, "props"):
setattr(instance, "props", getattr(parent, "props"))
return instance

def __setattr__(self, __name: str, __value: Any) -> None:
"""If the attribute is classes or props, then append the new value to the existing value."""
if __name in ["props", "classes"] and hasattr(self, __name):
if __value in getattr(self, __name):
return
__value = f"{getattr(self, __name)} {__value}"

return super().__setattr__(__name, __value)
7 changes: 7 additions & 0 deletions blitz/ui/components/buttons/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .flat import FlatButton
from .base import BaseButton as Button

__all__ = [
"FlatButton",
"Button",
]
Loading

0 comments on commit 04a4179

Please sign in to comment.