Support reflex app creation from templates from github (#2490)

This commit is contained in:
Martin Xu 2024-04-04 15:31:34 -07:00 committed by GitHub
parent 44d6c997dd
commit 7d36610cae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 341 additions and 736 deletions

View File

@ -10,7 +10,7 @@ omit =
[report]
show_missing = true
# TODO bump back to 79
fail_under = 72
fail_under = 70
precision = 2
# Regexes for lines to exclude from consideration

View File

@ -34,4 +34,3 @@ redis-server &
echo "Running reflex init in test project dir"
do_export blank
do_export sidebar

View File

@ -1,69 +0,0 @@
# Welcome to Reflex!
This is the base Reflex template - installed when you run `reflex init`.
If you want to use a different template, pass the `--template` flag to `reflex init`.
For example, if you want a more basic starting point, you can run:
```bash
reflex init --template blank
```
## About this Template
This template has the following directory structure:
```bash
├── README.md
├── assets
├── rxconfig.py
└── {your_app}
├── __init__.py
├── components
│   ├── __init__.py
│   └── sidebar.py
├── pages
│   ├── __init__.py
│   ├── dashboard.py
│   ├── index.py
│   └── settings.py
├── styles.py
├── templates
│   ├── __init__.py
│   └── template.py
└── {your_app}.py
```
See the [Project Structure docs](https://reflex.dev/docs/getting-started/project-structure/) for more information on general Reflex project structure.
### Adding Pages
In this template, the pages in your app are defined in `{your_app}/pages/`.
Each page is a function that returns a Reflex component.
For example, to edit this page you can modify `{your_app}/pages/index.py`.
See the [pages docs](https://reflex.dev/docs/pages/routes/) for more information on pages.
In this template, instead of using `rx.add_page` or the `@rx.page` decorator,
we use the `@template` decorator from `{your_app}/templates/template.py`.
To add a new page:
1. Add a new file in `{your_app}/pages/`. We recommend using one file per page, but you can also group pages in a single file.
2. Add a new function with the `@template` decorator, which takes the same arguments as `@rx.page`.
3. Import the page in your `{your_app}/pages/__init__.py` file and it will automatically be added to the app.
### Adding Components
In order to keep your code organized, we recommend putting components that are
used across multiple pages in the `{your_app}/components/` directory.
In this template, we have a sidebar component in `{your_app}/components/sidebar.py`.
### Adding State
As your app grows, we recommend using [substates](https://reflex.dev/docs/substates/overview/)
to organize your state.
You can either define substates in their own files, or if the state is
specific to a page, you can define it in the page file itself.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Github" clip-path="url(#clip0_469_1929)">
<path id="Vector" d="M8.0004 0.587524C3.80139 0.587524 0.400391 3.98851 0.400391 8.1875C0.400391 11.5505 2.57589 14.391 5.59689 15.398C5.97689 15.4645 6.11939 15.2365 6.11939 15.037C6.11939 14.8565 6.10989 14.258 6.10989 13.6215C4.20039 13.973 3.70639 13.156 3.55439 12.7285C3.46889 12.51 3.09839 11.8355 2.77539 11.655C2.50939 11.5125 2.12939 11.161 2.76589 11.1515C3.36439 11.142 3.79189 11.7025 3.93439 11.9305C4.61839 13.08 5.71089 12.757 6.14789 12.5575C6.21439 12.0635 6.41388 11.731 6.6324 11.541C4.94139 11.351 3.17439 10.6955 3.17439 7.7885C3.17439 6.962 3.46889 6.27801 3.95339 5.74601C3.87739 5.55601 3.61139 4.77701 4.02939 3.73201C4.02939 3.73201 4.66589 3.53251 6.11939 4.51101C6.7274 4.34001 7.3734 4.25451 8.0194 4.25451C8.6654 4.25451 9.3114 4.34001 9.9194 4.51101C11.3729 3.52301 12.0094 3.73201 12.0094 3.73201C12.4274 4.77701 12.1614 5.55601 12.0854 5.74601C12.5699 6.27801 12.8644 6.9525 12.8644 7.7885C12.8644 10.705 11.0879 11.351 9.3969 11.541C9.6724 11.7785 9.9099 12.2345 9.9099 12.947C9.9099 13.9635 9.9004 14.7805 9.9004 15.037C9.9004 15.2365 10.0429 15.474 10.4229 15.398C13.5165 14.3536 15.5996 11.4527 15.6004 8.1875C15.6004 3.98851 12.1994 0.587524 8.0004 0.587524Z" fill="#494369"/>
</g>
<defs>
<clipPath id="clip0_469_1929">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,68 +0,0 @@
<svg width="80" height="78" viewBox="0 0 80 78" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_ddddi_449_2821)">
<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" fill="url(#paint0_radial_449_2821)"/>
<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" fill="url(#paint1_radial_449_2821)"/>
<g filter="url(#filter1_i_449_2821)">
<path d="M31 37.5C30.4477 37.5 30 37.0523 30 36.5V13.5001C30 12.9478 30.4477 12.5001 31 12.5001H49C49.5523 12.5001 50 12.9478 50 13.5001V21.5001C50 22.0524 49.5523 22.5001 49 22.5001H45V18.5001C45 17.9478 44.5523 17.5001 44 17.5001H36C35.4477 17.5001 35 17.9478 35 18.5001V21.5001C35 22.0524 35.4477 22.5001 36 22.5001H45V27.5001H36C35.4477 27.5001 35 27.9478 35 28.5001V36.5C35 37.0523 34.5523 37.5 34 37.5H31ZM46 37.5C45.4477 37.5 45 37.0523 45 36.5V27.5001H49C49.5523 27.5001 50 27.9478 50 28.5001V36.5C50 37.0523 49.5523 37.5 49 37.5H46Z" fill="url(#paint2_radial_449_2821)"/>
</g>
<path d="M13 11C13 6.58172 16.5817 3 21 3H59C63.4183 3 67 6.58172 67 11V49C67 52.3137 64.3137 55 61 55H19C15.6863 55 13 52.3137 13 49V11Z" stroke="#20117E" stroke-opacity="0.04"/>
</g>
<defs>
<filter id="filter0_ddddi_449_2821" x="0.5" y="0.5" width="79" height="77" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_449_2821"/>
<feOffset dy="10"/>
<feGaussianBlur stdDeviation="8"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0784314 0 0 0 0 0.0705882 0 0 0 0 0.231373 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_449_2821"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="6" operator="erode" in="SourceAlpha" result="effect2_dropShadow_449_2821"/>
<feOffset dy="12"/>
<feGaussianBlur stdDeviation="3"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0784314 0 0 0 0 0.0705882 0 0 0 0 0.231373 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_449_2821" result="effect2_dropShadow_449_2821"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect3_dropShadow_449_2821"/>
<feOffset dy="10"/>
<feGaussianBlur stdDeviation="3"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.16 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_449_2821" result="effect3_dropShadow_449_2821"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="1" operator="dilate" in="SourceAlpha" result="effect4_dropShadow_449_2821"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="effect3_dropShadow_449_2821" result="effect4_dropShadow_449_2821"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_449_2821" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-8"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.607843 0 0 0 0 0.972549 0 0 0 0.2 0"/>
<feBlend mode="normal" in2="shape" result="effect5_innerShadow_449_2821"/>
</filter>
<filter id="filter1_i_449_2821" x="30" y="12.5001" width="20" height="26.9999" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.12549 0 0 0 0 0.0666667 0 0 0 0 0.494118 0 0 0 0.32 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_449_2821"/>
</filter>
<radialGradient id="paint0_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 3) rotate(90) scale(52 54)">
<stop stop-color="white" stop-opacity="0.9"/>
<stop offset="1" stop-color="#4E3DB9" stop-opacity="0.24"/>
</radialGradient>
<radialGradient id="paint1_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 3) rotate(90) scale(52 54)">
<stop stop-color="white"/>
<stop offset="1" stop-color="#F7F7F7"/>
</radialGradient>
<radialGradient id="paint2_radial_449_2821" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 12.5001) rotate(90) scale(24.9999 20)">
<stop stop-color="#F5F3FF"/>
<stop stop-color="white"/>
<stop offset="1" stop-color="#E1DDF4"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,13 +0,0 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="PaneLeft" clip-path="url(#clip0_469_1942)">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80217 0.525009C7.34654 0.525009 6.97717 0.894373 6.97717 1.35001V10.65C6.97717 11.1056 7.34654 11.475 7.80217 11.475H10.6522C11.1078 11.475 11.4772 11.1056 11.4772 10.65V1.35001C11.4772 0.894373 11.1078 0.525009 10.6522 0.525009H7.80217ZM8.02717 10.425V1.57501H10.4272V10.425H8.02717Z" fill="#494369"/>
<path d="M3.78215 8.14502L2.16213 6.525H5.92717V5.475H2.16213L3.78215 3.85498L3.03969 3.11252L0.523438 5.62877V6.37123L3.03969 8.88748L3.78215 8.14502Z" fill="#494369"/>
</g>
</g>
<defs>
<clipPath id="clip0_469_1942">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 807 B

View File

@ -1,37 +0,0 @@
<svg width="67" height="14" viewBox="0 0 67 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="67" height="14" fill="#1E1E1E"/>
<g id="Nav Template &#62; Initial" clip-path="url(#clip0_0_1)">
<rect width="1440" height="1024" transform="translate(-16 -17)" fill="white"/>
<g id="Sidebar">
<g clip-path="url(#clip1_0_1)">
<path d="M-16 -17H264V1007H-16V-17Z" fill="white"/>
<g id="Header">
<path d="M-16 -17H264V31H-16V-17Z" fill="white"/>
<g id="Button">
<rect x="-4" y="-3" width="74.316" height="20" rx="6" fill="white"/>
<g id="Logo">
<g id="Reflex">
<path d="M0 13.6316V0.368408H10.6106V5.67369H7.95792V3.02105H2.65264V5.67369H7.95792V8.32633H2.65264V13.6316H0ZM7.95792 13.6316V8.32633H10.6106V13.6316H7.95792Z" fill="#110F1F"/>
<path d="M13.2632 13.6316V0.368408H21.2211V3.02105H15.9158V5.67369H21.2211V8.32633H15.9158V10.979H21.2211V13.6316H13.2632Z" fill="#110F1F"/>
<path d="M23.8738 13.6316V0.368408H31.8317V3.02105H26.5264V5.67369H31.8317V8.32633H26.5264V13.6316H23.8738Z" fill="#110F1F"/>
<path d="M34.4843 13.6316V0.368408H37.137V10.979H42.4422V13.6316H34.4843Z" fill="#110F1F"/>
<path d="M45.0949 13.6316V0.368408H53.0528V3.02105H47.7475V5.67369H53.0528V8.32633H47.7475V10.979H53.0528V13.6316H45.0949Z" fill="#110F1F"/>
<path d="M55.7054 5.67369V0.368408H58.3581V5.67369H55.7054ZM63.6634 5.67369V0.368408H66.316V5.67369H63.6634ZM58.3581 8.32633V5.67369H63.6634V8.32633H58.3581ZM55.7054 13.6316V8.32633H58.3581V13.6316H55.7054ZM63.6634 13.6316V8.32633H66.316V13.6316H63.6634Z" fill="#110F1F"/>
</g>
</g>
</g>
<path d="M264 30.5H-16V31.5H264V30.5Z" fill="#F4F3F6"/>
</g>
</g>
<path d="M263.5 -17V1007H264.5V-17H263.5Z" fill="#F4F3F6"/>
</g>
</g>
<defs>
<clipPath id="clip0_0_1">
<rect width="1440" height="1024" fill="white" transform="translate(-16 -17)"/>
</clipPath>
<clipPath id="clip1_0_1">
<path d="M-16 -17H264V1007H-16V-17Z" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,8 +0,0 @@
<svg width="56" height="12" viewBox="0 0 56 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z" fill="white"/>
<path d="M11.2 11.5999V0.399902H17.92V2.6399H13.44V4.8799H17.92V7.1199H13.44V9.3599H17.92V11.5999H11.2Z" fill="white"/>
<path d="M20.16 11.5999V0.399902H26.88V2.6399H22.4V4.8799H26.88V7.1199H22.4V11.5999H20.16Z" fill="white"/>
<path d="M29.12 11.5999V0.399902H31.36V9.3599H35.84V11.5999H29.12Z" fill="white"/>
<path d="M38.08 11.5999V0.399902H44.8V2.6399H40.32V4.8799H44.8V7.1199H40.32V9.3599H44.8V11.5999H38.08Z" fill="white"/>
<path d="M47.04 4.8799V0.399902H49.28V4.8799H47.04ZM53.76 4.8799V0.399902H56V4.8799H53.76ZM49.28 7.1199V4.8799H53.76V7.1199H49.28ZM47.04 11.5999V7.1199H49.28V11.5999H47.04ZM53.76 11.5999V7.1199H56V11.5999H53.76Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 908 B

View File

@ -1 +0,0 @@
"""Base template for Reflex."""

View File

@ -1,141 +0,0 @@
"""Sidebar component for the app."""
from code import styles
import reflex as rx
def sidebar_header() -> rx.Component:
"""Sidebar header.
Returns:
The sidebar header component.
"""
return rx.hstack(
# The logo.
rx.color_mode_cond(
rx.image(src="/reflex_black.svg", height="2em"),
rx.image(src="/reflex_white.svg", height="2em"),
),
rx.spacer(),
rx.link(
rx.button(
rx.icon("github"),
color_scheme="gray",
variant="soft",
),
href="https://github.com/reflex-dev/reflex",
),
align="center",
width="100%",
border_bottom=styles.border,
padding_x="1em",
padding_y="2em",
)
def sidebar_footer() -> rx.Component:
"""Sidebar footer.
Returns:
The sidebar footer component.
"""
return rx.hstack(
rx.spacer(),
rx.link(
rx.text("Docs"),
href="https://reflex.dev/docs/getting-started/introduction/",
color_scheme="gray",
),
rx.link(
rx.text("Blog"),
href="https://reflex.dev/blog/",
color_scheme="gray",
),
width="100%",
border_top=styles.border,
padding="1em",
)
def sidebar_item(text: str, url: str) -> rx.Component:
"""Sidebar item.
Args:
text: The text of the item.
url: The URL of the item.
Returns:
rx.Component: The sidebar item component.
"""
# Whether the item is active.
active = (rx.State.router.page.path == f"/{text.lower()}") | (
(rx.State.router.page.path == "/") & text == "Home"
)
return rx.link(
rx.hstack(
rx.text(
text,
),
bg=rx.cond(
active,
rx.color("accent", 2),
"transparent",
),
border=rx.cond(
active,
f"1px solid {rx.color('accent', 6)}",
f"1px solid {rx.color('gray', 6)}",
),
color=rx.cond(
active,
styles.accent_text_color,
styles.text_color,
),
align="center",
border_radius=styles.border_radius,
width="100%",
padding="1em",
),
href=url,
width="100%",
)
def sidebar() -> rx.Component:
"""The sidebar.
Returns:
The sidebar component.
"""
# Get all the decorated pages and add them to the sidebar.
from reflex.page import get_decorated_pages
return rx.box(
rx.vstack(
sidebar_header(),
rx.vstack(
*[
sidebar_item(
text=page.get("title", page["route"].strip("/").capitalize()),
url=page["route"],
)
for page in get_decorated_pages()
],
width="100%",
overflow_y="auto",
align_items="flex-start",
padding="1em",
),
rx.spacer(),
sidebar_footer(),
height="100dvh",
),
display=["none", "none", "block"],
min_width=styles.sidebar_width,
height="100%",
position="sticky",
top="0px",
border_right=styles.border,
)

View File

@ -1,3 +0,0 @@
from .dashboard import dashboard
from .index import index
from .settings import settings

View File

@ -1,22 +0,0 @@
"""The dashboard page."""
from code.templates import template
import reflex as rx
@template(route="/dashboard", title="Dashboard")
def dashboard() -> rx.Component:
"""The dashboard page.
Returns:
The UI for the dashboard page.
"""
return rx.vstack(
rx.heading("Dashboard", size="8"),
rx.text("Welcome to Reflex!"),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/dashboard.py"),
),
)

View File

@ -1,18 +0,0 @@
"""The home page of the app."""
from code import styles
from code.templates import template
import reflex as rx
@template(route="/", title="Home")
def index() -> rx.Component:
"""The home page.
Returns:
The UI for the home page.
"""
with open("README.md", encoding="utf-8") as readme:
content = readme.read()
return rx.markdown(content, component_map=styles.markdown_style)

View File

@ -1,76 +0,0 @@
"""The settings page."""
from code.templates import ThemeState, template
import reflex as rx
@template(route="/settings", title="Settings")
def settings() -> rx.Component:
"""The settings page.
Returns:
The UI for the settings page.
"""
return rx.vstack(
rx.heading("Settings", size="8"),
rx.hstack(
rx.text("Dark mode: "),
rx.color_mode.switch(),
),
rx.hstack(
rx.text("Primary color: "),
rx.select(
[
"tomato",
"red",
"ruby",
"crimson",
"pink",
"plum",
"purple",
"violet",
"iris",
"indigo",
"blue",
"cyan",
"teal",
"jade",
"green",
"grass",
"brown",
"orange",
"sky",
"mint",
"lime",
"yellow",
"amber",
"gold",
"bronze",
"gray",
],
value=ThemeState.accent_color,
on_change=ThemeState.set_accent_color,
),
),
rx.hstack(
rx.text("Secondary color: "),
rx.select(
[
"gray",
"mauve",
"slate",
"sage",
"olive",
"sand",
],
value=ThemeState.gray_color,
on_change=ThemeState.set_gray_color,
),
),
rx.text(
"You can edit this page in ",
rx.code("{your_app}/pages/settings.py"),
size="1",
),
)

View File

@ -1,14 +0,0 @@
"""Welcome to Reflex!."""
# Import all the pages.
from code.pages import *
import reflex as rx
class State(rx.State):
"""Define empty state to allow access to rx.State.router."""
# Create the app.
app = rx.App()

View File

@ -1,45 +0,0 @@
"""Styles for the app."""
import reflex as rx
border_radius = "0.375rem"
border = f"1px solid {rx.color('gray', 6)}"
text_color = rx.color("gray", 11)
accent_text_color = rx.color("accent", 10)
accent_color = rx.color("accent", 1)
hover_accent_color = {"_hover": {"color": accent_text_color}}
hover_accent_bg = {"_hover": {"background_color": accent_color}}
content_width_vw = "90vw"
sidebar_width = "20em"
template_page_style = {"padding_top": "5em", "padding_x": ["auto", "2em"], "flex": "1"}
template_content_style = {
"align_items": "flex-start",
"border_radius": border_radius,
"padding": "1em",
"margin_bottom": "2em",
}
link_style = {
"color": accent_text_color,
"text_decoration": "none",
**hover_accent_color,
}
overlapping_button_style = {
"background_color": "white",
"border_radius": border_radius,
}
markdown_style = {
"code": lambda text: rx.code(text, color_scheme="gray"),
"codeblock": lambda text, **props: rx.code_block(text, **props, margin_y="1em"),
"a": lambda text, **props: rx.link(
text,
**props,
font_weight="bold",
text_decoration="underline",
text_decoration_color=accent_text_color,
),
}

View File

@ -1 +0,0 @@
from .template import ThemeState, template

View File

@ -1,144 +0,0 @@
"""Common templates used between pages in the app."""
from __future__ import annotations
from code import styles
from code.components.sidebar import sidebar
from typing import Callable
import reflex as rx
# Meta tags for the app.
default_meta = [
{
"name": "viewport",
"content": "width=device-width, shrink-to-fit=no, initial-scale=1",
},
]
def menu_item_link(text, href):
return rx.menu.item(
rx.link(
text,
href=href,
width="100%",
color="inherit",
),
_hover={
"color": styles.accent_color,
"background_color": styles.accent_text_color,
},
)
def menu_button() -> rx.Component:
"""The menu button on the top right of the page.
Returns:
The menu button component.
"""
from reflex.page import get_decorated_pages
return rx.box(
rx.menu.root(
rx.menu.trigger(
rx.button(
rx.icon("menu"),
variant="soft",
)
),
rx.menu.content(
*[
menu_item_link(page["title"], page["route"])
for page in get_decorated_pages()
],
rx.menu.separator(),
menu_item_link("About", "https://github.com/reflex-dev"),
menu_item_link("Contact", "mailto:founders@=reflex.dev"),
),
),
position="fixed",
right="2em",
top="2em",
z_index="500",
)
class ThemeState(rx.State):
"""The state for the theme of the app."""
accent_color: str = "crimson"
gray_color: str = "gray"
def template(
route: str | None = None,
title: str | None = None,
description: str | None = None,
meta: str | None = None,
script_tags: list[rx.Component] | None = None,
on_load: rx.event.EventHandler | list[rx.event.EventHandler] | None = None,
) -> Callable[[Callable[[], rx.Component]], rx.Component]:
"""The template for each page of the app.
Args:
route: The route to reach the page.
title: The title of the page.
description: The description of the page.
meta: Additionnal meta to add to the page.
on_load: The event handler(s) called when the page load.
script_tags: Scripts to attach to the page.
Returns:
The template with the page content.
"""
def decorator(page_content: Callable[[], rx.Component]) -> rx.Component:
"""The template for each page of the app.
Args:
page_content: The content of the page.
Returns:
The template with the page content.
"""
# Get the meta tags for the page.
all_meta = [*default_meta, *(meta or [])]
def templated_page():
return rx.hstack(
sidebar(),
rx.box(
rx.box(
page_content(),
**styles.template_content_style,
),
**styles.template_page_style,
),
menu_button(),
align="start",
background=f"radial-gradient(circle at top right, {rx.color('accent', 2)}, {rx.color('mauve', 1)});",
position="relative",
)
@rx.page(
route=route,
title=title,
description=description,
meta=all_meta,
script_tags=script_tags,
on_load=on_load,
)
def theme_wrap():
return rx.theme(
templated_page(),
has_background=True,
accent_color=ThemeState.accent_color,
gray_color=ThemeState.gray_color,
)
return theme_wrap
return decorator

View File

@ -646,7 +646,12 @@ export const useEventLoop = (
// Route after the initial page hydration.
useEffect(() => {
const change_start = () => dispatch["state"]({is_hydrated: false})
const change_start = () => {
const main_state_dispatch = dispatch["state"]
if (main_state_dispatch !== undefined) {
main_state_dispatch({is_hydrated: false})
}
}
const change_complete = () => addEvents(onLoadInternalEvent());
router.events.on("routeChangeStart", change_start);
router.events.on("routeChangeComplete", change_complete);

View File

@ -88,12 +88,11 @@ class ReflexHostingCLI(SimpleNamespace):
class Templates(SimpleNamespace):
"""Constants related to Templates."""
# Dynamically get the enum values from the .templates folder
template_dir = os.path.join(Reflex.ROOT_DIR, Reflex.MODULE_NAME, ".templates/apps")
template_dirs = next(os.walk(template_dir))[1]
# The route on Reflex backend to query which templates are available and their URLs.
APP_TEMPLATES_ROUTE = "/app-templates"
# Create an enum value for each directory in the .templates folder
Kind = Enum("Kind", {template.upper(): template for template in template_dirs})
# The default template
DEFAULT = "blank"
class Dirs(SimpleNamespace):
"""Folders used by the template system of Reflex."""

94
reflex/constants/base.pyi Normal file
View File

@ -0,0 +1,94 @@
"""Stub file for reflex/constants/base.py"""
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload
from reflex.vars import Var, BaseVar, ComputedVar
from reflex.event import EventChain, EventHandler, EventSpec
from reflex.style import Style
import os
import platform
from enum import Enum
from importlib import metadata
from types import SimpleNamespace
from platformdirs import PlatformDirs
IS_WINDOWS = platform.system() == "Windows"
class Dirs(SimpleNamespace):
WEB = ".web"
APP_ASSETS = "assets"
UTILS = "utils"
STATIC = "_static"
STATE_PATH = "/".join([UTILS, "state"])
COMPONENTS_PATH = "/".join([UTILS, "components"])
CONTEXTS_PATH = "/".join([UTILS, "context"])
WEB_PAGES = os.path.join(WEB, "pages")
WEB_STATIC = os.path.join(WEB, STATIC)
WEB_UTILS = os.path.join(WEB, UTILS)
WEB_ASSETS = os.path.join(WEB, "public")
ENV_JSON = os.path.join(WEB, "env.json")
REFLEX_JSON = os.path.join(WEB, "reflex.json")
POSTCSS_JS = os.path.join(WEB, "postcss.config.js")
class Reflex(SimpleNamespace):
MODULE_NAME = "reflex"
VERSION = metadata.version(MODULE_NAME)
JSON = os.path.join(Dirs.WEB, "reflex.json")
_dir = os.environ.get("REFLEX_DIR", "")
DIR = _dir or PlatformDirs(MODULE_NAME, False).user_data_dir
ROOT_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
class ReflexHostingCLI(SimpleNamespace):
MODULE_NAME = "reflex-hosting-cli"
class Templates(SimpleNamespace):
APP_TEMPLATES_ROUTE = "/app-templates"
DEFAULT = "blank"
class Dirs(SimpleNamespace):
BASE = os.path.join(Reflex.ROOT_DIR, Reflex.MODULE_NAME, ".templates")
WEB_TEMPLATE = os.path.join(BASE, "web")
JINJA_TEMPLATE = os.path.join(BASE, "jinja")
CODE = "code"
class Next(SimpleNamespace):
CONFIG_FILE = "next.config.js"
SITEMAP_CONFIG_FILE = os.path.join(Dirs.WEB, "next-sitemap.config.js")
NODE_MODULES = "node_modules"
PACKAGE_LOCK = "package-lock.json"
FRONTEND_LISTENING_REGEX = "Local:[\\s]+(.*)"
class ColorMode(SimpleNamespace):
NAME = "colorMode"
USE = "useColorMode"
TOGGLE = "toggleColorMode"
class Env(str, Enum):
DEV = "dev"
PROD = "prod"
class LogLevel(str, Enum):
DEBUG = "debug"
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000
class Ping(SimpleNamespace):
INTERVAL = 25
TIMEOUT = 120
COOKIES = "cookies"
LOCAL_STORAGE = "local_storage"
SKIP_COMPILE_ENV_VAR = "__REFLEX_SKIP_COMPILE"
ENV_MODE_ENV_VAR = "REFLEX_ENV_MODE"
PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST"
RELOAD_CONFIG = "__REFLEX_RELOAD_CONFIG"
REFLEX_VAR_OPENING_TAG = "<reflex.Var>"
REFLEX_VAR_CLOSING_TAG = "</reflex.Var>"

View File

@ -145,7 +145,7 @@ def _populate_demo_app(name_variants: NameVariants):
with set_directory(demo_app_dir):
# We start with the blank template as basis.
_init(name=demo_app_name, template=constants.Templates.Kind.BLANK)
_init(name=demo_app_name, template=constants.Templates.DEFAULT)
# Then overwrite the app source file with the one we want for testing custom components.
# This source file is rendered using jinja template file.
with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:

View File

@ -63,7 +63,7 @@ def main(
def _init(
name: str,
template: constants.Templates.Kind | None = constants.Templates.Kind.BLANK,
template: str | None = None,
loglevel: constants.LogLevel = config.loglevel,
):
"""Initialize a new Reflex app in the given directory."""
@ -79,31 +79,20 @@ def _init(
app_name = prerequisites.validate_app_name(name)
console.rule(f"[bold]Initializing {app_name}")
# Check prerequisites.
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
prerequisites.initialize_reflex_user_directory()
prerequisites.ensure_reflex_installation_id()
# When upgrading to 0.4, show migration instructions.
if prerequisites.should_show_rx_chakra_migration_instructions():
prerequisites.show_rx_chakra_migration_instructions()
# Set up the app directory, only if the config doesn't exist.
if not os.path.exists(constants.Config.FILE):
if template is None:
template = prerequisites.prompt_for_template()
prerequisites.create_config(app_name)
prerequisites.initialize_app_directory(app_name, template)
telemetry_event = "init"
else:
telemetry_event = "reinit"
# Set up the web project.
prerequisites.initialize_frontend_dependencies()
# Send the telemetry event after the .web folder is initialized.
telemetry.send(telemetry_event)
# Initialize the app.
prerequisites.initialize_app(app_name, template)
# Migrate Pynecone projects to Reflex.
prerequisites.migrate_to_reflex()
@ -123,7 +112,7 @@ def init(
name: str = typer.Option(
None, metavar="APP_NAME", help="The name of the app to initialize."
),
template: constants.Templates.Kind = typer.Option(
template: str = typer.Option(
None,
help="The template to initialize the app with.",
),
@ -576,20 +565,6 @@ def demo(
# Open the demo app in a terminal.
webbrowser.open("https://demo.reflex.run")
# Later: open the demo app locally.
# with tempfile.TemporaryDirectory() as tmp_dir:
# os.chdir(tmp_dir)
# _init(
# name="reflex_demo",
# template=constants.Templates.Kind.DEMO,
# loglevel=constants.LogLevel.DEBUG,
# )
# _run(
# frontend_port=frontend_port,
# backend_port=backend_port,
# loglevel=constants.LogLevel.DEBUG,
# )
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")

View File

@ -223,7 +223,7 @@ class AppHarness:
with chdir(self.app_path):
reflex.reflex._init(
name=self.app_name,
template=reflex.constants.Templates.Kind.BLANK,
template=reflex.constants.Templates.DEFAULT,
loglevel=reflex.constants.LogLevel.INFO,
)
self.app_module_path.write_text(source_code)

View File

@ -2,8 +2,6 @@
from __future__ import annotations
from typing import List, Optional
from rich.console import Console
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from rich.prompt import Prompt
@ -150,7 +148,10 @@ def error(msg: str, **kwargs):
def ask(
question: str, choices: Optional[List[str]] = None, default: Optional[str] = None
question: str,
choices: list[str] | None = None,
default: str | None = None,
show_choices: bool = True,
) -> str:
"""Takes a prompt question and optionally a list of choices
and returns the user input.
@ -159,11 +160,12 @@ def ask(
question: The question to ask the user.
choices: A list of choices to select from.
default: The default option selected.
show_choices: Whether to show the choices.
Returns:
A string with the user input.
"""
return Prompt.ask(question, choices=choices, default=default) # type: ignore
return Prompt.ask(question, choices=choices, default=default, show_choices=show_choices) # type: ignore
def progress():

View File

@ -10,6 +10,7 @@ import os
import platform
import random
import re
import shutil
import stat
import sys
import tempfile
@ -30,6 +31,7 @@ from redis.asyncio import Redis
import reflex
from reflex import constants, model
from reflex.base import Base
from reflex.compiler import templates
from reflex.config import Config, get_config
from reflex.utils import console, path_ops, processes
@ -37,6 +39,15 @@ from reflex.utils import console, path_ops, processes
CURRENTLY_INSTALLING_NODE = False
class Template(Base):
"""A template for a Reflex app."""
name: str
description: str
code_url: str
demo_url: str
def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed.
@ -407,17 +418,42 @@ def initialize_requirements_txt():
console.info(f"Unable to check {fp} for reflex dependency.")
def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
def initialize_app_directory(
app_name: str,
template_name: str = constants.Templates.DEFAULT,
template_code_dir_name: str | None = None,
template_dir: Path | None = None,
):
"""Initialize the app directory on reflex init.
Args:
app_name: The name of the app.
template: The template to use.
template_name: The name of the template to use.
template_code_dir_name: The name of the code directory in the template.
template_dir: The directory of the template source files.
Raises:
Exit: If template_name, template_code_dir_name, template_dir combination is not supported.
"""
console.log("Initializing the app directory.")
# Copy the template to the current directory.
template_dir = Path(constants.Templates.Dirs.BASE, "apps", template.value)
# By default, use the blank template from local assets.
if template_name == constants.Templates.DEFAULT:
if template_code_dir_name is not None or template_dir is not None:
console.error(
f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
)
raise typer.Exit(1)
template_code_dir_name = constants.Templates.Dirs.CODE
template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
else:
if template_code_dir_name is None or template_dir is None:
console.error(
f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
)
raise typer.Exit(1)
console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
# Remove all pyc and __pycache__ dirs in template directory.
for pyc_file in template_dir.glob("**/*.pyc"):
@ -430,16 +466,16 @@ def initialize_app_directory(app_name: str, template: constants.Templates.Kind):
path_ops.cp(str(file), file.name)
# Rename the template app to the app name.
path_ops.mv(constants.Templates.Dirs.CODE, app_name)
path_ops.mv(template_code_dir_name, app_name)
path_ops.mv(
os.path.join(app_name, template_dir.name + constants.Ext.PY),
os.path.join(app_name, template_name + constants.Ext.PY),
os.path.join(app_name, app_name + constants.Ext.PY),
)
# Fix up the imports.
path_ops.find_replace(
app_name,
f"from {constants.Templates.Dirs.CODE}",
f"from {template_name}",
f"from {app_name}",
)
@ -999,33 +1035,35 @@ def check_schema_up_to_date():
)
def prompt_for_template() -> constants.Templates.Kind:
def prompt_for_template(templates: list[Template]) -> str:
"""Prompt the user to specify a template.
Args:
templates: The templates to choose from.
Returns:
The template the user selected.
The template name the user selects.
"""
# Show the user the URLs of each temlate to preview.
# Show the user the URLs of each template to preview.
console.print("\nGet started with a template:")
console.print("blank (https://blank-template.reflex.run) - A minimal template.")
console.print(
"sidebar (https://sidebar-template.reflex.run) - A template with a sidebar to navigate pages."
)
console.print("")
# Prompt the user to select a template.
id_to_name = {
str(idx): f"{template.name} ({template.demo_url}) - {template.description}"
for idx, template in enumerate(templates)
}
for id in range(len(id_to_name)):
console.print(f"({id}) {id_to_name[str(id)]}")
template = console.ask(
"Which template would you like to use?",
choices=[
template.value
for template in constants.Templates.Kind
if template.value != "demo"
],
default=constants.Templates.Kind.BLANK.value,
choices=[str(i) for i in range(len(id_to_name))],
show_choices=False,
default="0",
)
# Return the template.
return constants.Templates.Kind(template)
return templates[int(template)].name
def should_show_rx_chakra_migration_instructions() -> bool:
@ -1178,3 +1216,166 @@ def migrate_to_reflex():
for old, new in updates.items():
line = line.replace(old, new)
print(line, end="")
def fetch_app_templates() -> dict[str, Template]:
"""Fetch the list of app templates from the Reflex backend server.
Returns:
The name and download URL as a dictionary.
"""
config = get_config()
if not config.cp_backend_url:
console.info(
"Skip fetching App templates. No backend URL is specified in the config."
)
return {}
try:
response = httpx.get(
f"{config.cp_backend_url}{constants.Templates.APP_TEMPLATES_ROUTE}"
)
response.raise_for_status()
return {
template["name"]: Template.parse_obj(template)
for template in response.json()
}
except httpx.HTTPError as ex:
console.info(f"Failed to fetch app templates: {ex}")
return {}
except (TypeError, KeyError, json.JSONDecodeError) as tkje:
console.info(f"Unable to process server response for app templates: {tkje}")
return {}
def create_config_init_app_from_remote_template(
app_name: str,
template_url: str,
):
"""Create new rxconfig and initialize app using a remote template.
Args:
app_name: The name of the app.
template_url: The path to the template source code as a zip file.
Raises:
Exit: If any download, file operations fail or unexpected zip file format.
"""
# Create a temp directory for the zip download.
try:
temp_dir = tempfile.mkdtemp()
except OSError as ose:
console.error(f"Failed to create temp directory for download: {ose}")
raise typer.Exit(1) from ose
# Use httpx GET with redirects to download the zip file.
zip_file_path = Path(temp_dir) / "template.zip"
try:
# Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
response = httpx.get(template_url, follow_redirects=True)
console.debug(f"Server responded download request: {response}")
response.raise_for_status()
except httpx.HTTPError as he:
console.error(f"Failed to download the template: {he}")
raise typer.Exit(1) from he
try:
with open(zip_file_path, "wb") as f:
f.write(response.content)
console.debug(f"Downloaded the zip to {zip_file_path}")
except OSError as ose:
console.error(f"Unable to write the downloaded zip to disk {ose}")
raise typer.Exit(1) from ose
# Create a temp directory for the zip extraction.
try:
unzip_dir = Path(tempfile.mkdtemp())
except OSError as ose:
console.error(f"Failed to create temp directory for extracting zip: {ose}")
raise typer.Exit(1) from ose
try:
zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
# The zip file downloaded from github looks like:
# repo-name-branch/**/*, so we need to remove the top level directory.
if len(subdirs := os.listdir(unzip_dir)) != 1:
console.error(f"Expected one directory in the zip, found {subdirs}")
raise typer.Exit(1)
template_dir = unzip_dir / subdirs[0]
console.debug(f"Template folder is located at {template_dir}")
except Exception as uze:
console.error(f"Failed to unzip the template: {uze}")
raise typer.Exit(1) from uze
# Move the rxconfig file here first.
path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
new_config = get_config(reload=True)
# Get the template app's name from rxconfig in case it is different than
# the source code repo name on github.
template_name = new_config.app_name
create_config(app_name)
initialize_app_directory(
app_name,
template_name=template_name,
template_code_dir_name=template_name,
template_dir=template_dir,
)
# Clean up the temp directories.
shutil.rmtree(temp_dir)
shutil.rmtree(unzip_dir)
def initialize_app(app_name: str, template: str | None = None):
"""Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
Args:
app_name: The name of the app.
template: The name of the template to use.
Raises:
Exit: If template is directly provided in the command flag and is invalid.
"""
# Local imports to avoid circular imports.
from reflex.utils import telemetry
# Check if the app is already initialized.
if os.path.exists(constants.Config.FILE):
telemetry.send("reinit")
return
# Get the available templates
templates: dict[str, Template] = fetch_app_templates()
# Prompt for a template if not provided.
if template is None and len(templates) > 0:
template = prompt_for_template(list(templates.values()))
elif template is None:
template = constants.Templates.DEFAULT
assert template is not None
# If the blank template is selected, create a blank app.
if template == constants.Templates.DEFAULT:
# Default app creation behavior: a blank app.
create_config(app_name)
initialize_app_directory(app_name)
else:
# Fetch App templates from the backend server.
console.debug(f"Available templates: {templates}")
# If user selects a template, it needs to exist.
if template in templates:
template_url = templates[template].code_url
else:
# Check if the template is a github repo.
if template.startswith("https://github.com"):
template_url = f"{template.strip('/')}/archive/main.zip"
else:
console.error(f"Template `{template}` not found.")
raise typer.Exit(1)
create_config_init_app_from_remote_template(
app_name=app_name,
template_url=template_url,
)
telemetry.send("init")