diff --git a/.gitignore b/.gitignore index 99d5061..565b0a5 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ database.db .nicegui/ demo-blitz-app/ .idea/ +.ruff_cache/ \ No newline at end of file diff --git a/blitz/__init__.py b/blitz/__init__.py index 116174e..25f5261 100644 --- a/blitz/__init__.py +++ b/blitz/__init__.py @@ -1,6 +1,9 @@ +import importlib.metadata + from .core import BlitzCore +__version__ = importlib.metadata.version("blitz") + __all__ = [ "BlitzCore", ] -__version__ = "0.1.0" diff --git a/blitz/api/logs.py b/blitz/api/logs.py index 3bd3d22..20f531d 100644 --- a/blitz/api/logs.py +++ b/blitz/api/logs.py @@ -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: @@ -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: diff --git a/blitz/cli/commands/create.py b/blitz/cli/commands/create.py index 12120f9..175a2d9 100644 --- a/blitz/cli/commands/create.py +++ b/blitz/cli/commands/create.py @@ -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 @@ -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]") @@ -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), @@ -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( diff --git a/blitz/ui/blitz_ui.py b/blitz/ui/blitz_ui.py index 2c689fc..00532b0 100644 --- a/blitz/ui/blitz_ui.py +++ b/blitz/ui/blitz_ui.py @@ -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) @@ -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 @@ -95,7 +94,6 @@ def _get_preprompt(self) -> str: def reset_preprompt(self) -> None: self.preprompt = self._get_preprompt() - print(self.preprompt) @lru_cache diff --git a/blitz/ui/components/__init__.py b/blitz/ui/components/__init__.py index e69de29..f75f24e 100644 --- a/blitz/ui/components/__init__.py +++ b/blitz/ui/components/__init__.py @@ -0,0 +1,5 @@ +from .base import BaseComponent as Component + +__all__ = [ + "Component", +] diff --git a/blitz/ui/domains/__init__.py b/blitz/ui/components/accordion/__init__.py similarity index 100% rename from blitz/ui/domains/__init__.py rename to blitz/ui/components/accordion/__init__.py diff --git a/blitz/ui/components/accordion/base.py b/blitz/ui/components/accordion/base.py new file mode 100644 index 0000000..705e1ba --- /dev/null +++ b/blitz/ui/components/accordion/base.py @@ -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, + ) diff --git a/blitz/ui/components/base.py b/blitz/ui/components/base.py index 3e0eded..9e92c33 100644 --- a/blitz/ui/components/base.py +++ b/blitz/ui/components/base.py @@ -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) diff --git a/blitz/ui/components/buttons/__init__.py b/blitz/ui/components/buttons/__init__.py new file mode 100644 index 0000000..9c09469 --- /dev/null +++ b/blitz/ui/components/buttons/__init__.py @@ -0,0 +1,7 @@ +from .flat import FlatButton +from .base import BaseButton as Button + +__all__ = [ + "FlatButton", + "Button", +] diff --git a/blitz/ui/components/buttons/base.py b/blitz/ui/components/buttons/base.py new file mode 100644 index 0000000..040bbac --- /dev/null +++ b/blitz/ui/components/buttons/base.py @@ -0,0 +1,36 @@ +from typing import Any +from nicegui import ui +from nicegui.ui import button as NGButton +from blitz.ui.components.base import BaseComponent + + +class BaseButton(BaseComponent[ui.button]): + def __init__( + self, + text: str = "", + *, + on_click: Any | None = None, + color: str | None = "primary", + icon: str | None = None, + props: str = "", + classes: str = "", + **kwargs: Any, + ) -> None: + self.text = text + self.on_click = on_click + self.color = color + self.icon = icon + super().__init__(props=props, classes=classes, **kwargs) + + + def render(self) -> None: + self.ng: NGButton = ( + ui.button( + self.text, + on_click=self.on_click, + color=self.color, + icon=self.icon, + ) + .props(self.props) + .classes(self.classes) + ) diff --git a/blitz/ui/components/buttons/flat.py b/blitz/ui/components/buttons/flat.py new file mode 100644 index 0000000..95869fc --- /dev/null +++ b/blitz/ui/components/buttons/flat.py @@ -0,0 +1,5 @@ +from .base import BaseButton + + +class FlatButton(BaseButton.variant(props="flat")): # type: ignore + """Flat button.""" diff --git a/blitz/ui/components/buttons/icon.py b/blitz/ui/components/buttons/icon.py new file mode 100644 index 0000000..7aac05f --- /dev/null +++ b/blitz/ui/components/buttons/icon.py @@ -0,0 +1,24 @@ +from typing import Any +from .flat import FlatButton + + +class IconButton(FlatButton.variant(props="dense")): # type: ignore + def __init__( + self, + icon: str, + on_click: Any | None = None, + color: str = "transparent", + icon_color: str = "secondary", + icon_size: str = "xm", + props: str = "", + classes: str = "", + **kwargs: Any, + ): + super().__init__( + on_click=on_click, + color=color, + icon=icon, + props=f"{props} color={icon_color} size={icon_size}", + classes=classes, + **kwargs, + ) diff --git a/blitz/ui/components/buttons/outline.py b/blitz/ui/components/buttons/outline.py new file mode 100644 index 0000000..f6a4b9c --- /dev/null +++ b/blitz/ui/components/buttons/outline.py @@ -0,0 +1,3 @@ +from .base import BaseButton + +OutlineButton = BaseButton.variant(props="outline") diff --git a/blitz/ui/components/drawers/__init__.py b/blitz/ui/components/drawers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blitz/ui/components/drawers/base.py b/blitz/ui/components/drawers/base.py new file mode 100644 index 0000000..6cec436 --- /dev/null +++ b/blitz/ui/components/drawers/base.py @@ -0,0 +1,47 @@ +from nicegui import ui + +from blitz.ui.components.base import BaseComponent + + +class BaseLeftDrawer(BaseComponent[ui.left_drawer]): + def __init__( + self, + value: bool | None = None, + fixed: bool = True, + bordered: bool = False, + elevated: bool = False, + top_corner: bool = False, + bottom_corner: bool = True, + props: str = "", + classes: str = "", + ) -> None: + self.value = value + self.fixed = fixed + self.bordered = bordered + self.elevated = elevated + self.top_corner = top_corner + self.bottom_corner = bottom_corner + super().__init__(props=props, classes=classes) + + def toggle(self) -> None: + self.ng.toggle() + + def show(self) -> None: + self.ng.show() + + def hide(self) -> None: + self.ng.hide() + + def render(self) -> None: + self.ng = ( + ui.left_drawer( + value=self.value, + fixed=self.fixed, + bordered=self.bordered, + elevated=self.elevated, + top_corner=self.top_corner, + bottom_corner=self.bottom_corner, + ) + .props(self.props) + .classes(self.classes) + ) diff --git a/blitz/ui/components/drawers/dashboard.py b/blitz/ui/components/drawers/dashboard.py new file mode 100644 index 0000000..588d196 --- /dev/null +++ b/blitz/ui/components/drawers/dashboard.py @@ -0,0 +1,17 @@ +from blitz.ui.components.links.menu_link import MenuLink +from .base import BaseLeftDrawer + + +class DashboardDrawer(BaseLeftDrawer.variant(classes="px-0 bg-[#14151a]", props="width=200")): # type: ignore + def __init__(self, drawer_open: bool) -> None: + super().__init__(value=drawer_open) + + def render(self) -> None: + super().render() + with self: + MenuLink("Dashboard", f"/projects/{self.current_project}", "dashboard") + MenuLink("Admin", f"{self.blitz_ui.localhost_url}/admin/", "table_chart") + MenuLink("Swagger", f"/projects/{self.current_project}/swagger", "api") + MenuLink("Blitz File", f"/projects/{self.current_project}/blitz-file", "article") + MenuLink("Diagram", f"/projects/{self.current_project}/diagram", "account_tree") + MenuLink("Logs", f"/projects/{self.current_project}/logs", "list") diff --git a/blitz/ui/components/element/__init__.py b/blitz/ui/components/element/__init__.py new file mode 100644 index 0000000..b33905c --- /dev/null +++ b/blitz/ui/components/element/__init__.py @@ -0,0 +1,5 @@ +from .base import BaseElement as Element + +__all__ = [ + "Element", +] diff --git a/blitz/ui/components/element/base.py b/blitz/ui/components/element/base.py new file mode 100644 index 0000000..093f078 --- /dev/null +++ b/blitz/ui/components/element/base.py @@ -0,0 +1,29 @@ +from nicegui import Client, ui + +from blitz.ui.components.base import BaseComponent + + +class BaseElement(BaseComponent[ui.element]): + def __init__( + self, tag: str | None = None, _client: Client | None = None, props: str = "", classes: str = "" + ) -> None: + self.tag = tag + self._client = _client + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ui.element(tag=self.tag, _client=self._client).props(self.props).classes(self.classes) + + +class IFrame(BaseElement): # type: ignore + """ + IFrame element. + + args: + src: str - URL to load in the iframe. + frameborder: int - Frame border width. + """ + + def __init__(self, src: str, frameborder: int, props: str = "", classes: str = "") -> None: + props = f"src={src} frameborder={frameborder} {props}" + super().__init__("iframe", props=props, classes=classes) diff --git a/blitz/ui/components/gpt_chat_components.py b/blitz/ui/components/gpt_chat_components.py index 132de9f..d557115 100644 --- a/blitz/ui/components/gpt_chat_components.py +++ b/blitz/ui/components/gpt_chat_components.py @@ -7,10 +7,18 @@ from pydantic import ValidationError from blitz.models.blitz.file import BlitzFile from openai.types.chat import ChatCompletionMessageParam - - +from blitz.ui.components import notify +from blitz.ui.components.buttons import FlatButton +from blitz.ui.components.icon import Icon import yaml +from blitz.ui.components.buttons.icon import IconButton +from blitz.ui.components.labels.error import ErrorLabel +from blitz.ui.components.labels.base import BoldLabel +from blitz.ui.components.markdown.base import BaseMarkdown, MarkdownResponse +from blitz.ui.components.rows import WFullItemsCenterRow +from blitz.ui.components.rows.base import ItemsCenterRow, WFullRow + class ResponseJSON: def __init__(self, text: str) -> None: @@ -23,7 +31,7 @@ def __init__(self, text: str) -> None: self.color = self._get_color(self.is_valid_blitz_file) self._expansion: Expansion | None = None - self._expansion_is_open = True + self._expansion_is_open = self.is_valid_blitz_file self._dialog: Dialog | None = None @staticmethod @@ -58,22 +66,17 @@ def extract_json(text: str) -> Any: return json.loads(match.group(1)) async def copy_code(self) -> None: - ui.run_javascript("navigator.clipboard.writeText(`" + str(self.json) + "`)") - ui.notify("Copied to clipboard", type="info", color="green") + # Can't put it in a file without a better integration like a bridge to js function or something like this + ui.run_javascript(f"navigator.clipboard.writeText(`{json.dumps(self.json, indent=4)}`)") + notify.info("Copied to clipboard") def action_buttons(self) -> None: - with ui.row(wrap=False).classes("items-center"): - ui.button( - icon="content_copy", - color="transparent", - on_click=self.copy_code, - ).props("dense flat size=xm color=grey") + with ItemsCenterRow(wrap=False): if self._dialog is None: # TODO: handle error raise Exception - ui.button(icon="file_download", color="transparent", on_click=self._dialog.open).props( - "dense flat size=xm color=grey" - ) + IconButton(icon="content_copy", icon_color="grey", on_click=self.copy_code) + IconButton(icon="file_download", icon_color="grey", on_click=self._dialog.open) def download_dialog(self) -> None: with ui.dialog() as self._dialog, ui.card().classes("w-full px-4"): @@ -81,12 +84,9 @@ def download_dialog(self) -> None: self.invalid_blitz_file() # with ui.expansion("Edit File", icon="edit").classes("w-full h-auto rounded-lg border-solid border overflow-hidden grow overflow-hidden"): # JsonEditorComponent(self.json).render() - with ui.row().classes("w-full justify-end"): - ui.button( - "Export as JSON", - on_click=self._download_json, - ).props("flat") - ui.button("Export as YAML", on_click=self._download_yaml).props("flat") + with WFullRow(classes="justify-end"): + FlatButton("Export as JSON", on_click=self._download_json) + FlatButton("Export as YAML", on_click=self._download_yaml) def _download_json(self) -> None: ui.download( @@ -101,9 +101,9 @@ def _get_filename(self, extension: str) -> str: return f"{self.blitz_app_title.replace(' ', '_').replace('.', '_').lower()}.{extension}" def invalid_blitz_file(self) -> None: - with ui.row().classes("items-center"): + with ItemsCenterRow(): ui.icon("error", color="red", size="sm") - ui.label("This is not a valid Blitz file.").classes("text-red") + ErrorLabel("This is not a valid Blitz file.") def _toggle_expansion(self) -> None: self._expansion_is_open = not self._expansion_is_open @@ -115,7 +115,7 @@ def _toggle_expansion(self) -> None: @ui.refreshable def render(self) -> None: self.download_dialog() - with ui.row(wrap=False).classes("items-center w-full"): + with WFullItemsCenterRow(wrap=False): with ui.expansion( self.blitz_app_title, icon="settings_suggest", @@ -126,19 +126,10 @@ def render(self) -> None: ) as self._expansion: if not self.is_valid_blitz_file: self.invalid_blitz_file() - ui.markdown(self.text) + BaseMarkdown(self.text) self.action_buttons() -class MarkdownResponse: - def __init__(self, text: str) -> None: - self.text = text - - @ui.refreshable - def render(self) -> None: - ui.markdown(self.text) - - class GPTChatComponent: def __init__( self, @@ -156,13 +147,13 @@ def __init__( @ui.refreshable def render(self) -> None: - with ui.row(wrap=False).classes("w-full"): + with WFullRow(wrap=False): ui.space().classes("w-1/3") with ui.column().classes("justify-start w-2/3"): - with ui.row(wrap=False).classes("items-center w-full"): + with ItemsCenterRow(wrap=False): with ui.avatar(color=self.avatar_color).props("size=sm"): - ui.icon(self.icon, size="xs", color="white") - ui.label(self.label).classes("font-bold") + Icon(self.icon, size="xs", color="white") + BoldLabel(self.label) if self.text_components: for component in self.text_components: @@ -170,7 +161,7 @@ def render(self) -> None: component.render() else: with ui.element().classes("px-10"): - ui.markdown(self.text) + BaseMarkdown(self.text) ui.space().classes("w-1/3") def as_gpt_dict(self) -> ChatCompletionMessageParam: @@ -210,7 +201,7 @@ class GPTResponse(GPTChatComponent): def __init__(self, text: str = "", text_is_finished: bool = False) -> None: super().__init__(label=self.LABEL, text=text, icon=self.ICON, avatar_color=self.AVATAR_COLOR) - self._text_is_finished = text_is_finished + self._text_is_finished: bool self.text_is_finished = text_is_finished def add(self, text: str) -> None: diff --git a/blitz/ui/components/grids/__init__.py b/blitz/ui/components/grids/__init__.py new file mode 100644 index 0000000..bdf43ae --- /dev/null +++ b/blitz/ui/components/grids/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseGrid + +__all__ = ["BaseGrid"] diff --git a/blitz/ui/components/grids/base.py b/blitz/ui/components/grids/base.py new file mode 100644 index 0000000..59a08f9 --- /dev/null +++ b/blitz/ui/components/grids/base.py @@ -0,0 +1,17 @@ +from blitz.ui.components.base import BaseComponent +from nicegui import ui + + +class BaseGrid(BaseComponent[ui.grid]): + def __init__(self, rows: int | None = None, columns: int | None = None, props: str = "", classes: str = "") -> None: + self.ng: ui.grid + self.rows = rows + self.columns = columns + super().__init__(props=props, classes=classes) + + def render(self) -> None: # type: ignore + self.ng = ui.grid(rows=self.rows, columns=self.columns).props(self.props).classes(self.classes) + + +class WFullGrid(BaseGrid.variant(classes="w-full")): # type: ignore + """Grid with w-full class.""" diff --git a/blitz/ui/components/header.py b/blitz/ui/components/header.py index 801c3e4..7cac4a2 100644 --- a/blitz/ui/components/header.py +++ b/blitz/ui/components/header.py @@ -1,41 +1,62 @@ from pathlib import Path +from typing import Self from nicegui import ui -from nicegui.page_layout import LeftDrawer -from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui -from blitz.ui.components.base import BaseComponent + +from blitz.ui.components import Component +from blitz.ui.components.buttons.icon import IconButton +from blitz.ui.components.drawers.dashboard import DashboardDrawer +from blitz.ui.components.element import Element +from blitz.ui.components.icon import Icon +from blitz.ui.components.image import Image +from blitz.ui.components.labels import Label +from blitz.ui.components.links import Link +from blitz.ui.components.rows import ItemsCenterContentCenterRow +from blitz.ui.components.tooltip import Tooltip MAIN_PINK = "#cd87ff" DARK_PINK = "#a72bff" -class HeaderMenuComponent: - def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: - pass +class HeaderElement(Component[ui.link]): + def __init__(self, label: str, link: str, new_tab: bool = False) -> None: + self.label = label + self.link = link + self.new_tab = new_tab + super().__init__() def render(self) -> None: - ui.button(icon="menu").props("flat") + with Link(target=self.link, new_tab=self.new_tab) as self.ng: + Label(self.label) + def disabled(self) -> Self: + self.ng.classes("disabled") + # Can't find better implementation to keep ui.link contract + self.ng._props["href"] = "" + return self + + +class HeaderComponent(Component[ui.header], reactive=True): + ThemeButton = IconButton.variant(props="fab-mini disabled") -class HeaderComponent: def __init__( self, title: str = "", - blitz_ui: BlitzUI = get_blitz_ui(), - drawer: LeftDrawer | None = None, + drawer: DashboardDrawer | None = None, ) -> None: + self.drawer = drawer self.title = title - self.blitz_ui = blitz_ui self.dark_mode = ui.dark_mode(value=True) - self.home_link = f"dashboard/projects/{blitz_ui.current_project}" if blitz_ui.current_project else "projects" - self.drawer = drawer - ui.add_head_html(f"") + self.home_link = ( + f"dashboard/projects/{self.blitz_ui.current_project}" if self.blitz_ui.current_project else "projects" + ) + # Need to refacto in another way than using components + ui.add_head_html(f"") ui.add_head_html( f"" ) - ui.colors( primary="fffafa", secondary="#a72bff", @@ -43,98 +64,52 @@ def __init__( positive="#53B689", dark="#3e3e42", ) + super().__init__() + + def dark_mode_button(self) -> None: + # Can be factorised for sure + with Element(): + Tooltip("White mode is coming soon") + self.ThemeButton( + icon="dark_mode", + icon_color="black", + on_click=lambda: self.dark_mode.set_value(not self.dark_mode.value), + ).ng.bind_visibility_from(self.dark_mode, "value", value=False) + self.ThemeButton( + icon="light_mode", + icon_color="white", + on_click=lambda: self.dark_mode.set_value(not self.dark_mode.value), + ).ng.bind_visibility_from(self.dark_mode, "value", value=True) def render(self) -> None: with ui.header(bordered=True).classes("pl-1 pr-8 justify-between content-center h-16 backdrop-blur-sm"): - with ui.row().classes("items-center space-x-20 content-center my-auto"): - with ui.row().classes("items-center space-x-0 content-center "): + with ItemsCenterContentCenterRow(classes="space-x-20 my-auto"): + with ItemsCenterContentCenterRow(classes="space-x-0"): if self.drawer is not None: - ui.button(icon="menu", on_click=self.drawer.toggle).props("flat") - ui.icon(name="bolt", color=DARK_PINK, size="32px") - with ui.link(target=f"/projects/{self.blitz_ui.current_project}"): - ui.label("Blitz Dashboard") - - with ui.row().classes("items-center justify-between content-center"): - with ui.link(target=f"{self.blitz_ui.localhost_url}/projects").classes("disabled"): - ui.tooltip("Multiple App management is coming soon") - ui.label("Projects") - with ui.link(target="/gpt"): - ui.label("GPT Builder") - with ui.link(target="https://paperz-org.github.io/blitz/", new_tab=True): - ui.label("Documentation") - with ui.row().classes("items-center content-center my-auto"): - with ui.element(): - ui.button( - icon="dark_mode", - on_click=lambda: self.dark_mode.set_value(True), - ).props("flat fab-mini color=black disabled").bind_visibility_from( - self.dark_mode, "value", value=False - ) - ui.button( - icon="light_mode", - on_click=lambda: self.dark_mode.set_value(False), - ).props("flat fab-mini color=white disabled").bind_visibility_from( - self.dark_mode, "value", value=True - ) - ui.tooltip("White mode is coming soon") - with ui.link(target="https://github.com/Paperz-org/blitz", new_tab=True).classes(" w-8"): - ui.image(Path(__file__).parent.parent / "./assets/github_white.png").classes("w-8 ") - - -class MenuLink: - def __init__(self, label: str, link: str, icon: str) -> None: - self.label = label - self.link = link - self.icon = icon - - def render(self) -> None: - with ui.link(target=self.link).classes("w-full"), ui.button(on_click=self.go_to).props( - "flat align=left" - ).classes("px-4 hover:bg-slate-700 rounded-sm w-full") as self.button: - ui.icon(name=self.icon, size="sm").props("flat").classes("pr-4") - ui.label(self.label) - - def go_to(self) -> None: - ui.open(self.link) - - -class FrameComponent(BaseComponent): - def __init__( - self, - show_drawer: bool = True, - drawer_open: bool = True, - ) -> None: - super().__init__() + IconButton(icon="menu", on_click=self.drawer.toggle) + Icon(name="bolt", color=DARK_PINK, size="32px") + HeaderElement(label="Blitz Dashboard", link=f"/projects/{self.blitz_ui.current_project}") + + with ItemsCenterContentCenterRow(classes="justify-between"): + with HeaderElement(label="Projects", link=f"{self.blitz_ui.localhost_url}/projects").disabled(): + Tooltip("Multiple App management is coming soon") + HeaderElement(label="GPT Builder", link="/gpt") + HeaderElement("Documentation", "https://paperz-org.github.io/blitz/", new_tab=True) + with ItemsCenterContentCenterRow(classes="my-auto"): + self.dark_mode_button() + with HeaderElement("", "https://paperz-org.github.io/blitz/", new_tab=True): + Image(Path(__file__).parent.parent / "./assets/github_white.png", classes="w-8") + + +class FrameComponent(Component[None]): + def __init__(self, show_drawer: bool = True, drawer_open: bool = True) -> None: self.show_drawer = show_drawer self.drawer_open = drawer_open - # Only for declarative - self.drawer: LeftDrawer | None = None - - def left_drawer(self) -> None: - with ui.left_drawer(value=self.drawer_open, fixed=True, bottom_corner=True).props("width=200").classes( - "px-0 bg-[#14151a]" - ) as self.drawer: - MenuLink("Dashboard", f"/projects/{self.current_project}", "dashboard").render() - MenuLink( - "Admin", - f"{self.blitz_ui.localhost_url}/admin/", - "table_chart", - ).render() - MenuLink("Swagger", f"/projects/{self.current_project}/swagger", "api").render() - MenuLink( - "Blitz File", - f"/projects/{self.current_project}/blitz-file", - "article", - ).render() - MenuLink( - "Diagram", - f"/projects/{self.current_project}/diagram", - "account_tree", - ).render() - MenuLink("Logs", f"/projects/{self.current_project}/logs", "list").render() + self.drawer: DashboardDrawer | None = None + super().__init__() def render(self) -> None: if self.show_drawer and self.blitz_ui.current_project is not None: - self.left_drawer() - HeaderComponent(drawer=self.drawer).render() + self.drawer = DashboardDrawer(drawer_open=self.drawer_open) + HeaderComponent(drawer=self.drawer) diff --git a/blitz/ui/components/icon/__init__.py b/blitz/ui/components/icon/__init__.py new file mode 100644 index 0000000..8f645a9 --- /dev/null +++ b/blitz/ui/components/icon/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseIcon as Icon + +__all__ = ["Icon"] diff --git a/blitz/ui/components/icon/base.py b/blitz/ui/components/icon/base.py new file mode 100644 index 0000000..0412317 --- /dev/null +++ b/blitz/ui/components/icon/base.py @@ -0,0 +1,23 @@ +from typing import Any +from nicegui import ui + +from blitz.ui.components.base import BaseComponent + + +class BaseIcon(BaseComponent[ui.icon]): + def __init__( + self, + name: str = "", + size: str | None = None, + color: str | None = None, + props: str = "", + classes: str = "", + **kwargs: Any, + ) -> None: + self.name = name + self.size = size + self.color = color + super().__init__(props=props, classes=classes, **kwargs) + + def render(self) -> None: + self.ng = ui.icon(name=self.name, size=self.size, color=self.color).props(self.props).classes(self.classes) diff --git a/blitz/ui/components/image/__init__.py b/blitz/ui/components/image/__init__.py new file mode 100644 index 0000000..6ec5f13 --- /dev/null +++ b/blitz/ui/components/image/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from nicegui import ui + +from blitz.ui.components import Component + + +class Image(Component[ui.image]): + def __init__(self, src: str | Path, props: str = "", classes: str = "") -> None: + self.src = src + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ui.image(self.src).props(self.props).classes(self.classes) diff --git a/blitz/ui/components/json_editor.py b/blitz/ui/components/json_editor.py index cc3fe58..2d5478e 100644 --- a/blitz/ui/components/json_editor.py +++ b/blitz/ui/components/json_editor.py @@ -5,7 +5,10 @@ import yaml from blitz.models.blitz.file import BlitzFile from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui +from blitz.ui.components.buttons import FlatButton from blitz.ui.components.header import DARK_PINK, MAIN_PINK +from blitz.ui.components import notify +from blitz.ui.components.rows.base import JustifyBetweenRow class JsonEditorComponent: @@ -60,21 +63,22 @@ def reset_content(self) -> None: self.content = self._original_content self.editor.run_editor_method("update", {"json": self.content}) app.storage.user["blitz_file_content"] = self.content - ui.notify("Content Reset", type="positive") + + notify.success("Content Reset") def validate(self) -> None: try: BlitzFile.from_dict(self.content) except ValidationError: - ui.notify("Invalid Blitz File", type="negative") + notify.error("Invalid Blitz File") else: - ui.notify("Valid Blitz File", type="positive") + notify.success("Valid Blitz File") def save(self) -> None: try: BlitzFile.from_dict(self.content) except ValidationError: - ui.notify("Invalid Blitz File", type="negative") + notify.error("Invalid Blitz File") return try: if self.blitz_ui.current_app is None: @@ -89,18 +93,18 @@ def save(self) -> None: elif self.blitz_ui.current_app.file.file_type == BlitzFile.FileType.YAML: f.write(yaml.dump(self.content, indent=4)) except Exception: - ui.notify("Error While Saving File", type="negative") + notify.error("Error While Saving File") else: - ui.notify("Content Saved", type="positive") + notify.success("Content Saved") def render(self) -> None: with ui.row().classes("w-full justify-between align-center p-4 rounded-lg border"): - with ui.row().classes("justify-between"): + with JustifyBetweenRow(): ui.switch("Edit BlitzFile", on_change=self.enable_editor) - ui.button("Reset", on_click=self.reset_content, icon="restart_alt").props("flat") - with ui.row().classes("justify-between"): - ui.button("Validate", on_click=self.validate, icon="verified").props("flat") - ui.button("Save", on_click=self.save, icon="save").props("flat") + FlatButton("Reset", on_click=self.reset_content, icon="restart_alt") + with JustifyBetweenRow(): + FlatButton("Validate", on_click=self.validate, icon="verified") + FlatButton("Save", on_click=self.save, icon="save") self.editor = ( ui.json_editor( { diff --git a/blitz/ui/components/labels/__init__.py b/blitz/ui/components/labels/__init__.py new file mode 100644 index 0000000..e4880ea --- /dev/null +++ b/blitz/ui/components/labels/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseLabel as Label, RedLabel + +__all__ = ["Label", "RedLabel"] diff --git a/blitz/ui/components/labels/base.py b/blitz/ui/components/labels/base.py new file mode 100644 index 0000000..8d797c8 --- /dev/null +++ b/blitz/ui/components/labels/base.py @@ -0,0 +1,59 @@ +from blitz.ui.components.base import BaseComponent +from nicegui import ui + + +class BaseLabel(BaseComponent[ui.label]): + def __init__(self, text: str = "", props: str = "", classes: str = "") -> None: + self.text = text + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ui.label(self.text).props(self.props).classes(self.classes) + + +class RedLabel(BaseLabel.variant(classes="text-red")): # type: ignore + """Label with text-red-500 class.""" + + +class BoldLabel(BaseLabel.variant(classes="font-bold")): # type: ignore + """Label with font-bold class.""" + + +class TextXsLabel(BaseLabel.variant(classes="text-xs")): # type: ignore + """Label with text-xs class.""" + + +class TextSmLabel(BaseLabel.variant(classes="text-sm")): # type: ignore + """Label with text-sm class.""" + + +class TextMdLabel(BaseLabel.variant(classes="text-md")): # type: ignore + """Label with text-md class.""" + + +class TextLgLabel(BaseLabel.variant(classes="text-lg")): # type: ignore + """Label with text-lg class.""" + + +class TextXlLabel(BaseLabel.variant(classes="text-xl")): # type: ignore + """Label with text-xl class.""" + + +class Text2XlLabel(BaseLabel.variant(classes="text-2xl")): # type: ignore + """Label with text-2xl class.""" + + +class TextMdBoldLabel(BoldLabel, TextMdLabel): + """Label with text-xs font-bold classes.""" + + +class TextLgBoldLabel(BoldLabel, TextLgLabel): + """Label with text-lg font-bold classes.""" + + +class TextXlBoldLabel(BoldLabel, TextXlLabel): + """Label with text-xl font-bold classes.""" + + +class Text2XlBoldLabel(BoldLabel, Text2XlLabel): + """Label with text-2xl font-bold classes.""" diff --git a/blitz/ui/components/labels/error.py b/blitz/ui/components/labels/error.py new file mode 100644 index 0000000..c2fdc45 --- /dev/null +++ b/blitz/ui/components/labels/error.py @@ -0,0 +1,3 @@ +from .base import RedLabel + +ErrorLabel = RedLabel diff --git a/blitz/ui/components/links/__init__.py b/blitz/ui/components/links/__init__.py new file mode 100644 index 0000000..d712381 --- /dev/null +++ b/blitz/ui/components/links/__init__.py @@ -0,0 +1,5 @@ +from .base import BaseLink as Link + +__all__ = [ + "Link", +] diff --git a/blitz/ui/components/links/base.py b/blitz/ui/components/links/base.py new file mode 100644 index 0000000..d8c3978 --- /dev/null +++ b/blitz/ui/components/links/base.py @@ -0,0 +1,22 @@ +from blitz.ui.components.base import BaseComponent +from nicegui import ui +from nicegui.element import Element + + +class BaseLink(BaseComponent[ui.link]): + def __init__( + self, text: str = "", target: str | Element = "#", new_tab: bool = False, props: str = "", classes: str = "" + ) -> None: + self.text = text + self.target = target + self.new_tab = new_tab + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ( + ui.link(text=self.text, target=self.target, new_tab=self.new_tab).props(self.props).classes(self.classes) + ) + + +class WFullLink(BaseLink.variant(classes="w-full")): # type: ignore + """Link with w-full class.""" diff --git a/blitz/ui/components/links/menu_link.py b/blitz/ui/components/links/menu_link.py new file mode 100644 index 0000000..e2197a2 --- /dev/null +++ b/blitz/ui/components/links/menu_link.py @@ -0,0 +1,24 @@ +from blitz.ui.components.buttons.flat import FlatButton +from .base import WFullLink +from blitz.ui.components.labels import Label +from nicegui import ui + + +class MenuLink(WFullLink): + _FlatButton = FlatButton.variant(props="align=left", classes="px-4 hover:bg-slate-700 rounded-sm w-full") + + def __init__(self, label: str, link: str, icon: str | None = None) -> None: + self.label = label + self.link = link + self.icon = icon + super().__init__() + + def render(self) -> None: + super().render() + with self, self._FlatButton(on_click=self.go_to): + if self.icon is not None: + ui.icon(name=self.icon, size="sm").props("flat").classes("pr-4") + Label(self.label) + + def go_to(self) -> None: + ui.open(self.link) diff --git a/blitz/ui/components/logger.py b/blitz/ui/components/logger.py index 92d5815..59c7c79 100644 --- a/blitz/ui/components/logger.py +++ b/blitz/ui/components/logger.py @@ -1,29 +1,30 @@ import logging -from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui + from nicegui import ui -from blitz.api.logs import InterceptHandler +from blitz.api.logs import InterceptHandler +from blitz.ui.components.base import BaseComponent -class LogElementHandler(InterceptHandler): - """A logging handler that emits messages to a log element.""" - def __init__(self, element: ui.log, level: int = logging.NOTSET) -> None: - self.element = element - super().__init__(level) +class LogComponent(BaseComponent[ui.log]): + class LogHandler(InterceptHandler): + """A logging handler that emits messages to a log element.""" - def emit(self, record: logging.LogRecord) -> None: - try: - if record.name != "uvicorn.access.ui": - self.element.push(record.getMessage()) - except Exception: - self.handleError(record) + def __init__(self, log: ui.log, level: int = logging.NOTSET) -> None: + self.log = log + super().__init__(level) + def emit(self, record: logging.LogRecord) -> None: + try: + if record.name != "uvicorn.access.ui": + self.log.push(record.getMessage()) + except Exception: + self.handleError(record) -class LogComponent: - def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: - self.blitz_ui = blitz_ui + def __init__(self) -> None: self._logger = logging.getLogger("uvicorn.access") + super().__init__() def render(self) -> None: - log = ui.log(max_lines=None).classes("w-full h-64 text-sm") - self._logger.addHandler(LogElementHandler(log)) + self.ng = ui.log(max_lines=None).classes("w-full h-64 text-sm") + self._logger.addHandler(self.LogHandler(self.ng)) diff --git a/blitz/ui/components/markdown/__init__.py b/blitz/ui/components/markdown/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blitz/ui/components/markdown/base.py b/blitz/ui/components/markdown/base.py new file mode 100644 index 0000000..a55f70a --- /dev/null +++ b/blitz/ui/components/markdown/base.py @@ -0,0 +1,24 @@ +from nicegui import ui + + +from blitz.ui.components.base import BaseComponent + + +class BaseMarkdown(BaseComponent[ui.markdown], reactive=True): + def __init__( + self, + content: str = "", + extras: list[str] = ["fenced-code-blocks", "tables"], + classes: str = "", + props: str = "", + ) -> None: + self.content = content + self.extras = extras + super().__init__(classes=classes, props=props) + + def render(self) -> None: + self.ng = ui.markdown(content=self.content, extras=self.extras).props(self.props).classes(self.classes) + + +class MarkdownResponse(BaseMarkdown, render=False): + """Don't render by default""" diff --git a/blitz/ui/components/notify/__init__.py b/blitz/ui/components/notify/__init__.py new file mode 100644 index 0000000..c515c56 --- /dev/null +++ b/blitz/ui/components/notify/__init__.py @@ -0,0 +1,11 @@ +from .success import success +from .error import error +from .info import info +from .warning import warning + +__all__ = [ + "success", + "error", + "info", + "warning", +] diff --git a/blitz/ui/components/notify/error.py b/blitz/ui/components/notify/error.py new file mode 100644 index 0000000..2f7b626 --- /dev/null +++ b/blitz/ui/components/notify/error.py @@ -0,0 +1,5 @@ +from nicegui import ui + + +def error(message: str) -> None: + ui.notify(message, type="negative") diff --git a/blitz/ui/components/notify/info.py b/blitz/ui/components/notify/info.py new file mode 100644 index 0000000..da7acff --- /dev/null +++ b/blitz/ui/components/notify/info.py @@ -0,0 +1,6 @@ +from typing import Any +from nicegui import ui + + +def info(message: Any) -> None: + ui.notify(message=message, type="info") diff --git a/blitz/ui/components/notify/success.py b/blitz/ui/components/notify/success.py new file mode 100644 index 0000000..26d7aff --- /dev/null +++ b/blitz/ui/components/notify/success.py @@ -0,0 +1,6 @@ +from typing import Any +from nicegui import ui + + +def success(message: Any) -> None: + ui.notify(message=message, type="positive") diff --git a/blitz/ui/components/notify/warning.py b/blitz/ui/components/notify/warning.py new file mode 100644 index 0000000..c3be53b --- /dev/null +++ b/blitz/ui/components/notify/warning.py @@ -0,0 +1,6 @@ +from typing import Any +from nicegui import ui + + +def warning(message: Any) -> None: + ui.notify(message=message, type="warning") diff --git a/blitz/ui/components/rows/__init__.py b/blitz/ui/components/rows/__init__.py new file mode 100644 index 0000000..7df93b8 --- /dev/null +++ b/blitz/ui/components/rows/__init__.py @@ -0,0 +1,19 @@ +from .base import ( + BaseRow, + WFullRow, + ContentCenterRow, + ItemsCenterRow, + WFullItemsCenterRow, + WFullContentCenterRow, + ItemsCenterContentCenterRow, +) + +__all__ = [ + "BaseRow", + "WFullRow", + "ContentCenterRow", + "ItemsCenterRow", + "WFullItemsCenterRow", + "WFullContentCenterRow", + "ItemsCenterContentCenterRow", +] diff --git a/blitz/ui/components/rows/base.py b/blitz/ui/components/rows/base.py new file mode 100644 index 0000000..bd0db2d --- /dev/null +++ b/blitz/ui/components/rows/base.py @@ -0,0 +1,40 @@ +from nicegui import ui + +from blitz.ui.components.base import BaseComponent + + +class BaseRow(BaseComponent[ui.row]): + def __init__(self, wrap: bool = True, props: str = "", classes: str = "") -> None: + self.wrap = wrap + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ui.row(wrap=self.wrap).props(self.props).classes(self.classes) + + +class WFullRow(BaseRow.variant(classes="w-full")): # type: ignore + """Row with w-full class.""" + + +class ContentCenterRow(BaseRow.variant(classes="content-center")): # type: ignore + """Row with content-center class.""" + + +class ItemsCenterRow(BaseRow.variant(classes="items-center")): # type: ignore + """Row with items-center class.""" + + +class JustifyBetweenRow(BaseRow.variant(classes="justify-between")): # type: ignore + """Row with justify-between class.""" + + +class WFullItemsCenterRow(WFullRow, ItemsCenterRow): + """Row with w-full and items-center classes.""" + + +class WFullContentCenterRow(WFullRow, ContentCenterRow): + """Row with w-full and content-center classes.""" + + +class ItemsCenterContentCenterRow(ItemsCenterRow, ContentCenterRow): + """Row with items-center and content-center classes.""" diff --git a/blitz/ui/components/status.py b/blitz/ui/components/status.py index 67d8cf4..7516f55 100644 --- a/blitz/ui/components/status.py +++ b/blitz/ui/components/status.py @@ -1,16 +1,30 @@ -from nicegui import ui +from typing import Any -from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui from httpx import AsyncClient +from blitz.ui.components.base import BaseComponent +from blitz.ui.components.grids.base import BaseGrid as Grid +from blitz.ui.components.icon.base import BaseIcon as Icon +from blitz.ui.components.labels.base import TextLgBoldLabel +from blitz.ui.components.timer.base import BaseTimer as Timer -class StatusComponent: - def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: - self.blitz_ui = blitz_ui - self.app = self.blitz_ui.current_app - self.api_up = False - self.admin_up = False - ui.timer(10.0, self.set_status) + +class StatusComponent(BaseComponent[Grid], reactive=True): + _GreenIcon = Icon("check_circle", classes="text-green-500", render=False) + _RedIcon = Icon("error", classes="text-red-500", render=False) + + api_up: bool = False + admin_up: bool = False + + def __init__( + self, + *args: Any, + props: str = "", + classes: str = "", + **kwargs: Any, + ) -> None: + Timer(10.0, self._set_status) + super().__init__(*args, props=props, classes=classes, **kwargs) async def _is_api_up(self) -> bool: async with AsyncClient() as client: @@ -22,21 +36,14 @@ async def _is_admin_up(self) -> bool: response = await client.get(f"{self.blitz_ui.localhost_url}/admin/") return response.status_code == 200 - async def set_status(self) -> None: + async def _set_status(self) -> None: self.api_up = await self._is_api_up() self.admin_up = await self._is_admin_up() - self.render.refresh() + self.refresh() - @ui.refreshable def render(self) -> None: - with ui.grid(rows=2, columns=2).classes("gap-4"): - ui.label("API:").classes("text-lg font-bold") - if self.api_up: - ui.icon(name="check_circle").classes("text-green-500") - else: - ui.icon(name="error").classes("text-red-500") - ui.label("Admin:").classes("text-lg font-bold") - if self.admin_up: - ui.icon(name="check_circle").classes("text-green-500") - else: - ui.icon(name="error").classes("text-red-500") + with Grid(rows=2, columns=2, classes="gap-4") as self.ng: # type: ignore + TextLgBoldLabel("API:") + self._GreenIcon() if self.api_up else self._RedIcon() + TextLgBoldLabel("Admin:") + self._GreenIcon() if self.admin_up else self._RedIcon() diff --git a/blitz/ui/components/timer/__init__.py b/blitz/ui/components/timer/__init__.py new file mode 100644 index 0000000..8a20efe --- /dev/null +++ b/blitz/ui/components/timer/__init__.py @@ -0,0 +1,3 @@ +from .base import BaseTimer + +__all__ = ["BaseTimer"] diff --git a/blitz/ui/components/timer/base.py b/blitz/ui/components/timer/base.py new file mode 100644 index 0000000..dd6651e --- /dev/null +++ b/blitz/ui/components/timer/base.py @@ -0,0 +1,29 @@ +from typing import Callable +from nicegui import ui +from blitz.ui.components.base import BaseComponent + +from typing import Any + + +class BaseTimer(BaseComponent[ui.timer]): + def __init__( + self, + interval: float, + callback: Callable[..., Any], + active: bool = True, + once: bool = False, + props: str = "", + classes: str = "", + ) -> None: + self.interval = interval + self.callback = callback + self.active = active + self.once = once + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ( + ui.timer(self.interval, self.callback, once=self.once, active=self.active) + .props(self.props) + .classes(self.classes) + ) diff --git a/blitz/ui/components/tooltip/__init__.py b/blitz/ui/components/tooltip/__init__.py new file mode 100644 index 0000000..bdd5e69 --- /dev/null +++ b/blitz/ui/components/tooltip/__init__.py @@ -0,0 +1,11 @@ +from nicegui import ui +from blitz.ui.components.base import BaseComponent + + +class Tooltip(BaseComponent[ui.tooltip]): + def __init__(self, text: str, props: str = "", classes: str = "") -> None: + self.text = text + super().__init__(props=props, classes=classes) + + def render(self) -> None: + self.ng = ui.tooltip(self.text).props(self.props).classes(self.classes) diff --git a/blitz/ui/pages/admin.py b/blitz/ui/pages/admin.py deleted file mode 100644 index 690f2dd..0000000 --- a/blitz/ui/pages/admin.py +++ /dev/null @@ -1,33 +0,0 @@ -from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui -from nicegui import ui - - -class AdminPage: - def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: - self.blitz_ui = blitz_ui - - def resize_iframe(self) -> None: - ui.run_javascript( - """ - var iframe = document.querySelector('iframe'); - var resizeIframe = function() { - iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px'; - }; - var navList = iframe.getElementById('navList'); - if (navList) { - console.log('hello') - var lastNavItem = navList.lastElementChild; - if (lastNavItem) { - lastNavItem.style.pointerEvents = 'none'; - lastNavItem.style.color = 'gray'; - } - } - }; - """ - ) - - def render_page(self) -> None: - self.resize_iframe() - ui.element("iframe").props( - f"src={self.blitz_ui.localhost_url}/admin/ frameborder=0 onload=resizeIframe()" - ).classes("w-full rounded-sm bg-white h-screen overflow-hidden") diff --git a/blitz/ui/pages/admin/__init__.py b/blitz/ui/pages/admin/__init__.py new file mode 100644 index 0000000..ff788f5 --- /dev/null +++ b/blitz/ui/pages/admin/__init__.py @@ -0,0 +1,26 @@ +from pathlib import Path +from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui +from nicegui import ui + +from blitz.ui.components.element.base import IFrame + + +class Page: + def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: + self.blitz_ui = blitz_ui + + def resize_iframe(self) -> None: + with open(Path(__file__).parent / "./resize_iframe.js") as f: + ui.run_javascript(f.read()) + + def render_page(self) -> None: + self.resize_iframe() + self.ng = IFrame( + src=f"{self.blitz_ui.localhost_url}/admin/", + frameborder=0, + classes="w-full rounded-sm bg-white h-screen overflow-hidden", + props="onload=resizeIframe()", + ) + + +AdminPage = Page diff --git a/blitz/ui/pages/admin/resize_iframe.js b/blitz/ui/pages/admin/resize_iframe.js new file mode 100644 index 0000000..f894514 --- /dev/null +++ b/blitz/ui/pages/admin/resize_iframe.js @@ -0,0 +1,14 @@ +const iframe = document.querySelector("iframe"); + +const resizeIframe = function () { + iframe.style.height = iframe.contentWindow.document.body.scrollHeight + "px"; +}; + +const navList = iframe.getElementById("navList"); +if (navList) { + var lastNavItem = navList.lastElementChild; + if (lastNavItem) { + lastNavItem.style.pointerEvents = "none"; + lastNavItem.style.color = "gray"; + } +} diff --git a/blitz/ui/pages/base.py b/blitz/ui/pages/base.py index 5b334f0..469064c 100644 --- a/blitz/ui/pages/base.py +++ b/blitz/ui/pages/base.py @@ -1,20 +1,26 @@ from typing import Any, Self -from blitz.ui.components.base import BaseComponent + from nicegui import ui from starlette.requests import Request +from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui from blitz.ui.components.header import FrameComponent +from blitz.ui.components.labels import Label -class BasePage(BaseComponent): +class BasePage: PAGE_NAME = "Blitz Dashboard" FRAME: FrameComponent - def __init__(self) -> None: - super().__init__() + def __init__(self, blitz_ui: BlitzUI = get_blitz_ui()) -> None: + self.blitz_ui = blitz_ui + self.current_project = self.blitz_ui.current_project + self.current_app = self.blitz_ui.current_app + self.setup() + super().__init__() + # self.frame() self.render() - self.frame() def __new__(cls, *args: Any, **kwargs: Any) -> Self: instance = super().__new__(cls, *args, **kwargs) @@ -22,20 +28,19 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: instance.FRAME = FrameComponent() return instance - def frame(self) -> None: - """The frame method HAVE to render a frame.""" - if self.FRAME is not None: - self.FRAME.render() + # def frame(self) -> None: + # """The frame method HAVE to render a frame.""" + # if self.FRAME is not None: + # self.FRAME def setup(self) -> None: """The setup method is called before the render method.""" pass def render(self) -> None: - ui.label("Base Page") + Label("Base Page") @classmethod def entrypoint(cls, request: Request) -> None: - print(request.url.path) ui.page_title(cls.PAGE_NAME) cls() diff --git a/blitz/ui/pages/dashboard.py b/blitz/ui/pages/dashboard.py index ceb09e1..be93583 100644 --- a/blitz/ui/pages/dashboard.py +++ b/blitz/ui/pages/dashboard.py @@ -1,40 +1,46 @@ from nicegui import ui +from blitz.ui.components.accordion.base import BaseAccordion from blitz.ui.components.base import BaseComponent from blitz.ui.components.logger import LogComponent from blitz.ui.components.status import StatusComponent from blitz.ui.pages.base import BasePage +from blitz.ui.components.labels.base import TextXlLabel, TextSmLabel -class ProjectDetailComponent(BaseComponent): +class ProjectDetailComponent(BaseComponent[ui.row]): + TitleLabel = TextXlLabel.variant(classes="font-bold") + TextLabel = TextSmLabel.variant(classes="font-normal") + BoldTextLabel = TextSmLabel.variant(classes="font-bold") + def render(self) -> None: with ui.row().classes("w-full justify-between items-center"): if self.current_app is None: # TODO handle error raise Exception - ui.label(f"{self.current_app.file.config.name}").classes("text-xl font-bold") - ui.label(f"Version: {self.current_app.file.config.version}").classes("font-bold text-sm") + self.TitleLabel(f"{self.current_app.file.config.name}") + self.BoldTextLabel(f"Version: {self.current_app.file.config.version}") ui.separator() - ui.label(f"Project Path: {self.current_app.path}").classes("text-sm") - ui.label(f"Description: {self.current_app.file.config.description}").classes("text-sm font-normal") + self.BoldTextLabel(f"Project Path: {self.current_app.path}") + self.TextLabel(f"Description: {self.current_app.file.config.description}") class DashboardPage(BasePage): PAGE_NAME = "Dashboard" + Accordion = BaseAccordion.variant(classes="w-full text-bold text-2xl") def setup(self) -> None: self.columns, self.rows = self.blitz_ui.get_ressources() def render(self) -> None: - with ui.element("div").classes("w-full h-full flex flex-row justify-center"): + with ui.element("div").classes("w-full h-full flex flex-row justify-center") as self.ng: with ui.column().classes("w-2/3 h-full border rounded-lg border-gray-300"): - with ui.expansion("Project", value=True, icon="info").classes("w-full text-bold text-2xl "): - ProjectDetailComponent().render() - with ui.expansion("Resources", value=True, icon="help_outline").classes("w-full text-bold text-2xl"): + with self.Accordion("Project", icon="info", is_open=True): + ProjectDetailComponent() + with self.Accordion("Resources", icon="help_outline", is_open=True): ui.table(columns=self.columns, rows=self.rows, row_key="name").classes("w-full no-shadow") - with ui.expansion("Status", value=True, icon="health_and_safety").classes("w-full text-bold text-2xl"): - # See https://github.com/zauberzeug/nicegui/issues/2174 - StatusComponent().render() # type: ignore - with ui.expansion("Logs", value=False, icon="list").classes("w-full text-bold text-2xl"): - LogComponent().render() + with self.Accordion("Status", icon="health_and_safety", is_open=True): + StatusComponent() + with self.Accordion("Logs", icon="list", is_open=False): + LogComponent() diff --git a/blitz/ui/pages/diagram.py b/blitz/ui/pages/diagram/__init__.py similarity index 62% rename from blitz/ui/pages/diagram.py rename to blitz/ui/pages/diagram/__init__.py index e094455..a80cdee 100644 --- a/blitz/ui/pages/diagram.py +++ b/blitz/ui/pages/diagram/__init__.py @@ -1,24 +1,24 @@ +from pathlib import Path from nicegui import ui from blitz.ui.pages.base import BasePage +from blitz.ui.components.buttons.icon import IconButton -class MermaidPage(BasePage): +class Page(BasePage): PAGE_NAME = "ERD" + ZoomButton = IconButton.variant(classes="border rounded-sm") def setup(self) -> None: self._width = 100 def remove_style(self) -> None: - ui.run_javascript( - """ - var svg = document.querySelector('svg'); - svg.removeAttribute("style"); - """ - ) + with open(Path(__file__).parent / "./remove_style.js") as f: + ui.run_javascript(f.read()) def zoom_svg(self) -> None: self._width += 50 self.remove_style() + # Can't put it in a file without a better integration like a bridge to js function or something like this ui.run_javascript( f""" var svg = document.querySelector('svg'); @@ -31,6 +31,7 @@ def unzoom_svg(self) -> None: if self._width < 100: self._width = 100 self.remove_style() + # Can't put it in a file without a better integration like a bridge to js function or something like this ui.run_javascript( f""" var svg = document.querySelector('svg'); @@ -45,5 +46,8 @@ def render(self) -> None: raise Exception ui.mermaid(self.blitz_ui.erd) with ui.footer().classes("w-full justify-start "): - ui.button(icon="zoom_in", on_click=self.zoom_svg).classes("borderrounded-sm").props("flat") - ui.button(icon="zoom_out", on_click=self.unzoom_svg).classes("border rounded-sm").props("flat") + self.ZoomButton(icon="zoom_in", on_click=self.zoom_svg) + self.ZoomButton(icon="zoom_out", on_click=self.unzoom_svg) + + +MermaidPage = Page diff --git a/blitz/ui/pages/diagram/remove_style.js b/blitz/ui/pages/diagram/remove_style.js new file mode 100644 index 0000000..c045d03 --- /dev/null +++ b/blitz/ui/pages/diagram/remove_style.js @@ -0,0 +1,2 @@ +var svg = document.querySelector("svg"); +svg.removeAttribute("style"); diff --git a/blitz/ui/pages/gpt_builder.py b/blitz/ui/pages/gpt_builder.py index 6b47265..ff7a9be 100644 --- a/blitz/ui/pages/gpt_builder.py +++ b/blitz/ui/pages/gpt_builder.py @@ -8,13 +8,20 @@ from blitz.settings import get_settings from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui +from blitz.ui.components import notify +from blitz.ui.components.buttons.flat import FlatButton +from blitz.ui.components.buttons.icon import IconButton +from blitz.ui.components.buttons.outline import OutlineButton from blitz.ui.components.gpt_chat_components import ( GPTChatComponent, GPTResponse, UserQuestion, ) from blitz.ui.components.header import FrameComponent +from blitz.ui.components.labels.base import BoldLabel, Text2XlBoldLabel, TextXlBoldLabel, TextXsLabel +from blitz.ui.components.rows.base import WFullItemsCenterRow from blitz.ui.pages.base import BasePage +from blitz.ui.components.labels import Label DEV_TEXT = """Sure! Here is a sample blitz_file with randomly generated models and fields: @@ -61,6 +68,8 @@ class GPTClient: + CloseButton = IconButton.variant(icon="close") + def __init__(self, api_key: str, model: str = "gpt-3.5-turbo", pre_prompt: str | None = None) -> None: self.model = model self._api_key = api_key @@ -181,13 +190,20 @@ def render(self) -> None: def footer(self) -> None: with ui.footer().classes("items-center space-y-0 pt-0 justify-center px-5"): with ui.grid(columns=10).classes("w-full items-center gap-5"): - with ui.button(on_click=self.delete_conversation_dialog.open).props("flat size=sm").classes( - "justify-self-start" - ): - ui.icon("delete_outline", color="grey-8", size="md").props("fab-mini") - with ui.button(on_click=self.open_settings).props("flat").classes("justify-self-end"): - ui.icon("settings", color="grey-6", size="md").props("fab-mini") - + IconButton( + icon="delete_outline", + icon_color="grey-8", + icon_size="lg", + on_click=self.delete_conversation_dialog.open, + classes="justify-self-start", + ) + IconButton( + icon="settings", + icon_color="grey-6", + icon_size="lg", + on_click=self.open_settings, + classes="justify-self-end", + ) with ui.row(wrap=False).classes( "w-full items-center rounded-lg pl-2 border-solid border col-start-3 col-span-6" ): @@ -198,16 +214,17 @@ def footer(self) -> None: self.ask_button() # type: ignore ui.space().classes("col-span-2") - ui.label("ChatGPT can make mistakes. Consider checking important information.").classes( - "text-xs text-gray-500 w-full text-center" + TextXsLabel( + "ChatGPT can make mistakes. Consider checking important information.", + classes="text-gray-500 w-full text-center", ) def delete_conversation(self) -> None: with ui.dialog() as self.delete_conversation_dialog, ui.card().classes("no-shadow"): - ui.label("Are you sure you want to delete this conversation?") + Label("Are you sure you want to delete this conversation?") with ui.row().classes("w-full items-center"): - ui.button("Cancel", on_click=self.delete_conversation_dialog.close).props("flat") - ui.button("Delete", on_click=self._handle_delete_conversation).props("flat") + FlatButton("Cancel", on_click=self.delete_conversation_dialog.close) + FlatButton("Delete", on_click=self._handle_delete_conversation) def _handle_delete_conversation(self) -> None: self.remove_conversation() @@ -240,15 +257,12 @@ def chat_area(self) -> None: @ui.refreshable def ask_button(self) -> None: - ask_button = ( - ui.button(on_click=self.ask_button_trigger).props("flat").bind_enabled_from(self, "can_send_request") - ) - - with ask_button: - if not self.thinking: - ui.icon("send", color="#a72bff").props("fab-mini") - else: - ui.icon("stop_circle", color="#a72bff").props("fab-mini") + IconButton( + icon="send" if not self.thinking else "stop_circle", + icon_color="purple-4", + on_click=self.ask_button_trigger, + props="fab-mini", + ).ng.bind_enabled_from(self, "can_send_request") async def handle_key(self, e: KeyEventArguments) -> None: if e.modifiers.meta and e.key.enter and self.can_send_request: @@ -320,10 +334,10 @@ def render(self) -> None: with self.dialog, ui.card().classes("w-full px-4"): self.quit_modal() self.header() - ui.label("OpenAI").classes("text-xl font-bold") + TextXlBoldLabel("OpenAI") # See https://github.com/zauberzeug/nicegui/issues/2174 self.openai_settings() # type: ignore - ui.label("Pre Prompt").classes("text-xl font-bold") + TextXlBoldLabel("Pre Prompt") # See https://github.com/zauberzeug/nicegui/issues/2174 self.pre_prompt_editor() # type: ignore @@ -343,17 +357,17 @@ def api_key_input_component(self) -> None: def header(self) -> None: """Render the header of the settings dialog""" - with ui.row().classes("w-full items-center justify-center"): - ui.button(icon="close", on_click=self.close).props("flat") - ui.label("Chat Settings").classes("text-2xl font-bold grow text-center") - ui.button("Save", icon="save", on_click=self.save).classes("text-color-black").props( - "flat" - ).bind_enabled_from(self, "settings_has_changed") + with WFullItemsCenterRow(classes="justify-center"): + IconButton(icon="close", icon_color="white", on_click=self.close) + Text2XlBoldLabel("Chat Settings", classes="grow text-center") + FlatButton("Save", icon="save", on_click=self.save, classes="text-color-black").ng.bind_enabled_from( + self, "settings_has_changed" + ) @ui.refreshable def openai_settings(self) -> None: """Render the openai settings""" - with ui.row().classes("w-full items-center"): + with WFullItemsCenterRow(): self.model_select = ( ui.select( {"gpt-3.5-turbo": "3.5 Turbo", "gpt-4": "4"}, @@ -364,13 +378,13 @@ def openai_settings(self) -> None: .classes("w-32 rounded-lg px-2 border-solid border") ) self.api_key_input_component() - ui.button("Check API KEY", on_click=self.validate_api_key).props("outline") + OutlineButton("Check API KEY", on_click=self.validate_api_key) ui.space() @ui.refreshable def pre_prompt_editor(self) -> None: - with ui.row().classes("w-full items-center"): - ui.button("Reset Pre-Prompt", on_click=self.reset_preprompt).props("outline") + with WFullItemsCenterRow(): + OutlineButton("Reset Pre-Prompt", on_click=self.reset_preprompt) switch = ui.switch("Edit Pre-Prompt", value=False) self.preprompt = ( ui.textarea(label="Pre-Prompt", value=self.blitz_ui.preprompt) @@ -402,12 +416,12 @@ def reset_preprompt(self) -> None: def quit_modal(self) -> None: with self.quit_dialog, ui.card(): - with ui.row().classes("w-full items-center"): - ui.button(icon="close", on_click=self.quit_dialog.close).props("flat") - ui.label("Some changes wasn't saved.").classes("font-bold") - with ui.row().classes("w-full items-center"): - ui.button("Discard changes", on_click=self.quit).props("flat") - ui.button("Save", on_click=self.save).props("flat") + with WFullItemsCenterRow(): + IconButton(icon="close", on_click=self.quit_dialog.close) + BoldLabel("Some changes wasn't saved.") + with WFullItemsCenterRow(): + FlatButton("Discard changes", on_click=self.quit) + FlatButton("Save", on_click=self.save) def close(self) -> None: if self.settings_has_changed: @@ -427,13 +441,13 @@ def save(self) -> None: self.gpt_client.model = self.model_select.value self.blitz_ui.preprompt = self.preprompt.value self.gpt_client.refresh_client(api_key=self.api_key_input.value) - ui.notify("Settings saved", type="positive") + notify.success("Settings saved") self.dialog.close() async def validate_api_key(self) -> None: try: gpt_client = GPTClient(api_key=self.api_key_input.value) await gpt_client.list_models() - ui.notify("Valid API Key", type="positive") + notify.success("Valid API Key") except (AuthenticationError, APIConnectionError): - ui.notify("Invalid API Key", type="warning") + notify.warning("Invalid API Key") diff --git a/blitz/ui/pages/log.py b/blitz/ui/pages/log.py index f62777b..556b02a 100644 --- a/blitz/ui/pages/log.py +++ b/blitz/ui/pages/log.py @@ -6,4 +6,4 @@ class LogPage(BasePage): PAGE_NAME = "Log" def render(self) -> None: - LogComponent().render() + LogComponent() diff --git a/blitz/ui/pages/projects.py b/blitz/ui/pages/projects.py index 97f7c25..00686dc 100644 --- a/blitz/ui/pages/projects.py +++ b/blitz/ui/pages/projects.py @@ -1,6 +1,11 @@ from nicegui import ui from blitz.ui.blitz_ui import BlitzUI, get_blitz_ui +from blitz.ui.components.buttons.flat import FlatButton +from blitz.ui.components.grids.base import WFullGrid +from blitz.ui.components.labels import Label +from blitz.ui.components.links.base import WFullLink +from blitz.ui.components.tooltip import Tooltip class ProjectDetail: @@ -19,14 +24,14 @@ def __init__( self.version = version def render(self) -> None: - with ui.link(target=f"/projects/{self.app_name}").classes("w-full hover:bg-slate-700 rounded-sm"), ui.grid( - columns=20 - ).classes("w-full my-2"): - ui.label(self.app_name).classes("col-span-2 pl-2") - ui.label(self.project_name).classes("col-span-2 pl-2") - ui.label(self.date).classes("col-span-4") - ui.label(self.description).classes("col-span-11") - ui.label(self.version).classes("col-span-1") + with WFullLink(target=f"/projects/{self.app_name}", classes="hover:bg-slate-700 rounded-sm"), WFullGrid( + columns=20, classes="my-2" + ): + Label(self.app_name).ng.classes("col-span-2 pl-2") + Label(self.project_name).ng.classes("col-span-2 pl-2") + Label(self.date).ng.classes("col-span-4") + Label(self.description).ng.classes("col-span-11") + Label(self.version).ng.classes("col-span-1") class HomePage: @@ -37,18 +42,18 @@ def render_page(self) -> None: with ui.element("div").classes("w-full justify-center items-center content-center p-10"): with ui.card().classes("no-shadow border align-center"): with ui.row().classes("w-full justify-between items-center"): - ui.label("Blitz Projects").classes("text-2xl") - with ui.button("New").props("disabled").props("flat"): - ui.tooltip("This feature is not developed yet. Create a new project with the CLI.") + Label("Blitz Projects").ng.classes("text-2xl") + with FlatButton("New").props("disabled"): + Tooltip("This feature is not developed yet. Create a new project with the CLI.") ui.input(label="Search for project").props("borderless standout dense").classes( " rounded-lg px-2 border-solid border w-full my-5" ) with ui.grid(columns=20).classes("w-full"): - ui.label("App").classes("col-span-2 pl-2") - ui.label("Name").classes("col-span-2 pl-2") - ui.label("Last modified").classes("col-span-4") - ui.label("Description").classes("col-span-11") - ui.label("Version").classes("col-span-1") + Label("App").ng.classes("col-span-2 pl-2") + Label("Name").ng.classes("col-span-2 pl-2") + Label("Last modified").ng.classes("col-span-4") + Label("Description").ng.classes("col-span-11") + Label("Version").ng.classes("col-span-1") ui.separator() diff --git a/blitz/ui/pages/swagger.py b/blitz/ui/pages/swagger.py deleted file mode 100644 index 2a9d867..0000000 --- a/blitz/ui/pages/swagger.py +++ /dev/null @@ -1,25 +0,0 @@ -from nicegui import ui - -from blitz.ui.pages.base import BasePage - - -class SwaggerPage(BasePage): - PAGE_NAME = "Swagger" - - def resize_iframe(self) -> None: - ui.run_javascript( - """ - var iframe = document.querySelector('iframe'); - var resizeIframe = function() { - iframe.style.height = iframe.contentWindow.document.body.scrollHeight + 'px'; - }; - - """ - ) - - def render(self) -> None: - self.resize_iframe() - - ui.element("iframe").props( - f"src={self.blitz_ui.localhost_url}/api/docs frameborder=0 onload=resizeIframe()" - ).classes("w-full rounded-sm bg-white h-screen overflow-hidden") diff --git a/blitz/ui/pages/swagger/__init__.py b/blitz/ui/pages/swagger/__init__.py new file mode 100644 index 0000000..b477a6f --- /dev/null +++ b/blitz/ui/pages/swagger/__init__.py @@ -0,0 +1,24 @@ +from nicegui import ui +from blitz.ui.components.element.base import IFrame +from pathlib import Path +from blitz.ui.pages.base import BasePage + + +class Page(BasePage): + PAGE_NAME = "Swagger" + + def resize_iframe(self) -> None: + with open(Path(__file__).parent / "./resize_iframe.js") as f: + ui.run_javascript(f.read()) + + def render(self) -> None: + self.resize_iframe() + self.ng = IFrame( + src=f"{self.blitz_ui.localhost_url}/api/docs", + frameborder=0, + classes="w-full rounded-sm bg-white h-screen overflow-hidden", + props="onload=resizeIframe()", + ) + + +SwaggerPage = Page diff --git a/blitz/ui/pages/swagger/resize_iframe.js b/blitz/ui/pages/swagger/resize_iframe.js new file mode 100644 index 0000000..570792c --- /dev/null +++ b/blitz/ui/pages/swagger/resize_iframe.js @@ -0,0 +1,5 @@ +const iframe = document.querySelector("iframe"); + +const resizeIframe = function () { + iframe.style.height = iframe.contentWindow.document.body.scrollHeight + "px"; +}; diff --git a/docs/blitzfile/index.md b/docs/blitzfile/index.md index cd49278..ad3b21e 100644 --- a/docs/blitzfile/index.md +++ b/docs/blitzfile/index.md @@ -49,28 +49,26 @@ The resources section is built as below: ```yaml resources: - - name: TodoList - fields: - - name: Todo - fields: + TodoList: + ... + Todo: + ... ``` === "Json" ```json - "resources": [ - { - "name": "TodoList", - "fields": {} + "resources": { + "TodoList": { + ... }, - { - "name": "Todo", - "fields": {} + "Todo": { + ... } - ] + } ``` -Each model is constructed with a `name` and a `fields` section. The `name` is the name of the model and the `fields` section contains the fields of the model. +A `name` which is the name of the resource and a `fields` section which contains the fields of the resource. > _Still pretty easy right ?_ @@ -88,12 +86,10 @@ Here is an example of a working Blitz file: ```yaml resources: - - name: TodoList - fields: + TodoList: owner!: str description: str - - name: Todo - fields: + Todo: due_date: str todo_list_id: TodoList.id todo_list: TodoList @@ -102,38 +98,30 @@ Here is an example of a working Blitz file: === "Json" ```json - "resources": [ - { - "name": "TodoList", - "fields": { - "owner!": "str", - "description": "str" - } + "resources": { + "TodoList": { + "owner!": "str", + "description": "str" }, - { - "name": "Todo", - "fields": { - "due_date": "str", - "todo_list_id": "TodoList.id", - "todo_list": "TodoList" - } + "Todo": { + "due_date": "str", + "todo_list_id": "TodoList.id", + "todo_list": "TodoList" } - ] + } ``` === "Yaml (explicit)" ```yaml resources: - - name: TodoList - fields: + TodoList: owner: type: str unique: true description: type: str - - name: Todo - fields: + Todo: due_date: type: str todo_list_id: @@ -147,40 +135,32 @@ Here is an example of a working Blitz file: === "Json (explicit)" ```json - "resources": [ - { - "name": "TodoList", - "fields": { - "owner": { - "type": "str", - "unique": true - }, - "description": { - "type": "str" - } + "resources": { + "TodoList": { + "owner": { + "type": "str", + "unique": true + }, + "description": { + "type": "str" } }, - { - "name": "Todo", - "fields": { - "due_date": { - "type": "str" - }, - "todo_list_id": { - "type": "foreign_key", - "foreign_key": "TodoList.id" - }, - "todo_list": { - "type": "relationship", - "relationship": "TodoList" - } + "Todo": { + "due_date": { + "type": "str" + }, + "todo_list_id": { + "type": "foreign_key", + "foreign_key": "TodoList.id" + }, + "todo_list": { + "type": "relationship", + "relationship": "TodoList" } } - ] + } ``` -> _You can try it in the [Blitz Playground](#)_ ! - !!! note We will maintain the 4 ways of writing fields in the Blitz file because we think that the explicit way is more readable and the shortcut way is more convenient. We are also about to implement in the Blitz dashboard a way to switch between the 4 ways really easily. diff --git a/docs/blitzfile/relationship.md b/docs/blitzfile/relationship.md deleted file mode 100644 index 856f052..0000000 --- a/docs/blitzfile/relationship.md +++ /dev/null @@ -1,252 +0,0 @@ -## Relationship -Currently supported relationships are: -- One-to-many -- One-to-one - -### One-to-Many -> A **Player** has many **Item**s. - -In the following example, a **player has many items** -=== "Yaml" - ```yaml - - name: Player - fields: - name: str - items: Item[] - - name: Item - fields: - name: str - player_id: Player.id - player: Player - ``` - -=== "Json" - ```json - [ - { - "name": "Player", - "fields": { - "name": "str", - "items": "Item[]" - } - }, - { - "name": "Item", - "fields": { - "name": "str", - "player_id": "Player.id", - "player": "Player" - } - } - ] - ``` - -=== "Yaml (explicit)" - ```yaml - - name: Player - fields: - name: - type: str - items: - type: relationship - relationship: Item - relationship_list: true - - name: Item - fields: - name: - type: str - player_id: - type: foreign_key - foreign_key: Player.id - player: - type: relationship - relationship: Player - ``` - -=== "Json (explicit)" - ```json - [ - { - "name": "Player", - "fields": { - "name": { - "type": "str" - }, - "items": { - "type": "relationship", - "relationship": "Item", - "relationship_list": true - } - } - }, - { - "name": "Item", - "fields": { - "name": { - "type": "str" - }, - "player_id": { - "type": "foreign_key", - "foreign_key": "Player.id" - - }, - "player": { - "type": "relationship", - "relationship": "Player" - } - } - } - ] - ``` - - -By specifying the player relationship from the **Item** entity, we made a `Item->Player` relationship where an **Item** is related to a single **Player**. - -Because the **Player** entity don't have any relationship declared, there is no rules concerning the relationship between `Player->Item`. - -!!! note - As you can see, you can declare a `items` relationship in the `Player` resource to make the relationship usable from the `Player` entity. - - This is fully optional and it don't do anything about the real relationship between `Player` and `Item` because evrything is set in the `Item` resource, but it allow the `Player` resource to display the linked `Item`s resources. - - -Then, one **Item** belongs to one **Player** entity and one **Player** can have multiple **Item** entities. This is a **One to Many** relationship. - - -### One to One - -In the following example, a **player has one bank account** and a **bank has many accounts**. -=== "Yaml" - ```yaml - - name: Player - fields: - name: str - account: BankAccount - - name: Bank - fields: - name: str - - name: BankAccount - fields: - bank_id: Bank.id - bank: Bank - player_id: Player.id - player: Player - ``` - -=== "Json" - ```json - [ - { - "name": "Player", - "fields": { - "name": "str", - "account": "BankAccount" - } - }, - { - "name": "Bank", - "fields": { - "name": "str" - } - }, - { - "name": "BankAccount", - "fields": { - "bank_id": "Bank.id", - "bank": "Bank", - "player_id": "Player.id", - "player": "Player" - } - } - ] - ``` - -=== "Yaml (explicit)" - ```yaml - - name: Player - fields: - name: - type: str - account: - type: relationship - relationship: BankAccount - - name: Bank - fields: - name: str - - name: BankAccount - fields: - bank_id: - type: foreign_key - foreign_key: Bank.id - bank: - type: relationship - relationship: Bank - player_id: - type: foreign_key - foreign_key: Player.id - unique: true - player: - type: relationship - relationship: Player - ``` - -=== "Json (explicit)" - ```json - [ - { - "name": "Player", - "fields": { - "name": { - "type": "str" - }, - "account": { - "type": "relationship", - "relationship": "BankAccount" - } - } - }, - { - "name": "Bank", - "fields": { - "name": "str" - } - }, - { - "name": "BankAccount", - "fields": { - "bank_id": { - "type": "foreign_key", - "foreign_key": "Bank.id" - }, - "bank": { - "type": "relationship", - "relationship": "Bank" - }, - "player_id": { - "type": "foreign_key", - "foreign_key": "Player.id", - "unique": true - }, - "player": { - "type": "relationship", - "relationship": "Player" - } - } - } - ] - ``` - -By specifying the player relationship from the **BankAccount** entity, we made a `BankAccount->Player` relationship where a BankAccount is related to a single **Player**. - -Because we also specify the **player_id** to be unique, The relationship to a player id can only exists once. Then a **Player** can have only one **BankAccount**. - - -!!! note - As you can see, you can declare a `account` relationship in the `Player` resource to make the relationship usable from the `Player` entity. - - This is fully optional and it don't do anything about the real relationship between `Player` and `BankAccount` because evrything is set in the `BankAccount` resource, but it allow the `Player` resource to display the linked `BankAccount` resource. - -Then, one **Player** has one **BankAccount** entity and one **BankAccount** is related to a single **Player**. This is a **One to One** relationship. - -## Known issues -- Currently you **MUST** specify a foreign key and a relationship attribute to make it work correctly. \ No newline at end of file diff --git a/docs/blitzfile/relationships.md b/docs/blitzfile/relationships.md new file mode 100644 index 0000000..ef60a86 --- /dev/null +++ b/docs/blitzfile/relationships.md @@ -0,0 +1,222 @@ +## Relationship + +Currently supported relationships are: + +- One-to-many +- One-to-one + +### One-to-Many + +> A **Player** has many **Item**s. + +In the following example, a **player has many items** +=== "Yaml" + + ```yaml + Player: + name: str + items: Item[] + Item: + name: str + player_id: Player.id + player: Player + ``` + +=== "Json" + + ```json + { + "Player": { + "name": "str", + "items": "Item[]" + }, + "Item": { + "name": "str", + "player_id": "Player.id", + "player": "Player" + } + } + ``` + +=== "Yaml (explicit)" + + ```yaml + Player: + name: + type: str + items: + type: relationship + relationship: Item + relationship_list: true + Item: + name: + type: str + player_id: + type: foreign_key + foreign_key: Player.id + player: + type: relationship + relationship: Player + ``` + +=== "Json (explicit)" + + ```json + { + "Player": { + "name": { + "type": "str" + }, + "items": { + "type": "relationship", + "relationship": "Item", + "relationship_list": true + } + }, + "Item": { + "name": { + "type": "str" + }, + "player_id": { + "type": "foreign_key", + "foreign_key": "Player.id" + }, + "player": { + "type": "relationship", + "relationship": "Player" + } + } + } + ``` + +By specifying the player relationship from the **Item** entity, we made a `Item->Player` relationship where an **Item** is related to a single **Player**. + +Because the **Player** entity don't have any relationship declared, there is no rules concerning the relationship between `Player->Item`. + +!!! note + + As you can see, you can declare a `items` relationship in the `Player` resource to make the relationship usable from the `Player` entity. + + This is fully optional and it don't do anything about the real relationship between `Player` and `Item` because evrything is set in the `Item` resource, but it allow the `Player` resource to display the linked `Item`s resources. + +Then, one **Item** belongs to one **Player** entity and one **Player** can have multiple **Item** entities. This is a **One to Many** relationship. + +### One to One + +In the following example, a **player has one bank account** and a **bank has many accounts**. + +=== "Yaml" + + ```yaml + Player: + name: str + account: BankAccount + Bank: + name: str + BankAccount: + bank_id: Bank.id + bank: Bank + player_id!: Player.id + player: Player + ``` + +=== "Json" + + ```json + { + "Player": { + "name": "str", + "account": "BankAccount" + }, + "Bank": { + "name": "str" + }, + "BankAccount": { + "bank_id": "Bank.id", + "bank": "Bank", + "player_id!": "Player.id", + "player": "Player" + } + } + ``` + +=== "Yaml (explicit)" + + ```yaml + Player: + name: + type: str + account: + type: relationship + relationship: BankAccount + Bank: + name: str + BankAccount: + bank_id: + type: foreign_key + foreign_key: Bank.id + bank: + type: relationship + relationship: Bank + player_id: + type: foreign_key + foreign_key: Player.id + unique: true + player: + type: relationship + relationship: Player + ``` + +=== "Json (explicit)" + + ```json + { + "Player": { + "name": { + "type": "str" + }, + "account": { + "type": "relationship", + "relationship": "BankAccount" + } + }, + "Bank": { + "name": "str" + }, + "BankAccount": { + "bank_id": { + "type": "foreign_key", + "foreign_key": "Bank.id" + }, + "bank": { + "type": "relationship", + "relationship": "Bank" + }, + "player_id": { + "type": "foreign_key", + "foreign_key": "Player.id", + "unique": true + }, + "player": { + "type": "relationship", + "relationship": "Player" + } + } + } + ``` + +By specifying the player relationship from the **BankAccount** entity, we made a `BankAccount->Player` relationship where a BankAccount is related to a single **Player**. + +Because we also specify the **player_id** to be unique, The relationship to a player id can only exists once. Then a **Player** can have only one **BankAccount**. + +!!! note + + As you can see, you can declare a `account` relationship in the `Player` resource to make the relationship usable from the `Player` entity. + + This is fully optional and it don't do anything about the real relationship between `Player` and `BankAccount` because evrything is set in the `BankAccount` resource, but it allow the `Player` resource to display the linked `BankAccount` resource. + +Then, one **Player** has one **BankAccount** entity and one **BankAccount** is related to a single **Player**. This is a **One to One** relationship. + +## Known issues + +- Currently you **MUST** specify a foreign key and a relationship attribute to make it work correctly. diff --git a/docs/blitzfile/resources.md b/docs/blitzfile/resources.md index 5e54706..cfa34ba 100644 --- a/docs/blitzfile/resources.md +++ b/docs/blitzfile/resources.md @@ -8,19 +8,18 @@ The `resources` section contains your Blitz resources description. It is built a ```yaml resources: - - name: Book - fields: + Book: + ... ``` === "Json" ```json - "resources": [ - { - "name": "Book", - "fields": {} + "resources": { + "Book": { + ... } - ] + } ``` Each resource contains at least a `name` and a `fields` section. The `name` is the name of the resource and the `fields` section contains the fields of the resource. @@ -28,44 +27,48 @@ Each resource contains at least a `name` and a `fields` section. The `name` is t ## Fields !!! note - The field section can be constructed in 2 way, the **explicit** way and the **shortcut** way. You can use both way in the same Blitz file because as the name says, the shortcut way is just a shortcut to the explicit way. +The field section can be constructed in 2 way, the **explicit** way and the **shortcut** way. You can use both way in the same Blitz file because as the name says, the shortcut way is just a shortcut to the explicit way. Each field must contain at least a `name` and a `type`. The available field types are listed below: -| Type | Description | Example | -| ---- | ----------- | ------- | -| `str` | A string | `Hello world` | -| `int` | An integer | `42` | -| `float` | A float | `3.14` | -| `bool` | A boolean | `true` | -| `uuid` | A UUID | `123e4567-e89b-12d3-a456-42661417` | -| `datetime` | A datetime | `2021-01-01T00:00:00` | +| Type | Description | Example | +| ---------- | ----------- | ---------------------------------- | +| `str` | A string | `Hello world` | +| `int` | An integer | `42` | +| `float` | A float | `3.14` | +| `bool` | A boolean | `true` | +| `uuid` | A UUID | `123e4567-e89b-12d3-a456-42661417` | +| `datetime` | A datetime | `2021-01-01T00:00:00` | === "Yaml" + ```yaml - fields: + Resource: description: str ``` === "Json" + ```json - "fields": { + "Resource": { "description": "str" } ``` === "Yaml (explicit)" + ```yaml - fields: + Resource: description: type: str ``` === "Json (explicit)" + ```json - "fields": { + "Resource": { "description": { "type": "str" } @@ -73,15 +76,15 @@ The available field types are listed below: ``` !!! note - In this example, the name of the field is `description` and the type is `str`. +In this example, the name of the field is `description` and the type is `str`. Let's have a look with a complete resource and then break it down: === "Yaml" + ```yaml resources: - - name: Book - fields: + Book: title: str! # (1)! identifier!: uuid! # (2)! author: str? # (3)! @@ -97,21 +100,19 @@ Let's have a look with a complete resource and then break it down: 4. The field `description` has a **default value** because of the `default` property (`default: "Here is a description"`). See the [default value](#default-value) for more details. === "Json" + ```json - "resources": [ - { - "name": "Book", - "fields": { - "title": "str!", // (1)! - "identifier!": "uuid!", // (2)! - "author": "str?", // (3)! - "description": { // (4)! - "type": "str", - "default": "Here is a description" - } + "resources": { + "Book": { + "title": "str!", // (1)! + "identifier!": "uuid!", // (2)! + "author": "str?", // (3)! + "description": { // (4)! + "type": "str", + "default": "Here is a description" } } - ] + } ``` 1. The field `title` is **required** because of the `!` modifier at the end of the field type (`str!`). See the [required field](#required-field) for more details. @@ -121,10 +122,10 @@ Let's have a look with a complete resource and then break it down: 4. The field `description` has a **default value** because of the `default` property (`"default": "Here is a description"`). See the [default value](#default-value) for more details. === "Yaml (explicit)" + ```yaml resources: - - name: Book - fields: + Book: title: # (1)! type: str required: true @@ -145,32 +146,29 @@ Let's have a look with a complete resource and then break it down: 3. The field `author` is **nullable** because of the `nullable` property (`nullable: true`). See the [nullable field](#nullable-field) for more details. 4. The field `description` has a **default value** because of the `default` property (`default: "Here is a description"`). See the [default value](#default-value) for more details. - === "Json (explicit)" + ```json - "resources": [ - { - "name": "Book", - "fields": { - "title": { // (1)! - "type": "str", - "required": true - }, - "identifier": { // (2)! - "type": "uuid", - "unique": true - }, - "author": { // (3)! - "type": "str", - "nullable": true - }, - "description": { // (4)! - "type": "str", - "default": "Here is a description" - } + "resources": { + "Book": { + "title": { // (1)! + "type": "str", + "required": true + }, + "identifier": { // (2)! + "type": "uuid", + "unique": true + }, + "author": { // (3)! + "type": "str", + "nullable": true + }, + "description": { // (4)! + "type": "str", + "default": "Here is a description" } } - ] + } ``` 1. The field `title` is **required** because of the `required` propertry (`"required": true`). See the [required field](#required-field) for more details. @@ -179,38 +177,38 @@ Let's have a look with a complete resource and then break it down: 3. The field `author` is **nullable** because of the `nullable` property (`"nullable": true`). See the [nullable field](#nullable-field) for more details. 4. The field `description` has a **default value** because of the `default` property (`"default": "Here is a description"`). See the [default value](#default-value) for more details. - - - - ### Unique field You can specify if a field is **unique** by adding a `!` at the end of the field name or by setting the `unique` property to `true`. === "Yaml" + ```yaml - fields: + Resource: identifier!: uuid ``` === "Json" + ```json - "fields": { + "Resource": { "identifier!": "uuid" } ``` === "Yaml (explicit)" + ```yaml - fields: + Resource: identifier: type: uuid unique: true ``` === "Json (explicit)" + ```json - "fields": { + "Resource": { "identifier": { "type": "uuid", "unique": true @@ -223,26 +221,33 @@ You can specify if a field is **unique** by adding a `!` at the end of the field You can specify if a field is **nullable** by adding a `?` at the end of the field type or by setting the `nullable` property to `true`. === "Yaml" + ```yaml - fields: + Resource: author: str? ``` + === "Json" + ```json - "fields": { + "Resource": { "author": "str?" } ``` + === "Yaml (explicit)" + ```yaml - fields: + Resource: author: type: str nullable: true ``` + === "Json (explicit)" + ```json - "fields": { + "Resource": { "author": { "type": "str", "nullable": true @@ -257,26 +262,33 @@ The default value will be set to `null` if the field is nullable. If you want to You can specify if a field is **required** by adding a `!` at the end of the field type or by setting the `required` property to `true`. === "Yaml" + ```yaml - fields: + Resource: title: str! ``` + === "Json" + ```json "fields": { "title": "str!" } ``` + === "Yaml (explicit)" + ```yaml - fields: + Resource: title: type: str required: true ``` + === "Json (explicit)" + ```json - "fields": { + "Resource": { "title": { "type": "str", "required": true @@ -287,24 +299,27 @@ You can specify if a field is **required** by adding a `!` at the end of the fie ### Default value ??? example "No shortcut yet" + There is no shortcut yet for the `default`property. You can specify a **default value** for a field by setting the `default` property to the value you want. === "Yaml" + ```yaml - fields: + Resource: description: type: str default: "Here is a description" ``` === "Json" + ```json - "fields": { + "Resource": { "description": { "type": "str", "default": "Here is a description" } } - ``` \ No newline at end of file + ``` diff --git a/docs/cli/create.md b/docs/cli/create.md index 9038a7a..e0c3a73 100644 --- a/docs/cli/create.md +++ b/docs/cli/create.md @@ -2,7 +2,6 @@ The `blitz create` command is used to create a new blitz app. It will ask you fo The default format is `yaml`. You can also use `json` format. -
@@ -21,5 +20,36 @@ To start your app, you can use:
+!!! tip + + You can also use `--demo` to create an already configured blitz app with some resources and relationships. + + + +
+ +```console +$ blitz create --demo +Demo Blitz App created successfully ! +To start your app, you can use: + blitz start demo-blitz-app + +$ blitz start demo-blitz-app +This is still an alpha. Please do not use in production. +Please report any issues on https://github.com/Paperz-org/blitz + +Blitz app deployed. + - Blitz UI : http://localhost:8100 + - Blitz admin : http://localhost:8100/admin + - Swagger UI : http://localhost:8100/api/docs + +INFO demo-blitz-app Started server process [21292026] +INFO demo-blitz-app Waiting for application startup. +INFO demo-blitz-app Application startup complete. +``` + +
+ !!! note + Use `blitz create --help` to see all available options. diff --git a/docs/index.md b/docs/index.md index a0c7512..ead5d1c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,15 @@ # ![image info](./images/blitz_banner.png) +

⚡️ Lightspeed API builder ⚡️

-___ - - +--- # **What is Blitz ?** + Blitz is a tool that build restfull API on the fly based on a simple and easy to maintain configuration file. Here is an example of how simple a Blitz file is: @@ -22,12 +22,10 @@ Here is an example of how simple a Blitz file is: description: Here is a simple blitz configuration file. version: 0.1.0 resources: - - name: TodoList - fields: + TodoList: name: str description: str - - name: Todo - fields: + Todo: name: str due_date: str todo_list_id: TodoList.id @@ -43,37 +41,34 @@ Here is an example of how simple a Blitz file is: "description": "Here is a simple blitz configuration file.", "version": "0.1.0" }, - "resources": [ - { - "name": "TodoList", - "fields": { - "name": "str", - "description": "str" - } + "resources": { + "TodoList": { + "name": "str", + "description": "str" }, - { - "name": "Todo", - "fields": { - "name": "str", - "due_date": "str", - "todo_list_id": "TodoList.id", - "todo_list": "TodoList" - } + "Todo": { + "name": "str", + "due_date": "str", + "todo_list_id": "TodoList.id", + "todo_list": "TodoList" } - ] + } } ``` Just run: + ``` blitz start your-blitz-project ``` -*And yeah, that's it.* + +_And yeah, that's it._ !!! note - Assuming a your-blitz-project directory created using the blitz create command -___ + Assuming a `your-blitz-project` directory created using the blitz create command + +--- You have now a fully functional API with two resources and the corresponding database schema with all the modern feature you can expect from a modern app like: @@ -83,4 +78,4 @@ You have now a fully functional API with two resources and the corresponding dat - Generated ERD diagram - Dashboard - Admin -- and more... \ No newline at end of file +- and more... diff --git a/docs/quickstart.md b/docs/quickstart.md index 449b1af..1b73ab1 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1 +1,6 @@ -First of all, have a look on the [Installation](installation.md) page to install Blitz. Once Blitz is installed, you can start to create your first Blitz project using the Blitz CLI. \ No newline at end of file +First of all, have a look on the [Installation](installation.md) page to install Blitz. Once Blitz is installed, you can start to create your first Blitz project using the Blitz CLI. + +```bash +blitz create --demo +blitz start demo-blitz-app +``` diff --git a/poetry.lock b/poetry.lock index e2ee9ea..e51f764 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1877,32 +1877,6 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] -[[package]] -name = "ruff" -version = "0.2.1" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, - {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, - {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, - {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, - {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, -] - [[package]] name = "semver" version = "3.0.2" @@ -2580,4 +2554,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "fa650a0268fc60e33bd5eaf7c104194d494bf68446d1646f3f7690a2bc68ff76" +content-hash = "12078799bbf74ae221085f3f8618dd2ab43d98faebfead1dc576f3d1cdcfe1d8" diff --git a/pyproject.toml b/pyproject.toml index 7055406..ebcef4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,6 @@ mkdocs-typer = "^0.0.3" [tool.poetry.group.dev.dependencies] -ruff = "^0.2.1" pytest = "^8.0.0" mypy = "^1.8.0" diff --git a/random-blitz-app/.blitz b/random-blitz-app/.blitz new file mode 100644 index 0000000..fe14167 --- /dev/null +++ b/random-blitz-app/.blitz @@ -0,0 +1 @@ +random-blitz-app/blitz.yaml \ No newline at end of file diff --git a/random-blitz-app/blitz.yaml b/random-blitz-app/blitz.yaml new file mode 100644 index 0000000..7026fee --- /dev/null +++ b/random-blitz-app/blitz.yaml @@ -0,0 +1,5 @@ +config: + description: '' + name: Random Blitz App + version: 0.1.0 +