This commit is contained in:
Khaleel Al-Adhami 2024-09-20 14:56:44 -07:00
commit 825463c494
387 changed files with 18696 additions and 22107 deletions

View File

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

View File

@ -81,19 +81,11 @@ jobs:
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-12] os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.10.13', '3.11.5', '3.12.0']
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: '3.10.13'
- os: windows-latest
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
# keep only one python version for MacOS # keep only one python version for MacOS
- os: macos-latest
python-version: '3.8.18'
- os: macos-latest
python-version: '3.9.18'
- os: macos-latest - os: macos-latest
python-version: '3.10.13' python-version: '3.10.13'
- os: macos-12 - os: macos-12
@ -101,11 +93,7 @@ jobs:
include: include:
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View File

@ -23,7 +23,7 @@ jobs:
strategy: strategy:
matrix: matrix:
state_manager: ['redis', 'memory'] state_manager: ['redis', 'memory']
python-version: ['3.8.18', '3.11.5', '3.12.0'] python-version: ['3.11.5', '3.12.0']
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
# Label used to access the service container # Label used to access the service container

View File

@ -43,21 +43,14 @@ jobs:
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-12] os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.10.13', '3.11.5', '3.12.0']
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: '3.10.13'
- os: windows-latest
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include: include:
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:

View File

@ -37,13 +37,15 @@ jobs:
path: reflex-examples path: reflex-examples
- uses: Vampire/setup-wsl@v3 - uses: Vampire/setup-wsl@v3
with:
distribution: Ubuntu-24.04
- name: Install Python - name: Install Python
shell: wsl-bash {0} shell: wsl-bash {0}
run: | run: |
apt update apt update
apt install -y python3 python3-pip curl dos2unix zip unzip apt install -y python3 python3-pip curl dos2unix zip unzip
- name: Install Poetry - name: Install Poetry
shell: wsl-bash {0} shell: wsl-bash {0}
run: | run: |

View File

@ -28,22 +28,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-12] os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ['3.10.13', '3.11.5', '3.12.0']
# Windows is a bit behind on Python version availability in Github # Windows is a bit behind on Python version availability in Github
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: '3.10.13'
- os: windows-latest
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include: include:
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# Service containers to run with `runner-job` # Service containers to run with `runner-job`
services: services:

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
assets/external/* assets/external/*
dist/* dist/*
examples/ examples/
.web
.idea .idea
.vscode .vscode
.coverage .coverage

View File

@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib
**Prerequisites:** **Prerequisites:**
- Python >= 3.8 - Python >= 3.10
- Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info). - Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info).
**1. Fork this repository:** **1. Fork this repository:**

View File

@ -10,7 +10,6 @@
### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨** ### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -35,7 +34,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
## ⚙️ Installation ## ⚙️ Installation
Open a terminal and run (Requires Python 3.8+): Open a terminal and run (Requires Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -130,7 +130,6 @@ def render_multiple_pages(app, num: int):
def AppWithOnePage(): def AppWithOnePage():
"""A reflex app with one page.""" """A reflex app with one page."""
import reflex_chakra as rc
from rxconfig import config # type: ignore from rxconfig import config # type: ignore
import reflex as rx import reflex as rx
@ -145,7 +144,7 @@ def AppWithOnePage():
def index() -> rx.Component: def index() -> rx.Component:
return rx.center( return rx.center(
rc.input( rx.input(
id="token", value=State.router.session.client_token, is_read_only=True id="token", value=State.router.session.client_token, is_read_only=True
), ),
rx.vstack( rx.vstack(

View File

@ -1,133 +1,30 @@
# Reflex Docker Container # Reflex Docker Examples
This example describes how to create and use a container image for Reflex with your own code. This directory contains several examples of how to deploy Reflex apps using docker.
## Update Requirements In all cases, ensure that your `requirements.txt` file is up to date and
includes the `reflex` package.
The `requirements.txt` includes the reflex package which is needed to install ## `simple-two-port`
Reflex framework. If you use additional packages in your project you have to add
this in the `requirements.txt` first. Copy the `Dockerfile`, `.dockerignore` and
the `requirements.txt` file in your project folder.
## Build Simple Reflex Container Image The most basic production deployment exposes two HTTP ports and relies on an
existing load balancer to forward the traffic appropriately.
The main `Dockerfile` is intended to build a very simple, single container deployment that runs ## `simple-one-port`
the Reflex frontend and backend together, exposing ports 3000 and 8000.
To build your container image run the following command: This deployment exports the frontend statically and serves it via a single HTTP
port using Caddy. This is useful for platforms that only support a single port
or where running a node server in the container is undesirable.
```bash ## `production-compose`
docker build -t reflex-app:latest .
```
## Start Container Service This deployment is intended for use with a standalone VPS that is only hosting a
single Reflex app. It provides the entire stack in a single `compose.yaml`
including a webserver, one or more backend instances, redis, and a postgres
database.
Finally, you can start your Reflex container service as follows: ## `production-app-platform`
```bash This example deployment is intended for use with App hosting platforms, like
docker run -it --rm -p 3000:3000 -p 8000:8000 --name app reflex-app:latest Azure, AWS, or Google Cloud Run. It is the backend of the deployment, which
``` depends on a separately hosted redis instance and static frontend deployment.
It may take a few seconds for the service to become available.
Access your app at http://localhost:3000.
Note that this container has _no persistence_ and will lose all data when
stopped. You can use bind mounts or named volumes to persist the database and
uploaded_files directories as needed.
# Production Service with Docker Compose and Caddy
An example production deployment uses automatic TLS with Caddy serving static files
for the frontend and proxying requests to both the frontend and backend.
Copy the following files to your project directory:
* `compose.yaml`
* `compose.prod.yaml`
* `compose.tools.yaml`
* `prod.Dockerfile`
* `Caddy.Dockerfile`
* `Caddyfile`
The production app container, based on `prod.Dockerfile`, builds and exports the
frontend statically (to be served by Caddy). The resulting image only runs the
backend service.
The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend
and `Caddyfile` into the container to configure the reverse proxy routes that will
forward requests to the backend service. Caddy will automatically provision TLS
for localhost or the domain specified in the environment variable `DOMAIN`.
This type of deployment should use less memory and be more performant since
nodejs is not required at runtime.
## Customize `Caddyfile` (optional)
If the app uses additional backend API routes, those should be added to the
`@backend_routes` path matcher to ensure they are forwarded to the backend.
## Build Reflex Production Service
During build, set `DOMAIN` environment variable to the domain where the app will
be hosted! (Do not include http or https, it will always use https).
**If `DOMAIN` is not provided, the service will default to `localhost`.**
```bash
DOMAIN=example.com docker compose build
```
This will build both the `app` service from the `prod.Dockerfile` and the `webserver`
service via `Caddy.Dockerfile`.
## Run Reflex Production Service
```bash
DOMAIN=example.com docker compose up
```
The app should be available at the specified domain via HTTPS. Certificate
provisioning will occur automatically and may take a few minutes.
### Data Persistence
Named docker volumes are used to persist the app database (`db-data`),
uploaded_files (`upload-data`), and caddy TLS keys and certificates
(`caddy-data`).
## More Robust Deployment
For a more robust deployment, consider bringing the service up with
`compose.prod.yaml` which includes postgres database and redis cache, allowing
the backend to run with multiple workers and service more requests.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d
```
Postgres uses its own named docker volume for data persistence.
## Admin Tools
When needed, the services in `compose.tools.yaml` can be brought up, providing
graphical database administration (Adminer on http://localhost:8080) and a
redis cache browser (redis-commander on http://localhost:8081). It is not recommended
to deploy these services if they are not in active use.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
```
# Container Hosting
Most container hosting services automatically terminate TLS and expect the app
to be listening on a single port (typically `$PORT`).
To host a Reflex app on one of these platforms, like Google Cloud Run, Render,
Railway, etc, use `app.Dockerfile` to build a single image containing a reverse
proxy that will serve that frontend as static files and proxy requests to the
backend for specific endpoints.
If the chosen platform does not support buildx and thus heredoc, you can copy
the Caddyfile configuration into a separate Caddyfile in the root of the
project.

View File

@ -0,0 +1,5 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

View File

@ -0,0 +1,65 @@
# This docker file is intended to be used with container hosting services
#
# After deploying this image, get the URL pointing to the backend service
# and run API_URL=https://path-to-my-container.example.com reflex export frontend
# then copy the contents of `frontend.zip` to your static file server (github pages, s3, etc).
#
# Azure Static Web App example:
# npx @azure/static-web-apps-cli deploy --env production --app-location .web/_static
#
# For dynamic routes to function properly, ensure that 404s are redirected to /404 on the
# static file host (for github pages, this works out of the box; remember to create .nojekyll).
#
# For azure static web apps, add `staticwebapp.config.json` to to `.web/_static` with the following:
# {
# "responseOverrides": {
# "404": {
# "rewrite": "/404.html"
# }
# }
# }
#
# Note: many container hosting platforms require amd64 images, so when building on an M1 Mac
# for example, pass `docker build --platform=linux/amd64 ...`
# Stage 1: init
FROM python:3.11 as init
ARG uv=/root/.cargo/bin/uv
# Install `uv` for faster package boostrapping
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh
# Copy local context to `/app` inside container (see .dockerignore)
WORKDIR /app
COPY . .
RUN mkdir -p /app/data /app/uploaded_files
# Create virtualenv which will be copied into final container
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN $uv venv
# Install app requirements and reflex inside virtualenv
RUN $uv pip install -r requirements.txt
# Deploy templates and prepare app
RUN reflex init
# Stage 2: copy artifacts into slim image
FROM python:3.11-slim
WORKDIR /app
RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg2 (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
# Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
exec reflex run --env prod --backend-only --backend-port ${PORT:-8000}

View File

@ -0,0 +1,113 @@
# production-app-platform
This example deployment is intended for use with App hosting platforms, like
Azure, AWS, or Google Cloud Run.
## Architecture
The production deployment consists of a few pieces:
* Backend container - built by `Dockerfile` Runs the Reflex backend
service on port 8000 and is scalable to multiple instances.
* Redis container - A single instance the standard `redis` docker image should
share private networking with the backend
* Static frontend - HTML/CSS/JS files that are hosted via a CDN or static file
server. This is not included in the docker image.
## Deployment
These general steps do not cover the specifics of each platform, but all platforms should
support the concepts described here.
### Vnet
All containers in the deployment should be hooked up to the same virtual private
network so they can access the redis service and optionally the database server.
The vnet should not be exposed to the internet, use an ingress rule to terminate
TLS at the load balancer and forward the traffic to a backend service replica.
### Redis
Deploy a `redis` instance on the vnet.
### Backend
The backend is built by the `Dockerfile` in this directory. When deploying the
backend, be sure to set REDIS_URL=redis://internal-redis-hostname to connect to
the redis service.
### Ingress
Configure the load balancer for the app to forward traffic to port 8000 on the
backend service replicas. Most platforms will generate an ingress hostname
automatically. Make sure when you access the ingress endpoint on `/ping` that it
returns "pong", indicating that the backend is up an available.
### Frontend
The frontend should be hosted on a static file server or CDN.
**Important**: when exporting the frontend, set the API_URL environment variable
to the ingress hostname of the backend service.
If you will host the frontend from a path other than the root, set the
`FRONTEND_PATH` environment variable appropriately when exporting the frontend.
Most static hosts will automatically use the `/404.html` file to handle 404
errors. _This is essential for dynamic routes to work correctly._ Ensure that
missing routes return the `/404.html` content to the user if this is not the
default behavior.
_For Github Pages_: ensure the file `.nojekyll` is present in the root of the repo
to avoid special processing of underscore-prefix directories, like `_next`.
## Platform Notes
The following sections are currently a work in progress and may be incomplete.
### Azure
In the Azure load balancer, per-message deflate is not supported. Add the following
to your `rxconfig.py` to workaround this issue.
```python
import uvicorn.workers
import reflex as rx
class NoWSPerMessageDeflate(uvicorn.workers.UvicornH11Worker):
CONFIG_KWARGS = {
**uvicorn.workers.UvicornH11Worker.CONFIG_KWARGS,
"ws_per_message_deflate": False,
}
config = rx.Config(
app_name="my_app",
gunicorn_worker_class="rxconfig.NoWSPerMessageDeflate",
)
```
#### Persistent Storage
If you need to use a database or upload files, you cannot save them to the
container volume. Use Azure Files and mount it into the container at /app/uploaded_files.
#### Resource Types
* Create a new vnet with 10.0.0.0/16
* Create a new subnet for redis, database, and containers
* Deploy redis as a Container Instances
* Deploy database server as "Azure Database for PostgreSQL"
* Create a new database for the app
* Set db-url as a secret containing the db user/password connection string
* Deploy Storage account for uploaded files
* Enable access from the vnet and container subnet
* Create a new file share
* In the environment, create a new files share (get the storage key)
* Deploy the backend as a Container App
* Create a custom Container App Environment linked up to the same vnet as the redis container.
* Set REDIS_URL and DB_URL environment variables
* Add the volume from the environment
* Add the volume mount to the container
* Deploy the frontend as a Static Web App

View File

@ -42,10 +42,11 @@ COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg2 (skip if not using postgres). # Install libpq-dev for psycopg2 (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex USER reflex
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
# Needed until Reflex properly passes SIGTERM on backend. # Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend. # Always apply migrations before starting the backend.
CMD reflex db migrate && reflex run --env prod --backend-only CMD [ -d alembic ] && reflex db migrate; \
exec reflex run --env prod --backend-only

View File

@ -0,0 +1,75 @@
# production-compose
This example production deployment uses automatic TLS with Caddy serving static
files for the frontend and proxying requests to both the frontend and backend.
It is intended for use with a standalone VPS that is only hosting a single
Reflex app.
The production app container (`Dockerfile`), builds and exports the frontend
statically (to be served by Caddy). The resulting image only runs the backend
service.
The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend
and `Caddyfile` into the container to configure the reverse proxy routes that will
forward requests to the backend service. Caddy will automatically provision TLS
for localhost or the domain specified in the environment variable `DOMAIN`.
This type of deployment should use less memory and be more performant since
nodejs is not required at runtime.
## Customize `Caddyfile` (optional)
If the app uses additional backend API routes, those should be added to the
`@backend_routes` path matcher to ensure they are forwarded to the backend.
## Build Reflex Production Service
During build, set `DOMAIN` environment variable to the domain where the app will
be hosted! (Do not include http or https, it will always use https).
**If `DOMAIN` is not provided, the service will default to `localhost`.**
```bash
DOMAIN=example.com docker compose build
```
This will build both the `app` service from the `prod.Dockerfile` and the `webserver`
service via `Caddy.Dockerfile`.
## Run Reflex Production Service
```bash
DOMAIN=example.com docker compose up
```
The app should be available at the specified domain via HTTPS. Certificate
provisioning will occur automatically and may take a few minutes.
### Data Persistence
Named docker volumes are used to persist the app database (`db-data`),
uploaded_files (`upload-data`), and caddy TLS keys and certificates
(`caddy-data`).
## More Robust Deployment
For a more robust deployment, consider bringing the service up with
`compose.prod.yaml` which includes postgres database and redis cache, allowing
the backend to run with multiple workers and service more requests.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d
```
Postgres uses its own named docker volume for data persistence.
## Admin Tools
When needed, the services in `compose.tools.yaml` can be brought up, providing
graphical database administration (Adminer on http://localhost:8080) and a
redis cache browser (redis-commander on http://localhost:8081). It is not recommended
to deploy these services if they are not in active use.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
```

View File

@ -12,7 +12,6 @@ services:
DB_URL: sqlite:///data/reflex.db DB_URL: sqlite:///data/reflex.db
build: build:
context: . context: .
dockerfile: prod.Dockerfile
volumes: volumes:
- db-data:/app/data - db-data:/app/data
- upload-data:/app/uploaded_files - upload-data:/app/uploaded_files

View File

@ -1 +0,0 @@
reflex

View File

@ -0,0 +1,5 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

View File

@ -0,0 +1,14 @@
:{$PORT}
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
root * /srv
route {
try_files {path} {path}/ /404.html
file_server
}

View File

@ -10,31 +10,13 @@ FROM python:3.11
ARG PORT=8080 ARG PORT=8080
# Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend. # Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend.
ARG API_URL ARG API_URL
ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
# Install Caddy server inside image # Install Caddy and redis server inside image
RUN apt-get update -y && apt-get install -y caddy && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
# Create a simple Caddyfile to serve as reverse proxy
RUN cat > Caddyfile <<EOF
:{\$PORT}
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
root * /srv
route {
try_files {path} {path}/ /404.html
file_server
}
EOF
# Copy local context to `/app` inside container (see .dockerignore) # Copy local context to `/app` inside container (see .dockerignore)
COPY . . COPY . .
@ -54,4 +36,6 @@ EXPOSE $PORT
# Apply migrations before starting the backend. # Apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \ CMD [ -d alembic ] && reflex db migrate; \
caddy start && reflex run --env prod --backend-only --loglevel debug caddy start && \
redis-server --daemonize yes && \
exec reflex run --env prod --backend-only

View File

@ -0,0 +1,36 @@
# simple-one-port
This docker deployment runs Reflex in prod mode, exposing a single HTTP port:
* `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend.
The deployment also runs a local Redis server to store state for each user.
Using this method may be preferable for deploying in memory constrained
environments, because it serves a static frontend export, rather than running
the NextJS server via node.
For platforms which only terminate TLS to a single port, this container can be
deployed instead of the `simple-two-port` example.
## Build
```console
docker build -t reflex-simple-one-port .
```
## Run
```console
docker run -p 8080:8080 reflex-simple-one-port
```
Note that this container has _no persistence_ and will lose all data when
stopped. You can use bind mounts or named volumes to persist the database and
uploaded_files directories as needed.
## Usage
This container should be used with an existing load balancer or reverse proxy to
terminate TLS.
It is also useful for deploying to simple app platforms, such as Render or Heroku.

View File

@ -0,0 +1,5 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

View File

@ -1,5 +1,8 @@
# This Dockerfile is used to deploy a simple single-container Reflex app instance. # This Dockerfile is used to deploy a simple single-container Reflex app instance.
FROM python:3.11 FROM python:3.12
RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/*
ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
# Copy local context to `/app` inside container (see .dockerignore) # Copy local context to `/app` inside container (see .dockerignore)
WORKDIR /app WORKDIR /app
@ -18,4 +21,6 @@ RUN reflex export --frontend-only --no-zip
STOPSIGNAL SIGKILL STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend. # Always apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; reflex run --env prod CMD [ -d alembic ] && reflex db migrate; \
redis-server --daemonize yes && \
exec reflex run --env prod

View File

@ -0,0 +1,44 @@
# simple-two-port
This docker deployment runs Reflex in prod mode, exposing two HTTP ports:
* `3000` - node NextJS server using optimized production build
* `8000` - python gunicorn server hosting the Reflex backend
The deployment also runs a local Redis server to store state for each user.
## Build
```console
docker build -t reflex-simple-two-port .
```
## Run
```console
docker run -p 3000:3000 -p 8000:8000 reflex-simple-two-port
```
Note that this container has _no persistence_ and will lose all data when
stopped. You can use bind mounts or named volumes to persist the database and
uploaded_files directories as needed.
## Usage
This container should be used with an existing load balancer or reverse proxy to
route traffic to the appropriate port inside the container.
For example, the following Caddyfile can be used to terminate TLS and forward
traffic to the frontend and backend from outside the container.
```
my-domain.com
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
reverse_proxy localhost:3000
```

View File

@ -10,7 +10,6 @@
### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨** ### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -35,7 +34,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
## ⚙️ Installation ## ⚙️ Installation
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.8+): Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
## ⚙️ Instalación ## ⚙️ Instalación
Abra un terminal y ejecute (Requiere Python 3.8+): Abra un terminal y ejecute (Requiere Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -11,7 +11,6 @@ Pynecone की तलाश हैं? आप सही रेपो में
### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨** ### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -36,7 +35,7 @@ Reflex के अंदर के कामकाज को जानने क
## ⚙️ इंस्टॉलेशन (Installation) ## ⚙️ इंस्टॉलेशन (Installation)
एक टर्मिनल खोलें और चलाएं (Python 3.8+ की आवश्यकता है): एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है):
```bash ```bash
pip install reflex pip install reflex

View File

@ -10,7 +10,6 @@
### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨** ### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -23,7 +22,7 @@
## ⚙️ Installazione ## ⚙️ Installazione
Apri un terminale ed esegui (Richiede Python 3.8+): Apri un terminale ed esegui (Richiede Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -11,7 +11,6 @@
### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨** ### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)

View File

@ -10,7 +10,6 @@
### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨** ### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -21,7 +20,7 @@
--- ---
## ⚙️ 설치 ## ⚙️ 설치
터미널을 열고 실행하세요. (Python 3.8+ 필요): 터미널을 열고 실행하세요. (Python 3.10+ 필요):
```bash ```bash
pip install reflex pip install reflex

View File

@ -10,7 +10,6 @@
### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨** ### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -35,7 +34,7 @@
## ⚙️ Installation - نصب و راه اندازی ## ⚙️ Installation - نصب و راه اندازی
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.8+): یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -21,7 +21,7 @@
--- ---
## ⚙️ Instalação ## ⚙️ Instalação
Abra um terminal e execute (Requer Python 3.8+): Abra um terminal e execute (Requer Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -11,7 +11,6 @@
### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨** ### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -25,7 +24,7 @@
## ⚙️ Kurulum ## ⚙️ Kurulum
Bir terminal açın ve çalıştırın (Python 3.8+ gerekir): Bir terminal açın ve çalıştırın (Python 3.10+ gerekir):
```bash ```bash
pip install reflex pip install reflex

View File

@ -10,7 +10,6 @@
### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨** ### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)

View File

@ -11,7 +11,6 @@
**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨** **✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -37,7 +36,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
## ⚙️ 安裝 ## ⚙️ 安裝
開啟一個終端機並且執行 (需要 Python 3.8+): 開啟一個終端機並且執行 (需要 Python 3.10+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -1,4 +1,4 @@
FROM python:3.8 FROM python:3.10
ARG USERNAME=kerrigan ARG USERNAME=kerrigan
RUN useradd -m $USERNAME RUN useradd -m $USERNAME

View File

@ -13,7 +13,6 @@ def BackgroundTask():
import asyncio import asyncio
import pytest import pytest
import reflex_chakra as rc
import reflex as rx import reflex as rx
from reflex.state import ImmutableStateError from reflex.state import ImmutableStateError
@ -87,6 +86,13 @@ def BackgroundTask():
third_state = await self.get_state(ThirdState) third_state = await self.get_state(ThirdState)
await third_state._triple_count() await third_state._triple_count()
@rx.background
async def yield_in_async_with_self(self):
async with self:
self.counter += 1
yield
self.counter += 1
class OtherState(rx.State): class OtherState(rx.State):
@rx.background @rx.background
async def get_other_state(self): async def get_other_state(self):
@ -109,11 +115,11 @@ def BackgroundTask():
def index() -> rx.Component: def index() -> rx.Component:
return rx.vstack( return rx.vstack(
rc.input( rx.input(
id="token", value=State.router.session.client_token, is_read_only=True id="token", value=State.router.session.client_token, is_read_only=True
), ),
rx.heading(State.counter, id="counter"), rx.heading(State.counter, id="counter"),
rc.input( rx.input(
id="iterations", id="iterations",
placeholder="Iterations", placeholder="Iterations",
value=State.iterations.to_string(), # type: ignore value=State.iterations.to_string(), # type: ignore
@ -155,6 +161,11 @@ def BackgroundTask():
on_click=OtherState.get_other_state, on_click=OtherState.get_other_state,
id="increment-from-other-state", id="increment-from-other-state",
), ),
rx.button(
"Yield in Async with Self",
on_click=State.yield_in_async_with_self,
id="yield-in-async-with-self",
),
rx.button("Reset", on_click=State.reset_counter, id="reset"), rx.button("Reset", on_click=State.reset_counter, id="reset"),
) )
@ -334,3 +345,30 @@ def test_get_state(
increment_button.click() increment_button.click()
assert background_task._poll_for(lambda: counter.text == "13", timeout=5) assert background_task._poll_for(lambda: counter.text == "13", timeout=5)
def test_yield_in_async_with_self(
background_task: AppHarness,
driver: WebDriver,
token: str,
):
"""Test that yielding inside async with self does not disable mutability.
Args:
background_task: harness for BackgroundTask app.
driver: WebDriver instance.
token: The token for the connected client.
"""
assert background_task.app_instance is not None
# get a reference to all buttons
yield_in_async_with_self_button = driver.find_element(
By.ID, "yield-in-async-with-self"
)
# get a reference to the counter
counter = driver.find_element(By.ID, "counter")
assert background_task._poll_for(lambda: counter.text == "0", timeout=5)
yield_in_async_with_self_button.click()
assert background_task._poll_for(lambda: counter.text == "2", timeout=5)

View File

@ -17,8 +17,6 @@ from . import utils
def ClientSide(): def ClientSide():
"""App for testing client-side state.""" """App for testing client-side state."""
import reflex_chakra as rc
import reflex as rx import reflex as rx
class ClientSideState(rx.State): class ClientSideState(rx.State):
@ -72,18 +70,18 @@ def ClientSide():
def index(): def index():
return rx.fragment( return rx.fragment(
rc.input( rx.input(
value=ClientSideState.router.session.client_token, value=ClientSideState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",
), ),
rc.input( rx.input(
placeholder="state var", placeholder="state var",
value=ClientSideState.state_var, value=ClientSideState.state_var,
on_change=ClientSideState.set_state_var, # type: ignore on_change=ClientSideState.set_state_var, # type: ignore
id="state_var", id="state_var",
), ),
rc.input( rx.input(
placeholder="input value", placeholder="input value",
value=ClientSideState.input_value, value=ClientSideState.input_value,
on_change=ClientSideState.set_input_value, # type: ignore on_change=ClientSideState.set_input_value, # type: ignore
@ -313,7 +311,6 @@ async def test_client_side_state(
# no cookies should be set yet! # no cookies should be set yet!
assert not driver.get_cookies() assert not driver.get_cookies()
local_storage_items = local_storage.items() local_storage_items = local_storage.items()
local_storage_items.pop("chakra-ui-color-mode", None)
local_storage_items.pop("last_compiled_time", None) local_storage_items.pop("last_compiled_time", None)
assert not local_storage_items assert not local_storage_items
@ -429,7 +426,6 @@ async def test_client_side_state(
assert f"{sub_state_name}.c3" not in cookie_info_map(driver) assert f"{sub_state_name}.c3" not in cookie_info_map(driver)
local_storage_items = local_storage.items() local_storage_items = local_storage.items()
local_storage_items.pop("chakra-ui-color-mode", None)
local_storage_items.pop("last_compiled_time", None) local_storage_items.pop("last_compiled_time", None)
assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value" assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value"
assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value" assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value"

View File

@ -17,13 +17,10 @@ def DynamicRoute():
"""App for testing dynamic routes.""" """App for testing dynamic routes."""
from typing import List from typing import List
import reflex_chakra as rc
import reflex as rx import reflex as rx
class DynamicState(rx.State): class DynamicState(rx.State):
order: List[str] = [] order: List[str] = []
page_id: str = ""
def on_load(self): def on_load(self):
self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}") self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
@ -42,13 +39,13 @@ def DynamicRoute():
def index(): def index():
return rx.fragment( return rx.fragment(
rc.input( rx.input(
value=DynamicState.router.session.client_token, value=DynamicState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",
), ),
rc.input(value=DynamicState.page_id, is_read_only=True, id="page_id"), rx.input(value=rx.State.page_id, is_read_only=True, id="page_id"), # type: ignore
rc.input( rx.input(
value=DynamicState.router.page.raw_path, value=DynamicState.router.page.raw_path,
is_read_only=True, is_read_only=True,
id="raw_path", id="raw_path",
@ -61,22 +58,80 @@ def DynamicRoute():
id="link_page_next", # type: ignore id="link_page_next", # type: ignore
), ),
rx.link("missing", href="/missing", id="link_missing"), rx.link("missing", href="/missing", id="link_missing"),
rc.list( rx.list( # type: ignore
rx.foreach( rx.foreach(
DynamicState.order, # type: ignore DynamicState.order, # type: ignore
lambda i: rc.list_item(rx.text(i)), lambda i: rx.list_item(rx.text(i)),
), ),
), ),
) )
class ArgState(rx.State):
"""The app state."""
@rx.var
def arg(self) -> int:
return int(self.arg_str or 0)
class ArgSubState(ArgState):
@rx.var(cache=True)
def cached_arg(self) -> int:
return self.arg
@rx.var(cache=True)
def cached_arg_str(self) -> str:
return self.arg_str
@rx.page(route="/arg/[arg_str]")
def arg() -> rx.Component:
return rx.vstack(
rx.data_list.root(
rx.data_list.item(
rx.data_list.label("rx.State.arg_str (dynamic)"),
rx.data_list.value(rx.State.arg_str, id="state-arg_str"), # type: ignore
),
rx.data_list.item(
rx.data_list.label("ArgState.arg_str (dynamic) (inherited)"),
rx.data_list.value(ArgState.arg_str, id="argstate-arg_str"), # type: ignore
),
rx.data_list.item(
rx.data_list.label("ArgState.arg"),
rx.data_list.value(ArgState.arg, id="argstate-arg"),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.arg_str (dynamic) (inherited)"),
rx.data_list.value(ArgSubState.arg_str, id="argsubstate-arg_str"), # type: ignore
),
rx.data_list.item(
rx.data_list.label("ArgSubState.arg (inherited)"),
rx.data_list.value(ArgSubState.arg, id="argsubstate-arg"),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.cached_arg"),
rx.data_list.value(
ArgSubState.cached_arg, id="argsubstate-cached_arg"
),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.cached_arg_str"),
rx.data_list.value(
ArgSubState.cached_arg_str, id="argsubstate-cached_arg_str"
),
),
),
rx.link("+", href=f"/arg/{ArgState.arg + 1}", id="next-page"),
align="center",
height="100vh",
)
@rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore
def redirect_page(): def redirect_page():
return rx.fragment(rx.text("redirecting...")) return rx.fragment(rx.text("redirecting..."))
app = rx.App(state=rx.State) app = rx.App(state=rx.State)
app.add_page(index)
app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) # type: ignore app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) # type: ignore
app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore
app.add_page(index)
app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore
@ -305,3 +360,50 @@ async def test_on_load_navigate_non_dynamic(
link.click() link.click()
assert urlsplit(driver.current_url).path == "/static/x/" assert urlsplit(driver.current_url).path == "/static/x/"
await poll_for_order(["/static/x-no page id", "/static/x-no page id"]) await poll_for_order(["/static/x-no page id", "/static/x-no page id"])
@pytest.mark.asyncio
async def test_render_dynamic_arg(
dynamic_route: AppHarness,
driver: WebDriver,
):
"""Assert that dynamic arg var is rendered correctly in different contexts.
Args:
dynamic_route: harness for DynamicRoute app.
driver: WebDriver instance.
"""
assert dynamic_route.app_instance is not None
with poll_for_navigation(driver):
driver.get(f"{dynamic_route.frontend_url}/arg/0")
def assert_content(expected: str, expect_not: str):
ids = [
"state-arg_str",
"argstate-arg",
"argstate-arg_str",
"argsubstate-arg_str",
"argsubstate-arg",
"argsubstate-cached_arg",
"argsubstate-cached_arg_str",
]
for id in ids:
el = driver.find_element(By.ID, id)
assert el
assert (
dynamic_route.poll_for_content(el, exp_not_equal=expect_not) == expected
)
assert_content("0", "")
next_page_link = driver.find_element(By.ID, "next-page")
assert next_page_link
with poll_for_navigation(driver):
next_page_link.click()
assert driver.current_url == f"{dynamic_route.frontend_url}/arg/1/"
assert_content("1", "0")
next_page_link = driver.find_element(By.ID, "next-page")
assert next_page_link
with poll_for_navigation(driver):
next_page_link.click()
assert driver.current_url == f"{dynamic_route.frontend_url}/arg/2/"
assert_content("2", "1")

View File

@ -16,8 +16,6 @@ def TestEventAction():
"""App for testing event_actions.""" """App for testing event_actions."""
from typing import List, Optional from typing import List, Optional
import reflex_chakra as rc
import reflex as rx import reflex as rx
class EventActionState(rx.State): class EventActionState(rx.State):
@ -55,7 +53,7 @@ def TestEventAction():
def index(): def index():
return rx.vstack( return rx.vstack(
rc.input( rx.input(
value=EventActionState.router.session.client_token, value=EventActionState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",
@ -148,10 +146,10 @@ def TestEventAction():
200 200
).stop_propagation, ).stop_propagation,
), ),
rc.list( rx.list( # type: ignore
rx.foreach( rx.foreach(
EventActionState.order, # type: ignore EventActionState.order, # type: ignore
rc.list_item, rx.list_item,
), ),
), ),
on_click=EventActionState.on_click("outer"), # type: ignore on_click=EventActionState.on_click("outer"), # type: ignore

View File

@ -18,8 +18,6 @@ def EventChain():
import time import time
from typing import List from typing import List
import reflex_chakra as rc
import reflex as rx import reflex as rx
# repeated here since the outer global isn't exported into the App module # repeated here since the outer global isn't exported into the App module
@ -129,7 +127,7 @@ def EventChain():
app = rx.App(state=rx.State) app = rx.App(state=rx.State)
token_input = rc.input( token_input = rx.input(
value=State.router.session.client_token, is_read_only=True, id="token" value=State.router.session.client_token, is_read_only=True, id="token"
) )
@ -137,7 +135,7 @@ def EventChain():
def index(): def index():
return rx.fragment( return rx.fragment(
token_input, token_input,
rc.input(value=State.interim_value, is_read_only=True, id="interim_value"), rx.input(value=State.interim_value, is_read_only=True, id="interim_value"),
rx.button( rx.button(
"Return Event", "Return Event",
id="return_event", id="return_event",

View File

@ -20,8 +20,6 @@ def FormSubmit(form_component):
""" """
from typing import Dict, List from typing import Dict, List
import reflex_chakra as rc
import reflex as rx import reflex as rx
class FormState(rx.State): class FormState(rx.State):
@ -37,28 +35,29 @@ def FormSubmit(form_component):
@app.add_page @app.add_page
def index(): def index():
return rx.vstack( return rx.vstack(
rc.input( rx.input(
value=FormState.router.session.client_token, value=FormState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",
), ),
eval(form_component)( eval(form_component)(
rx.vstack( rx.vstack(
rc.input(id="name_input"), rx.input(id="name_input"),
rx.hstack(rc.pin_input(length=4, id="pin_input")),
rc.number_input(id="number_input"),
rx.checkbox(id="bool_input"), rx.checkbox(id="bool_input"),
rx.switch(id="bool_input2"), rx.switch(id="bool_input2"),
rx.checkbox(id="bool_input3"), rx.checkbox(id="bool_input3"),
rx.switch(id="bool_input4"), rx.switch(id="bool_input4"),
rx.slider(id="slider_input", default_value=[50], width="100%"), rx.slider(id="slider_input", default_value=[50], width="100%"),
rc.range_slider(id="range_input"),
rx.radio(["option1", "option2"], id="radio_input"), rx.radio(["option1", "option2"], id="radio_input"),
rx.radio(FormState.var_options, id="radio_input_var"), rx.radio(FormState.var_options, id="radio_input_var"),
rc.select(["option1", "option2"], id="select_input"), rx.select(
rc.select(FormState.var_options, id="select_input_var"), ["option1", "option2"],
name="select_input",
default_value="option1",
),
rx.select(FormState.var_options, id="select_input_var"),
rx.text_area(id="text_area_input"), rx.text_area(id="text_area_input"),
rc.input( rx.input(
id="debounce_input", id="debounce_input",
debounce_timeout=0, debounce_timeout=0,
on_change=rx.console_log, on_change=rx.console_log,
@ -81,8 +80,6 @@ def FormSubmitName(form_component):
""" """
from typing import Dict, List from typing import Dict, List
import reflex_chakra as rc
import reflex as rx import reflex as rx
class FormState(rx.State): class FormState(rx.State):
@ -98,22 +95,19 @@ def FormSubmitName(form_component):
@app.add_page @app.add_page
def index(): def index():
return rx.vstack( return rx.vstack(
rc.input( rx.input(
value=FormState.router.session.client_token, value=FormState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",
), ),
eval(form_component)( eval(form_component)(
rx.vstack( rx.vstack(
rc.input(name="name_input"), rx.input(name="name_input"),
rx.hstack(rc.pin_input(length=4, name="pin_input")),
rc.number_input(name="number_input"),
rx.checkbox(name="bool_input"), rx.checkbox(name="bool_input"),
rx.switch(name="bool_input2"), rx.switch(name="bool_input2"),
rx.checkbox(name="bool_input3"), rx.checkbox(name="bool_input3"),
rx.switch(name="bool_input4"), rx.switch(name="bool_input4"),
rx.slider(name="slider_input", default_value=[50], width="100%"), rx.slider(name="slider_input", default_value=[50], width="100%"),
rc.range_slider(name="range_input"),
rx.radio(FormState.options, name="radio_input"), rx.radio(FormState.options, name="radio_input"),
rx.select( rx.select(
FormState.options, FormState.options,
@ -121,21 +115,13 @@ def FormSubmitName(form_component):
default_value=FormState.options[0], default_value=FormState.options[0],
), ),
rx.text_area(name="text_area_input"), rx.text_area(name="text_area_input"),
rc.input_group( rx.input(
rc.input_left_element(rx.icon(tag="chevron_right")), name="debounce_input",
rc.input( debounce_timeout=0,
name="debounce_input", on_change=rx.console_log,
debounce_timeout=0,
on_change=rx.console_log,
),
rc.input_right_element(rx.icon(tag="chevron_left")),
),
rc.button_group(
rx.button("Submit", type_="submit"),
rx.icon_button(FormState.val, icon=rx.icon(tag="plus")),
variant="outline",
is_attached=True,
), ),
rx.button("Submit", type_="submit"),
rx.icon_button(FormState.val, icon=rx.icon(tag="plus")),
), ),
on_submit=FormState.form_submit, on_submit=FormState.form_submit,
custom_attrs={"action": "/invalid"}, custom_attrs={"action": "/invalid"},
@ -152,16 +138,12 @@ def FormSubmitName(form_component):
functools.partial(FormSubmitName, form_component="rx.form.root"), functools.partial(FormSubmitName, form_component="rx.form.root"),
functools.partial(FormSubmit, form_component="rx.el.form"), functools.partial(FormSubmit, form_component="rx.el.form"),
functools.partial(FormSubmitName, form_component="rx.el.form"), functools.partial(FormSubmitName, form_component="rx.el.form"),
functools.partial(FormSubmit, form_component="rc.form"),
functools.partial(FormSubmitName, form_component="rc.form"),
], ],
ids=[ ids=[
"id-radix", "id-radix",
"name-radix", "name-radix",
"id-html", "id-html",
"name-html", "name-html",
"id-chakra",
"name-chakra",
], ],
) )
def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]: def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
@ -224,16 +206,6 @@ async def test_submit(driver, form_submit: AppHarness):
name_input = driver.find_element(by, "name_input") name_input = driver.find_element(by, "name_input")
name_input.send_keys("foo") name_input.send_keys("foo")
pin_inputs = driver.find_elements(By.CLASS_NAME, "chakra-pin-input")
pin_values = ["8", "1", "6", "4"]
for i, pin_input in enumerate(pin_inputs):
pin_input.send_keys(pin_values[i])
number_input = driver.find_element(By.CLASS_NAME, "chakra-numberinput")
buttons = number_input.find_elements(By.XPATH, "//div[@role='button']")
for _ in range(3):
buttons[1].click()
checkbox_input = driver.find_element(By.XPATH, "//button[@role='checkbox']") checkbox_input = driver.find_element(By.XPATH, "//button[@role='checkbox']")
checkbox_input.click() checkbox_input.click()
@ -275,15 +247,12 @@ async def test_submit(driver, form_submit: AppHarness):
print(form_data) print(form_data)
assert form_data["name_input"] == "foo" assert form_data["name_input"] == "foo"
assert form_data["pin_input"] == pin_values
assert form_data["number_input"] == "-3"
assert form_data["bool_input"] assert form_data["bool_input"]
assert form_data["bool_input2"] assert form_data["bool_input2"]
assert not form_data.get("bool_input3", False) assert not form_data.get("bool_input3", False)
assert not form_data.get("bool_input4", False) assert not form_data.get("bool_input4", False)
assert form_data["slider_input"] == "50" assert form_data["slider_input"] == "50"
assert form_data["range_input"] == ["25", "75"]
assert form_data["radio_input"] == "option2" assert form_data["radio_input"] == "option2"
assert form_data["select_input"] == "option1" assert form_data["select_input"] == "option1"
assert form_data["text_area_input"] == "Some\nText" assert form_data["text_area_input"] == "Some\nText"

View File

@ -11,8 +11,6 @@ from reflex.testing import AppHarness
def ServerSideEvent(): def ServerSideEvent():
"""App with inputs set via event handlers and set_value.""" """App with inputs set via event handlers and set_value."""
import reflex_chakra as rc
import reflex as rx import reflex as rx
class SSState(rx.State): class SSState(rx.State):
@ -41,12 +39,12 @@ def ServerSideEvent():
@app.add_page @app.add_page
def index(): def index():
return rx.fragment( return rx.fragment(
rc.input( rx.input(
id="token", value=SSState.router.session.client_token, is_read_only=True id="token", value=SSState.router.session.client_token, is_read_only=True
), ),
rc.input(default_value="a", id="a"), rx.input(default_value="a", id="a"),
rc.input(default_value="b", id="b"), rx.input(default_value="b", id="b"),
rc.input(default_value="c", id="c"), rx.input(default_value="c", id="c"),
rx.button( rx.button(
"Clear Immediate", "Clear Immediate",
id="clear_immediate", id="clear_immediate",

View File

@ -10,91 +10,47 @@ from reflex.testing import AppHarness
def Table(): def Table():
"""App using table component.""" """App using table component."""
from typing import List
import reflex_chakra as rc
import reflex as rx import reflex as rx
class TableState(rx.State):
rows: List[List[str]] = [
["John", "30", "New York"],
["Jane", "31", "San Fransisco"],
["Joe", "32", "Los Angeles"],
]
headers: List[str] = ["Name", "Age", "Location"]
footers: List[str] = ["footer1", "footer2", "footer3"]
caption: str = "random caption"
app = rx.App(state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
return rx.center( return rx.center(
rc.input( rx.input(
id="token", id="token",
value=TableState.router.session.client_token, value=rx.State.router.session.client_token,
is_read_only=True, is_read_only=True,
), ),
rc.table_container( rx.table.root(
rc.table( rx.table.header(
headers=TableState.headers, rx.table.row(
rows=TableState.rows, rx.table.column_header_cell("Name"),
footers=TableState.footers, rx.table.column_header_cell("Age"),
caption=TableState.caption, rx.table.column_header_cell("Location"),
variant="striped", ),
color_scheme="blue",
width="100%",
), ),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("John"),
rx.table.cell(30),
rx.table.cell("New York"),
),
rx.table.row(
rx.table.row_header_cell("Jane"),
rx.table.cell(31),
rx.table.cell("San Fransisco"),
),
rx.table.row(
rx.table.row_header_cell("Joe"),
rx.table.cell(32),
rx.table.cell("Los Angeles"),
),
),
width="100%",
), ),
) )
@app.add_page
def another():
return rx.center(
rc.table_container(
rc.table( # type: ignore
rc.thead( # type: ignore
rc.tr( # type: ignore
rc.th("Name"),
rc.th("Age"),
rc.th("Location"),
)
),
rc.tbody( # type: ignore
rc.tr( # type: ignore
rc.td("John"),
rc.td(30),
rc.td("New York"),
),
rc.tr( # type: ignore
rc.td("Jane"),
rc.td(31),
rc.td("San Francisco"),
),
rc.tr( # type: ignore
rc.td("Joe"),
rc.td(32),
rc.td("Los Angeles"),
),
),
rc.tfoot( # type: ignore
rc.tr(
rc.td("footer1"),
rc.td("footer2"),
rc.td("footer3"),
) # type: ignore
),
rc.table_caption("random caption"),
variant="striped",
color_scheme="teal",
)
)
)
@pytest.fixture() @pytest.fixture()
def table(tmp_path_factory) -> Generator[AppHarness, None, None]: def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
@ -138,23 +94,20 @@ def driver(table: AppHarness):
driver.quit() driver.quit()
@pytest.mark.parametrize("route", ["", "/another"]) def test_table(driver, table: AppHarness):
def test_table(driver, table: AppHarness, route):
"""Test that a table component is rendered properly. """Test that a table component is rendered properly.
Args: Args:
driver: Selenium WebDriver open to the app driver: Selenium WebDriver open to the app
table: Harness for Table app table: Harness for Table app
route: Page route or path.
""" """
driver.get(f"{table.frontend_url}/{route}")
assert table.app_instance is not None, "app is not running" assert table.app_instance is not None, "app is not running"
thead = driver.find_element(By.TAG_NAME, "thead") thead = driver.find_element(By.TAG_NAME, "thead")
# poll till page is fully loaded. # poll till page is fully loaded.
table.poll_for_content(element=thead) table.poll_for_content(element=thead)
# check headers # check headers
assert thead.find_element(By.TAG_NAME, "tr").text == "NAME AGE LOCATION" assert thead.find_element(By.TAG_NAME, "tr").text == "Name Age Location"
# check first row value # check first row value
assert ( assert (
driver.find_element(By.TAG_NAME, "tbody") driver.find_element(By.TAG_NAME, "tbody")
@ -162,12 +115,3 @@ def test_table(driver, table: AppHarness, route):
.text .text
== "John 30 New York" == "John 30 New York"
) )
# check footer
assert (
driver.find_element(By.TAG_NAME, "tfoot")
.find_element(By.TAG_NAME, "tr")
.text.lower()
== "footer1 footer2 footer3"
)
# check caption
assert driver.find_element(By.TAG_NAME, "caption").text == "random caption"

View File

@ -27,8 +27,6 @@ def TailwindApp(
""" """
from pathlib import Path from pathlib import Path
import reflex_chakra as rc
import reflex as rx import reflex as rx
class UnusedState(rx.State): class UnusedState(rx.State):
@ -36,7 +34,7 @@ def TailwindApp(
def index(): def index():
return rx.el.div( return rx.el.div(
rc.text(paragraph_text, class_name=paragraph_class_name), rx.text(paragraph_text, class_name=paragraph_class_name),
rx.el.p(paragraph_text, class_name=paragraph_class_name), rx.el.p(paragraph_text, class_name=paragraph_class_name),
rx.text(paragraph_text, as_="p", class_name=paragraph_class_name), rx.text(paragraph_text, as_="p", class_name=paragraph_class_name),
rx.el.div("Test external stylesheet", class_name="external"), rx.el.div("Test external stylesheet", class_name="external"),
@ -109,7 +107,7 @@ def test_tailwind_app(tailwind_app: AppHarness, tailwind_disabled: bool):
assert len(paragraphs) == 3 assert len(paragraphs) == 3
for p in paragraphs: for p in paragraphs:
assert tailwind_app.poll_for_content(p, exp_not_equal="") == PARAGRAPH_TEXT assert tailwind_app.poll_for_content(p, exp_not_equal="") == PARAGRAPH_TEXT
assert p.value_of_css_property("font-family") == '"monospace"' assert p.value_of_css_property("font-family") == "monospace"
if tailwind_disabled: if tailwind_disabled:
# expect default color, not "text-red-500" from tailwind utility class # expect default color, not "text-red-500" from tailwind utility class
assert p.value_of_css_property("color") not in TEXT_RED_500_COLOR assert p.value_of_css_property("color") not in TEXT_RED_500_COLOR

View File

@ -16,8 +16,6 @@ def UploadFile():
"""App for testing dynamic routes.""" """App for testing dynamic routes."""
from typing import Dict, List from typing import Dict, List
import reflex_chakra as rc
import reflex as rx import reflex as rx
class UploadState(rx.State): class UploadState(rx.State):
@ -46,7 +44,7 @@ def UploadFile():
def index(): def index():
return rx.vstack( return rx.vstack(
rc.input( rx.input(
value=UploadState.router.session.client_token, value=UploadState.router.session.client_token,
is_read_only=True, is_read_only=True,
id="token", id="token",

View File

@ -14,11 +14,12 @@ def VarOperations():
"""App with var operations.""" """App with var operations."""
from typing import Dict, List from typing import Dict, List
import reflex_chakra as rc
import reflex as rx import reflex as rx
from reflex.ivars.base import LiteralVar from reflex.vars.base import LiteralVar
from reflex.ivars.sequence import ArrayVar from reflex.vars.sequence import ArrayVar
class Object(rx.Base):
str: str = "hello"
class VarOperationState(rx.State): class VarOperationState(rx.State):
int_var1: int = 10 int_var1: int = 10
@ -29,6 +30,7 @@ def VarOperations():
list1: List = [1, 2] list1: List = [1, 2]
list2: List = [3, 4] list2: List = [3, 4]
list3: List = ["first", "second", "third"] list3: List = ["first", "second", "third"]
list4: List = [Object(name="obj_1"), Object(name="obj_2")]
str_var1: str = "first" str_var1: str = "first"
str_var2: str = "second" str_var2: str = "second"
str_var3: str = "ThIrD" str_var3: str = "ThIrD"
@ -474,6 +476,7 @@ def VarOperations():
rx.text( rx.text(
VarOperationState.list1.contains(1).to_string(), id="list_contains" VarOperationState.list1.contains(1).to_string(), id="list_contains"
), ),
rx.text(VarOperationState.list4.pluck("name").to_string(), id="list_pluck"),
rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"), rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"),
# LIST, INT # LIST, INT
rx.text( rx.text(
@ -547,10 +550,7 @@ def VarOperations():
VarOperationState.html_str, VarOperationState.html_str,
id="html_str", id="html_str",
), ),
rc.highlight( rx.el.mark("second"),
"second",
query=[VarOperationState.str_var2],
),
rx.text(ArrayVar.range(2, 5).join(","), id="list_join_range1"), rx.text(ArrayVar.range(2, 5).join(","), id="list_join_range1"),
rx.text(ArrayVar.range(2, 10, 2).join(","), id="list_join_range2"), rx.text(ArrayVar.range(2, 10, 2).join(","), id="list_join_range2"),
rx.text(ArrayVar.range(5, 0, -1).join(","), id="list_join_range3"), rx.text(ArrayVar.range(5, 0, -1).join(","), id="list_join_range3"),
@ -588,6 +588,16 @@ def VarOperations():
int_var2=VarOperationState.int_var2, int_var2=VarOperationState.int_var2,
id="memo_comp_nested", id="memo_comp_nested",
), ),
# foreach in a match
rx.box(
rx.match(
VarOperationState.list3.length(),
(0, rx.text("No choices")),
(1, rx.text("One choice")),
rx.foreach(VarOperationState.list3, lambda choice: rx.text(choice)),
),
id="foreach_in_match",
),
) )
@ -749,6 +759,7 @@ def test_var_operations(driver, var_operations: AppHarness):
("list_and_list", "[3,4]"), ("list_and_list", "[3,4]"),
("list_or_list", "[1,2]"), ("list_or_list", "[1,2]"),
("list_contains", "true"), ("list_contains", "true"),
("list_pluck", '["obj_1","obj_2"]'),
("list_reverse", "[2,1]"), ("list_reverse", "[2,1]"),
("list_join", "firstsecondthird"), ("list_join", "firstsecondthird"),
("list_join_comma", "first,second,third"), ("list_join_comma", "first,second,third"),
@ -784,6 +795,8 @@ def test_var_operations(driver, var_operations: AppHarness):
# rx.memo component with state # rx.memo component with state
("memo_comp", "1210"), ("memo_comp", "1210"),
("memo_comp_nested", "345"), ("memo_comp_nested", "345"),
# foreach in a match
("foreach_in_match", "first\nsecond\nthird"),
] ]
for tag, expected in tests: for tag, expected in tests:

1429
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,10 +26,10 @@ packages = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.10"
dill = ">=0.3.8,<0.4" dill = ">=0.3.8,<0.4"
fastapi = ">=0.96.0,<0.111.0" fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<23.0" gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0" jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0" psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.2,<3.0" pydantic = ">=1.10.2,<3.0"
@ -40,8 +40,6 @@ rich = ">=13.0.0,<14.0"
sqlmodel = ">=0.0.14,<0.1" sqlmodel = ">=0.0.14,<0.1"
typer = ">=0.4.2,<1.0" typer = ">=0.4.2,<1.0"
uvicorn = ">=0.20.0" uvicorn = ">=0.20.0"
watchdog = ">=2.3.1,<5.0"
watchfiles = ">=0.19.0,<1.0"
starlette-admin = ">=0.11.0,<1.0" starlette-admin = ">=0.11.0,<1.0"
alembic = ">=1.11.1,<2.0" alembic = ">=1.11.1,<2.0"
platformdirs = ">=3.10.0,<5.0" platformdirs = ">=3.10.0,<5.0"
@ -61,7 +59,7 @@ httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<6.0" twine = ">=4.0.0,<6.0"
tomlkit = ">=0.12.4,<1.0" tomlkit = ">=0.12.4,<1.0"
lazy_loader = ">=0.4" lazy_loader = ">=0.4"
reflex-chakra = ">=0.6.0a" reflex-chakra = ">=0.6.0a6"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = ">=7.1.2,<8.0" pytest = ">=7.1.2,<8.0"
@ -72,16 +70,11 @@ toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.20.1,<0.22.0" # https://github.com/pytest-dev/pytest-asyncio/issues/706 pytest-asyncio = ">=0.20.1,<0.22.0" # https://github.com/pytest-dev/pytest-asyncio/issues/706
pytest-cov = ">=4.0.0,<5.0" pytest-cov = ">=4.0.0,<5.0"
ruff = "^0.4.9" ruff = "^0.4.9"
pandas = [ pandas = ">=2.1.1,<3.0"
{version = ">=2.1.1,<3.0", python = ">=3.9,<3.13"}, pillow = ">=10.0.0,<11.0"
{version = ">=1.5.3,<2.0", python = ">=3.8,<3.9"},
]
pillow = [
{version = ">=10.0.0,<11.0", python = ">=3.8,<4.0"}
]
plotly = ">=5.13.0,<6.0" plotly = ">=5.13.0,<6.0"
asynctest = ">=0.13.0,<1.0" asynctest = ">=0.13.0,<1.0"
pre-commit = {version = ">=3.2.1", python = ">=3.8,<4.0"} pre-commit = ">=3.2.1"
selenium = ">=4.11.0,<5.0" selenium = ">=4.11.0,<5.0"
pytest-benchmark = ">=4.0.0,<5.0" pytest-benchmark = ">=4.0.0,<5.0"
@ -95,7 +88,7 @@ build-backend = "poetry.core.masonry.api"
[tool.pyright] [tool.pyright]
[tool.ruff] [tool.ruff]
target-version = "py38" target-version = "py310"
lint.select = ["B", "D", "E", "F", "I", "SIM", "W"] lint.select = ["B", "D", "E", "F", "I", "SIM", "W"]
lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"] lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"]
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"

View File

@ -1,4 +0,0 @@
*.db
*.py[cod]
.web
__pycache__/

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,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,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 +0,0 @@
"""Base template for Reflex."""

View File

@ -1,127 +0,0 @@
"""Welcome to Reflex! This file outlines the steps to create a basic app."""
from typing import Callable
import reflex as rx
from .pages import chatapp_page, datatable_page, forms_page, graphing_page, home_page
from .sidebar import sidebar
from .state import State
from .styles import *
meta = [
{
"name": "viewport",
"content": "width=device-width, shrink-to-fit=no, initial-scale=1",
},
]
def template(main_content: Callable[[], rx.Component]) -> rx.Component:
"""The template for each page of the app.
Args:
main_content (Callable[[], rx.Component]): The main content of the page.
Returns:
rx.Component: The template for each page of the app.
"""
menu_button = rx.chakra.box(
rx.chakra.menu(
rx.chakra.menu_button(
rx.chakra.icon(
tag="hamburger",
size="4em",
color=text_color,
),
),
rx.chakra.menu_list(
rx.chakra.menu_item(rx.chakra.link("Home", href="/", width="100%")),
rx.chakra.menu_divider(),
rx.chakra.menu_item(
rx.chakra.link(
"About", href="https://github.com/reflex-dev", width="100%"
)
),
rx.chakra.menu_item(
rx.chakra.link(
"Contact", href="mailto:founders@reflex.dev", width="100%"
)
),
),
),
position="fixed",
right="1.5em",
top="1.5em",
z_index="500",
)
return rx.chakra.hstack(
sidebar(),
main_content(),
rx.chakra.spacer(),
menu_button,
align_items="flex-start",
transition="left 0.5s, width 0.5s",
position="relative",
left=rx.cond(State.sidebar_displayed, "0px", f"-{sidebar_width}"),
)
@rx.page("/", meta=meta)
@template
def home() -> rx.Component:
"""Home page.
Returns:
rx.Component: The home page.
"""
return home_page()
@rx.page("/forms", meta=meta)
@template
def forms() -> rx.Component:
"""Forms page.
Returns:
rx.Component: The settings page.
"""
return forms_page()
@rx.page("/graphing", meta=meta)
@template
def graphing() -> rx.Component:
"""Graphing page.
Returns:
rx.Component: The graphing page.
"""
return graphing_page()
@rx.page("/datatable", meta=meta)
@template
def datatable() -> rx.Component:
"""Data Table page.
Returns:
rx.Component: The chatapp page.
"""
return datatable_page()
@rx.page("/chatapp", meta=meta)
@template
def chatapp() -> rx.Component:
"""Chatapp page.
Returns:
rx.Component: The chatapp page.
"""
return chatapp_page()
# Create the app.
app = rx.App(style=base_style)

View File

@ -1,7 +0,0 @@
"""The pages of the app."""
from .chatapp import chatapp_page
from .datatable import datatable_page
from .forms import forms_page
from .graphing import graphing_page
from .home import home_page

View File

@ -1,31 +0,0 @@
"""The main Chat app."""
import reflex as rx
from ..styles import *
from ..webui import styles
from ..webui.components import chat, modal, navbar, sidebar
def chatapp_page() -> rx.Component:
"""The main app.
Returns:
The UI for the main app.
"""
return rx.chakra.box(
rx.chakra.vstack(
navbar(),
chat.chat(),
chat.action_bar(),
sidebar(),
modal(),
bg=styles.bg_dark_color,
color=styles.text_light_color,
min_h="100vh",
align_items="stretch",
spacing="0",
style=template_content_style,
),
style=template_page_style,
)

View File

@ -1,360 +0,0 @@
"""The settings page for the template."""
from typing import Any
import reflex as rx
from reflex.components.datadisplay.dataeditor import DataEditorTheme
from ..styles import *
from ..webui.state import State
class DataTableState(State):
"""Datatable state."""
cols: list[Any] = [
{"title": "Title", "type": "str"},
{
"title": "Name",
"type": "str",
"group": "Data",
"width": 300,
},
{
"title": "Birth",
"type": "str",
"group": "Data",
"width": 150,
},
{
"title": "Human",
"type": "bool",
"group": "Data",
"width": 80,
},
{
"title": "House",
"type": "str",
"group": "Data",
},
{
"title": "Wand",
"type": "str",
"group": "Data",
"width": 250,
},
{
"title": "Patronus",
"type": "str",
"group": "Data",
},
{
"title": "Blood status",
"type": "str",
"group": "Data",
"width": 200,
},
]
data = [
[
"1",
"Harry James Potter",
"31 July 1980",
True,
"Gryffindor",
"11' Holly phoenix feather",
"Stag",
"Half-blood",
],
[
"2",
"Ronald Bilius Weasley",
"1 March 1980",
True,
"Gryffindor",
"12' Ash unicorn tail hair",
"Jack Russell terrier",
"Pure-blood",
],
[
"3",
"Hermione Jean Granger",
"19 September, 1979",
True,
"Gryffindor",
"10¾' vine wood dragon heartstring",
"Otter",
"Muggle-born",
],
[
"4",
"Albus Percival Wulfric Brian Dumbledore",
"Late August 1881",
True,
"Gryffindor",
"15' Elder Thestral tail hair core",
"Phoenix",
"Half-blood",
],
[
"5",
"Rubeus Hagrid",
"6 December 1928",
False,
"Gryffindor",
"16' Oak unknown core",
"None",
"Part-Human (Half-giant)",
],
[
"6",
"Fred Weasley",
"1 April, 1978",
True,
"Gryffindor",
"Unknown",
"Unknown",
"Pure-blood",
],
[
"7",
"George Weasley",
"1 April, 1978",
True,
"Gryffindor",
"Unknown",
"Unknown",
"Pure-blood",
],
]
code_show = """rx.chakra.hstack(
rx.chakra.divider(orientation="vertical", height="100vh", border="solid black 1px"),
rx.chakra.vstack(
rx.chakra.box(
rx.data_editor(
columns=DataTableState.cols,
data=DataTableState.data,
draw_focus_ring=True,
row_height=50,
smooth_scroll_x=True,
smooth_scroll_y=True,
column_select="single",
# style
theme=DataEditorTheme(**darkTheme),
width="80vw",
height="80vh",
),
),
rx.chakra.spacer(),
height="100vh",
spacing="25",
),
)"""
state_show = """class DataTableState(State):
cols: list[Any] = [
{"title": "Title", "type": "str"},
{
"title": "Name",
"type": "str",
"group": "Data",
"width": 300,
},
{
"title": "Birth",
"type": "str",
"group": "Data",
"width": 150,
},
{
"title": "Human",
"type": "bool",
"group": "Data",
"width": 80,
},
{
"title": "House",
"type": "str",
"group": "Data",
},
{
"title": "Wand",
"type": "str",
"group": "Data",
"width": 250,
},
{
"title": "Patronus",
"type": "str",
"group": "Data",
},
{
"title": "Blood status",
"type": "str",
"group": "Data",
"width": 200,
},
]"""
data_show = """[
["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"],
["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"],
["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"],
["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"],
["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"],
["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"],
["7", "George Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"],
]"""
darkTheme = {
"accent_color": "#8c96ff",
"accent_light": "rgba(202, 206, 255, 0.253)",
"text_dark": "#ffffff",
"text_medium": "#b8b8b8",
"text_light": "#a0a0a0",
"text_bubble": "#ffffff",
"bg_icon_header": "#b8b8b8",
"fg_icon_header": "#000000",
"text_header": "#a1a1a1",
"text_header_selected": "#000000",
"bg_cell": "#16161b",
"bg_cell_medium": "#202027",
"bg_header": "#212121",
"bg_header_has_focus": "#474747",
"bg_header_hovered": "#404040",
"bg_bubble": "#212121",
"bg_bubble_selected": "#000000",
"bg_search_result": "#423c24",
"border_color": "rgba(225,225,225,0.2)",
"drilldown_border": "rgba(225,225,225,0.4)",
"link_color": "#4F5DFF",
"header_font_style": "bold 14px",
"base_font_style": "13px",
"font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif",
}
darkTheme_show = """darkTheme={
"accent_color": "#8c96ff",
"accent_light": "rgba(202, 206, 255, 0.253)",
"text_dark": "#ffffff",
"text_medium": "#b8b8b8",
"text_light": "#a0a0a0",
"text_bubble": "#ffffff",
"bg_icon_header": "#b8b8b8",
"fg_icon_header": "#000000",
"text_header": "#a1a1a1",
"text_header_selected": "#000000",
"bg_cell": "#16161b",
"bg_cell_medium": "#202027",
"bg_header": "#212121",
"bg_header_has_focus": "#474747",
"bg_header_hovered": "#404040",
"bg_bubble": "#212121",
"bg_bubble_selected": "#000000",
"bg_search_result": "#423c24",
"border_color": "rgba(225,225,225,0.2)",
"drilldown_border": "rgba(225,225,225,0.4)",
"link_color": "#4F5DFF",
"header_font_style": "bold 14px",
"base_font_style": "13px",
"font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif",
}"""
def datatable_page() -> rx.Component:
"""The UI for the settings page.
Returns:
rx.Component: The UI for the settings page.
"""
return rx.chakra.box(
rx.chakra.vstack(
rx.chakra.heading(
"Data Table Demo",
font_size="3em",
),
rx.chakra.hstack(
rx.chakra.vstack(
rx.chakra.box(
rx.data_editor(
columns=DataTableState.cols,
data=DataTableState.data,
draw_focus_ring=True,
row_height=50,
smooth_scroll_x=True,
smooth_scroll_y=True,
column_select="single",
# style
theme=DataEditorTheme(**darkTheme),
width="80vw",
),
),
rx.chakra.spacer(),
spacing="25",
),
),
rx.chakra.tabs(
rx.chakra.tab_list(
rx.chakra.tab("Code", style=tab_style),
rx.chakra.tab("Data", style=tab_style),
rx.chakra.tab("State", style=tab_style),
rx.chakra.tab("Styling", style=tab_style),
padding_x=0,
),
rx.chakra.tab_panels(
rx.chakra.tab_panel(
rx.code_block(
code_show,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
data_show,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
state_show,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
darkTheme_show,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
width="100%",
),
variant="unstyled",
color_scheme="purple",
align="end",
width="100%",
padding_top=".5em",
),
style=template_content_style,
),
style=template_page_style,
)

View File

@ -1,257 +0,0 @@
"""The settings page for the template."""
import reflex as rx
from ..states.form_state import FormState, UploadState
from ..styles import *
forms_1_code = """rx.chakra.vstack(
rx.chakra.form(
rx.chakra.vstack(
rx.chakra.input(
placeholder="First Name",
id="first_name",
),
rx.chakra.input(
placeholder="Last Name", id="last_name"
),
rx.chakra.hstack(
rx.chakra.checkbox("Checked", id="check"),
rx.chakra.switch("Switched", id="switch"),
),
rx.chakra.button("Submit",
type_="submit",
bg="#ecfdf5",
color="#047857",
border_radius="lg",
),
),
on_submit=FormState.handle_submit,
),
rx.chakra.divider(),
rx.chakra.heading("Results"),
rx.chakra.text(FormState.form_data.to_string()),
width="100%",
)"""
color = "rgb(107,99,246)"
forms_1_state = """class FormState(rx.State):
form_data: dict = {}
def handle_submit(self, form_data: dict):
"Handle the form submit."
self.form_data = form_data"""
forms_2_code = """rx.chakra.vstack(
rx.upload(
rx.chakra.vstack(
rx.chakra.button(
"Select File",
color=color,
bg="white",
border=f"1px solid {color}",
),
rx.chakra.text(
"Drag and drop files here or click to select files"
),
),
border=f"1px dotted {color}",
padding="5em",
),
rx.chakra.hstack(rx.foreach(rx.selected_files, rx.chakra.text)),
rx.chakra.button(
"Upload",
on_click=lambda: UploadState.handle_upload(
rx.upload_files()
),
),
rx.chakra.button(
"Clear",
on_click=rx.clear_selected_files,
),
rx.foreach(
UploadState.img, lambda img: rx.chakra.image(src=img, width="20%", height="auto",)
),
padding="5em",
width="100%",
)"""
forms_2_state = """class UploadState(State):
"The app state."
# The images to show.
img: list[str]
async def handle_upload(
self, files: list[rx.UploadFile]
):
"Handle the upload of file(s).
Args:
files: The uploaded files.
"
for file in files:
upload_data = await file.read()
outfile = rx.get_asset_path(file.filename)
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img.append(f"/{file.filename}")"""
def forms_page() -> rx.Component:
"""The UI for the settings page.
Returns:
rx.Component: The UI for the settings page.
"""
return rx.chakra.box(
rx.chakra.vstack(
rx.chakra.heading(
"Forms Demo",
font_size="3em",
),
rx.chakra.vstack(
rx.chakra.form(
rx.chakra.vstack(
rx.chakra.input(
placeholder="First Name",
id="first_name",
),
rx.chakra.input(placeholder="Last Name", id="last_name"),
rx.chakra.hstack(
rx.chakra.checkbox("Checked", id="check"),
rx.chakra.switch("Switched", id="switch"),
),
rx.chakra.button(
"Submit",
type_="submit",
bg="#ecfdf5",
color="#047857",
border_radius="lg",
),
),
on_submit=FormState.handle_submit,
),
rx.chakra.divider(),
rx.chakra.heading("Results"),
rx.chakra.text(FormState.form_data.to_string()),
width="100%",
),
rx.chakra.tabs(
rx.chakra.tab_list(
rx.chakra.tab("Code", style=tab_style),
rx.chakra.tab("State", style=tab_style),
padding_x=0,
),
rx.chakra.tab_panels(
rx.chakra.tab_panel(
rx.code_block(
forms_1_code,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
forms_1_state,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
width="100%",
),
variant="unstyled",
color_scheme="purple",
align="end",
width="100%",
padding_top=".5em",
),
rx.chakra.heading("Upload Example", font_size="3em"),
rx.chakra.text("Try uploading some images and see how they look."),
rx.chakra.vstack(
rx.upload(
rx.chakra.vstack(
rx.chakra.button(
"Select File",
color=color,
bg="white",
border=f"1px solid {color}",
),
rx.chakra.text(
"Drag and drop files here or click to select files"
),
),
border=f"1px dotted {color}",
padding="5em",
),
rx.chakra.hstack(rx.foreach(rx.selected_files, rx.chakra.text)),
rx.chakra.button(
"Upload",
on_click=lambda: UploadState.handle_upload(rx.upload_files()),
),
rx.chakra.button(
"Clear",
on_click=rx.clear_selected_files,
),
rx.foreach(
UploadState.img,
lambda img: rx.chakra.image(
src=img,
width="20%",
height="auto",
),
),
padding="5em",
width="100%",
),
rx.chakra.tabs(
rx.chakra.tab_list(
rx.chakra.tab("Code", style=tab_style),
rx.chakra.tab("State", style=tab_style),
padding_x=0,
),
rx.chakra.tab_panels(
rx.chakra.tab_panel(
rx.code_block(
forms_2_code,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
forms_2_state,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
width="100%",
),
variant="unstyled",
color_scheme="purple",
align="end",
width="100%",
padding_top=".5em",
),
style=template_content_style,
),
style=template_page_style,
)

View File

@ -1,253 +0,0 @@
"""The dashboard page for the template."""
import reflex as rx
from ..states.pie_state import PieChartState
from ..styles import *
data_1 = [
{"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400},
{"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210},
{"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290},
{"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000},
{"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181},
{"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500},
{"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100},
]
data_1_show = """[
{"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400},
{"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210},
{"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290},
{"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000},
{"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181},
{"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500},
{"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100},
]"""
graph_1_code = """rx.recharts.composed_chart(
rx.recharts.area(
data_key="uv", stroke="#8884d8", fill="#8884d8"
),
rx.recharts.bar(
data_key="amt", bar_size=20, fill="#413ea0"
),
rx.recharts.line(
data_key="pv", type_="monotone", stroke="#ff7300"
),
rx.recharts.x_axis(data_key="name"),
rx.recharts.y_axis(),
rx.recharts.cartesian_grid(stroke_dasharray="3 3"),
rx.recharts.graphing_tooltip(),
data=data,
)"""
graph_2_code = """rx.recharts.pie_chart(
rx.recharts.pie(
data=PieChartState.resources,
data_key="count",
name_key="type_",
cx="50%",
cy="50%",
start_angle=180,
end_angle=0,
fill="#8884d8",
label=True,
),
rx.recharts.graphing_tooltip(),
),
rx.chakra.vstack(
rx.foreach(
PieChartState.resource_types,
lambda type_, i: rx.chakra.hstack(
rx.chakra.button(
"-",
on_click=PieChartState.decrement(type_),
),
rx.chakra.text(
type_,
PieChartState.resources[i]["count"],
),
rx.chakra.button(
"+",
on_click=PieChartState.increment(type_),
),
),
),
)"""
graph_2_state = """class PieChartState(rx.State):
resources: list[dict[str, Any]] = [
dict(type_="🏆", count=1),
dict(type_="🪵", count=1),
dict(type_="🥑", count=1),
dict(type_="🧱", count=1),
]
@rx.cached_var
def resource_types(self) -> list[str]:
return [r["type_"] for r in self.resources]
def increment(self, type_: str):
for resource in self.resources:
if resource["type_"] == type_:
resource["count"] += 1
break
def decrement(self, type_: str):
for resource in self.resources:
if (
resource["type_"] == type_
and resource["count"] > 0
):
resource["count"] -= 1
break
"""
def graphing_page() -> rx.Component:
"""The UI for the dashboard page.
Returns:
rx.Component: The UI for the dashboard page.
"""
return rx.chakra.box(
rx.chakra.vstack(
rx.chakra.heading(
"Graphing Demo",
font_size="3em",
),
rx.chakra.heading(
"Composed Chart",
font_size="2em",
),
rx.chakra.stack(
rx.recharts.composed_chart(
rx.recharts.area(data_key="uv", stroke="#8884d8", fill="#8884d8"),
rx.recharts.bar(data_key="amt", bar_size=20, fill="#413ea0"),
rx.recharts.line(data_key="pv", type_="monotone", stroke="#ff7300"),
rx.recharts.x_axis(data_key="name"),
rx.recharts.y_axis(),
rx.recharts.cartesian_grid(stroke_dasharray="3 3"),
rx.recharts.graphing_tooltip(),
data=data_1,
# height="15em",
),
width="100%",
height="20em",
),
rx.chakra.tabs(
rx.chakra.tab_list(
rx.chakra.tab("Code", style=tab_style),
rx.chakra.tab("Data", style=tab_style),
padding_x=0,
),
rx.chakra.tab_panels(
rx.chakra.tab_panel(
rx.code_block(
graph_1_code,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
data_1_show,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
width="100%",
),
variant="unstyled",
color_scheme="purple",
align="end",
width="100%",
padding_top=".5em",
),
rx.chakra.heading("Interactive Example", font_size="2em"),
rx.chakra.hstack(
rx.recharts.pie_chart(
rx.recharts.pie(
data=PieChartState.resources,
data_key="count",
name_key="type_",
cx="50%",
cy="50%",
start_angle=180,
end_angle=0,
fill="#8884d8",
label=True,
),
rx.recharts.graphing_tooltip(),
),
rx.chakra.vstack(
rx.foreach(
PieChartState.resource_types,
lambda type_, i: rx.chakra.hstack(
rx.chakra.button(
"-",
on_click=PieChartState.decrement(type_),
),
rx.chakra.text(
type_,
PieChartState.resources[i]["count"],
),
rx.chakra.button(
"+",
on_click=PieChartState.increment(type_),
),
),
),
),
width="100%",
height="15em",
),
rx.chakra.tabs(
rx.chakra.tab_list(
rx.chakra.tab("Code", style=tab_style),
rx.chakra.tab("State", style=tab_style),
padding_x=0,
),
rx.chakra.tab_panels(
rx.chakra.tab_panel(
rx.code_block(
graph_2_code,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
rx.chakra.tab_panel(
rx.code_block(
graph_2_state,
language="python",
show_line_numbers=True,
),
width="100%",
padding_x=0,
padding_y=".25em",
),
width="100%",
),
variant="unstyled",
color_scheme="purple",
align="end",
width="100%",
padding_top=".5em",
),
style=template_content_style,
min_h="100vh",
),
style=template_page_style,
min_h="100vh",
)

View File

@ -1,56 +0,0 @@
"""The home page of the app."""
import reflex as rx
from ..styles import *
def home_page() -> rx.Component:
"""The UI for the home page.
Returns:
rx.Component: The UI for the home page.
"""
return rx.chakra.box(
rx.chakra.vstack(
rx.chakra.heading(
"Welcome to Reflex! 👋",
font_size="3em",
),
rx.chakra.text(
"Reflex is an open-source app framework built specifically to allow you to build web apps in pure python. 👈 Select a demo from the sidebar to see some examples of what Reflex can do!",
),
rx.chakra.heading(
"Things to check out:",
font_size="2em",
),
rx.chakra.unordered_list(
rx.chakra.list_item(
"Take a look at ",
rx.chakra.link(
"reflex.dev",
href="https://reflex.dev",
color="rgb(107,99,246)",
),
),
rx.chakra.list_item(
"Check out our ",
rx.chakra.link(
"docs",
href="https://reflex.dev/docs/getting-started/introduction/",
color="rgb(107,99,246)",
),
),
rx.chakra.list_item(
"Ask a question in our ",
rx.chakra.link(
"community",
href="https://discord.gg/T5WSbC2YtQ",
color="rgb(107,99,246)",
),
),
),
style=template_content_style,
),
style=template_page_style,
)

View File

@ -1,178 +0,0 @@
"""Sidebar component for the app."""
import reflex as rx
from .state import State
from .styles import *
def sidebar_header() -> rx.Component:
"""Sidebar header.
Returns:
rx.Component: The sidebar header component.
"""
return rx.chakra.hstack(
rx.chakra.image(
src="/icon.svg",
height="2em",
),
rx.chakra.spacer(),
rx.chakra.link(
rx.chakra.center(
rx.chakra.image(
src="/github.svg",
height="3em",
padding="0.5em",
),
box_shadow=box_shadow,
bg="transparent",
border_radius=border_radius,
_hover={
"bg": accent_color,
},
),
href="https://github.com/reflex-dev/reflex",
),
width="100%",
border_bottom=border,
padding="1em",
)
def sidebar_footer() -> rx.Component:
"""Sidebar footer.
Returns:
rx.Component: The sidebar footer component.
"""
return rx.chakra.hstack(
rx.chakra.link(
rx.chakra.center(
rx.chakra.image(
src="/paneleft.svg",
height="2em",
padding="0.5em",
),
bg="transparent",
border_radius=border_radius,
**hover_accent_bg,
),
on_click=State.toggle_sidebar_displayed,
transform=rx.cond(~State.sidebar_displayed, "rotate(180deg)", ""),
transition="transform 0.5s, left 0.5s",
position="relative",
left=rx.cond(State.sidebar_displayed, "0px", "20.5em"),
**overlapping_button_style,
),
rx.chakra.spacer(),
rx.chakra.link(
rx.chakra.text(
"Docs",
),
href="https://reflex.dev/docs/getting-started/introduction/",
),
rx.chakra.link(
rx.chakra.text(
"Blog",
),
href="https://reflex.dev/blog/",
),
width="100%",
border_top=border,
padding="1em",
)
def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
"""Sidebar item.
Args:
text (str): The text of the item.
icon (str): The icon of the item.
url (str): The URL of the item.
Returns:
rx.Component: The sidebar item component.
"""
return rx.chakra.link(
rx.chakra.hstack(
rx.chakra.image(
src=icon,
height="2.5em",
padding="0.5em",
),
rx.chakra.text(
text,
),
bg=rx.cond(
State.origin_url == f"/{text.lower()}/",
accent_color,
"transparent",
),
color=rx.cond(
State.origin_url == f"/{text.lower()}/",
accent_text_color,
text_color,
),
border_radius=border_radius,
box_shadow=box_shadow,
width="100%",
padding_x="1em",
),
href=url,
width="100%",
)
def sidebar() -> rx.Component:
"""Sidebar.
Returns:
rx.Component: The sidebar component.
"""
return rx.chakra.box(
rx.chakra.vstack(
sidebar_header(),
rx.chakra.vstack(
sidebar_item(
"Welcome",
"/github.svg",
"/",
),
sidebar_item(
"Graphing Demo",
"/github.svg",
"/graphing",
),
sidebar_item(
"Data Table Demo",
"/github.svg",
"/datatable",
),
sidebar_item(
"Forms Demo",
"/github.svg",
"/forms",
),
sidebar_item(
"Chat App Demo",
"/github.svg",
"/chatapp",
),
width="100%",
overflow_y="auto",
align_items="flex-start",
padding="1em",
),
rx.chakra.spacer(),
sidebar_footer(),
height="100dvh",
),
display=["none", "none", "block"],
min_width=sidebar_width,
height="100%",
position="sticky",
top="0px",
border_right=border,
)

View File

@ -1,22 +0,0 @@
"""Base state for the app."""
import reflex as rx
class State(rx.State):
"""State for the app."""
sidebar_displayed: bool = True
@rx.var
def origin_url(self) -> str:
"""Get the url of the current page.
Returns:
str: The url of the current page.
"""
return self.router_data.get("asPath", "")
def toggle_sidebar_displayed(self) -> None:
"""Toggle the sidebar displayed."""
self.sidebar_displayed = not self.sidebar_displayed

View File

@ -1,40 +0,0 @@
import reflex as rx
from ..state import State
class FormState(State):
"""Form state."""
form_data: dict = {}
def handle_submit(self, form_data: dict):
"""Handle the form submit.
Args:
form_data: The form data.
"""
self.form_data = form_data
class UploadState(State):
"""The app state."""
# The images to show.
img: list[str]
async def handle_upload(self, files: list[rx.UploadFile]):
"""Handle the upload of file(s).
Args:
files: The uploaded files.
"""
for file in files:
upload_data = await file.read()
outfile = rx.get_asset_path(file.filename)
# Save the file.
with open(outfile, "wb") as file_object:
file_object.write(upload_data)
# Update the img var.
self.img.append(f"/{file.filename}")

View File

@ -1,47 +0,0 @@
from typing import Any
import reflex as rx
from ..state import State
class PieChartState(State):
"""Pie Chart State."""
resources: list[dict[str, Any]] = [
dict(type_="🏆", count=1),
dict(type_="🪵", count=1),
dict(type_="🥑", count=1),
dict(type_="🧱", count=1),
]
@rx.cached_var
def resource_types(self) -> list[str]:
"""Get the resource types.
Returns:
The resource types.
"""
return [r["type_"] for r in self.resources]
def increment(self, type_: str):
"""Increment the count of a resource type.
Args:
type_: The type of resource to increment.
"""
for resource in self.resources:
if resource["type_"] == type_:
resource["count"] += 1
break
def decrement(self, type_: str):
"""Decrement the count of a resource type.
Args:
type_: The type of resource to decrement.
"""
for resource in self.resources:
if resource["type_"] == type_ and resource["count"] > 0:
resource["count"] -= 1
break

View File

@ -1,68 +0,0 @@
"""Styles for the app."""
import reflex as rx
from .state import State
border_radius = "0.375rem"
box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
border = "1px solid #F4F3F6"
text_color = "black"
accent_text_color = "#1A1060"
accent_color = "#F5EFFE"
hover_accent_color = {"_hover": {"color": accent_color}}
hover_accent_bg = {"_hover": {"bg": accent_color}}
content_width_vw = "90vw"
sidebar_width = "20em"
template_page_style = {
"padding_top": "5em",
"padding_x": "2em",
}
template_content_style = {
"width": rx.cond(
State.sidebar_displayed,
f"calc({content_width_vw} - {sidebar_width})",
content_width_vw,
),
"min-width": sidebar_width,
"align_items": "flex-start",
"box_shadow": box_shadow,
"border_radius": border_radius,
"padding": "1em",
"margin_bottom": "2em",
}
link_style = {
"color": text_color,
"text_decoration": "none",
**hover_accent_color,
}
overlapping_button_style = {
"background_color": "white",
"border": border,
"border_radius": border_radius,
}
base_style = {
rx.chakra.MenuButton: {
"width": "3em",
"height": "3em",
**overlapping_button_style,
},
rx.chakra.MenuItem: hover_accent_bg,
}
tab_style = {
"color": "#494369",
"font_weight": 600,
"_selected": {
"color": "#5646ED",
"bg": "#F5EFFE",
"padding_x": "0.5em",
"padding_y": "0.25em",
"border_radius": "8px",
},
}

View File

@ -1,4 +0,0 @@
from .loading_icon import loading_icon
from .modal import modal
from .navbar import navbar
from .sidebar import sidebar

View File

@ -1,118 +0,0 @@
import reflex as rx
from ...webui import styles
from ...webui.components import loading_icon
from ...webui.state import QA, State
def message(qa: QA) -> rx.Component:
"""A single question/answer message.
Args:
qa: The question/answer pair.
Returns:
A component displaying the question/answer pair.
"""
return rx.chakra.box(
rx.chakra.box(
rx.chakra.text(
qa.question,
bg=styles.border_color,
shadow=styles.shadow_light,
**styles.message_style,
),
text_align="right",
margin_top="1em",
),
rx.chakra.box(
rx.chakra.text(
qa.answer,
bg=styles.accent_color,
shadow=styles.shadow_light,
**styles.message_style,
),
text_align="left",
padding_top="1em",
),
width="100%",
)
def chat() -> rx.Component:
"""List all the messages in a single conversation.
Returns:
A component displaying all the messages in a single conversation.
"""
return rx.chakra.vstack(
rx.chakra.box(rx.foreach(State.chats[State.current_chat], message)),
py="8",
flex="1",
width="100%",
max_w="3xl",
padding_x="4",
align_self="center",
overflow="hidden",
padding_bottom="5em",
**styles.base_style[rx.chakra.Vstack],
)
def action_bar() -> rx.Component:
"""The action bar to send a new message.
Returns:
The action bar to send a new message.
"""
return rx.chakra.box(
rx.chakra.vstack(
rx.chakra.form(
rx.chakra.form_control(
rx.chakra.hstack(
rx.chakra.input(
placeholder="Type something...",
value=State.question,
on_change=State.set_question,
_placeholder={"color": "#fffa"},
_hover={"border_color": styles.accent_color},
style=styles.input_style,
),
rx.chakra.button(
rx.cond(
State.processing,
loading_icon(height="1em"),
rx.chakra.text("Send"),
),
type_="submit",
_hover={"bg": styles.accent_color},
style=styles.input_style,
),
**styles.base_style[rx.chakra.Hstack],
),
is_disabled=State.processing,
),
on_submit=State.process_question,
width="100%",
),
rx.chakra.text(
"ReflexGPT may return factually incorrect or misleading responses. Use discretion.",
font_size="xs",
color="#fff6",
text_align="center",
),
width="100%",
max_w="3xl",
mx="auto",
**styles.base_style[rx.chakra.Vstack],
),
position="sticky",
bottom="0",
left="0",
py="4",
backdrop_filter="auto",
backdrop_blur="lg",
border_top=f"1px solid {styles.border_color}",
align_items="stretch",
width="100%",
)

View File

@ -1,19 +0,0 @@
import reflex as rx
class LoadingIcon(rx.Component):
"""A custom loading icon component."""
library = "react-loading-icons"
tag = "SpinningCircles"
stroke: rx.Var[str]
stroke_opacity: rx.Var[str]
fill: rx.Var[str]
fill_opacity: rx.Var[str]
stroke_width: rx.Var[str]
speed: rx.Var[str]
height: rx.Var[str]
on_change: rx.EventHandler[lambda status: [status]]
loading_icon = LoadingIcon.create

View File

@ -1,56 +0,0 @@
import reflex as rx
from ...webui.state import State
def modal() -> rx.Component:
"""A modal to create a new chat.
Returns:
The modal component.
"""
return rx.chakra.modal(
rx.chakra.modal_overlay(
rx.chakra.modal_content(
rx.chakra.modal_header(
rx.chakra.hstack(
rx.chakra.text("Create new chat"),
rx.chakra.icon(
tag="close",
font_size="sm",
on_click=State.toggle_modal,
color="#fff8",
_hover={"color": "#fff"},
cursor="pointer",
),
align_items="center",
justify_content="space-between",
)
),
rx.chakra.modal_body(
rx.chakra.input(
placeholder="Type something...",
on_blur=State.set_new_chat_name,
bg="#222",
border_color="#fff3",
_placeholder={"color": "#fffa"},
),
),
rx.chakra.modal_footer(
rx.chakra.button(
"Create",
bg="#5535d4",
box_shadow="md",
px="4",
py="2",
h="auto",
_hover={"bg": "#4c2db3"},
on_click=State.create_chat,
),
),
bg="#222",
color="#fff",
),
),
is_open=State.modal_open,
)

View File

@ -1,70 +0,0 @@
import reflex as rx
from ...webui import styles
from ...webui.state import State
def navbar():
return rx.chakra.box(
rx.chakra.hstack(
rx.chakra.hstack(
rx.chakra.icon(
tag="hamburger",
mr=4,
on_click=State.toggle_drawer,
cursor="pointer",
),
rx.chakra.link(
rx.chakra.box(
rx.chakra.image(src="favicon.ico", width=30, height="auto"),
p="1",
border_radius="6",
bg="#F0F0F0",
mr="2",
),
href="/",
),
rx.chakra.breadcrumb(
rx.chakra.breadcrumb_item(
rx.chakra.heading("ReflexGPT", size="sm"),
),
rx.chakra.breadcrumb_item(
rx.chakra.text(
State.current_chat, size="sm", font_weight="normal"
),
),
),
),
rx.chakra.hstack(
rx.chakra.button(
"+ New chat",
bg=styles.accent_color,
px="4",
py="2",
h="auto",
on_click=State.toggle_modal,
),
rx.chakra.menu(
rx.chakra.menu_button(
rx.chakra.avatar(name="User", size="md"),
rx.chakra.box(),
),
rx.chakra.menu_list(
rx.chakra.menu_item("Help"),
rx.chakra.menu_divider(),
rx.chakra.menu_item("Settings"),
),
),
spacing="8",
),
justify="space-between",
),
bg=styles.bg_dark_color,
backdrop_filter="auto",
backdrop_blur="lg",
p="4",
border_bottom=f"1px solid {styles.border_color}",
position="sticky",
top="0",
z_index="100",
)

View File

@ -1,66 +0,0 @@
import reflex as rx
from ...webui import styles
from ...webui.state import State
def sidebar_chat(chat: str) -> rx.Component:
"""A sidebar chat item.
Args:
chat: The chat item.
Returns:
The sidebar chat item.
"""
return rx.chakra.hstack(
rx.chakra.box(
chat,
on_click=lambda: State.set_chat(chat),
style=styles.sidebar_style,
color=styles.icon_color,
flex="1",
),
rx.chakra.box(
rx.chakra.icon(
tag="delete",
style=styles.icon_style,
on_click=State.delete_chat,
),
style=styles.sidebar_style,
),
color=styles.text_light_color,
cursor="pointer",
)
def sidebar() -> rx.Component:
"""The sidebar component.
Returns:
The sidebar component.
"""
return rx.chakra.drawer(
rx.chakra.drawer_overlay(
rx.chakra.drawer_content(
rx.chakra.drawer_header(
rx.chakra.hstack(
rx.chakra.text("Chats"),
rx.chakra.icon(
tag="close",
on_click=State.toggle_drawer,
style=styles.icon_style,
),
)
),
rx.chakra.drawer_body(
rx.chakra.vstack(
rx.foreach(State.chat_titles, lambda chat: sidebar_chat(chat)),
align_items="stretch",
)
),
),
),
placement="left",
is_open=State.drawer_open,
)

View File

@ -1,146 +0,0 @@
import asyncio
import reflex as rx
from ..state import State
# openai.api_key = os.environ["OPENAI_API_KEY"]
# openai.api_base = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1")
class QA(rx.Base):
"""A question and answer pair."""
question: str
answer: str
DEFAULT_CHATS = {
"Intros": [],
}
class State(State):
"""The app state."""
# A dict from the chat name to the list of questions and answers.
chats: dict[str, list[QA]] = DEFAULT_CHATS
# The current chat name.
current_chat = "Intros"
# The current question.
question: str
# Whether we are processing the question.
processing: bool = False
# The name of the new chat.
new_chat_name: str = ""
# Whether the drawer is open.
drawer_open: bool = False
# Whether the modal is open.
modal_open: bool = False
def create_chat(self):
"""Create a new chat."""
# Add the new chat to the list of chats.
self.current_chat = self.new_chat_name
self.chats[self.new_chat_name] = []
# Toggle the modal.
self.modal_open = False
def toggle_modal(self):
"""Toggle the new chat modal."""
self.modal_open = not self.modal_open
def toggle_drawer(self):
"""Toggle the drawer."""
self.drawer_open = not self.drawer_open
def delete_chat(self):
"""Delete the current chat."""
del self.chats[self.current_chat]
if len(self.chats) == 0:
self.chats = DEFAULT_CHATS
# set self.current_chat to the first chat.
self.current_chat = next(iter(self.chats))
self.toggle_drawer()
def set_chat(self, chat_name: str):
"""Set the name of the current chat.
Args:
chat_name: The name of the chat.
"""
self.current_chat = chat_name
self.toggle_drawer()
@rx.var
def chat_titles(self) -> list[str]:
"""Get the list of chat titles.
Returns:
The list of chat names.
"""
return [*self.chats]
async def process_question(self, form_data: dict[str, str]):
"""Get the response from the API.
Args:
form_data: A dict with the current question.
Yields:
The current question and the response.
"""
# Check if the question is empty
if self.question == "":
return
# Add the question to the list of questions.
qa = QA(question=self.question, answer="")
self.chats[self.current_chat].append(qa)
# Clear the input and start the processing.
self.processing = True
self.question = ""
yield
# # Build the messages.
# messages = [
# {"role": "system", "content": "You are a friendly chatbot named Reflex."}
# ]
# for qa in self.chats[self.current_chat]:
# messages.append({"role": "user", "content": qa.question})
# messages.append({"role": "assistant", "content": qa.answer})
# # Remove the last mock answer.
# messages = messages[:-1]
# Start a new session to answer the question.
# session = openai.ChatCompletion.create(
# model=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"),
# messages=messages,
# stream=True,
# )
# Stream the results, yielding after every word.
# for item in session:
answer = "I don't know! This Chatbot still needs to add in AI API keys!"
for i in range(len(answer)):
# Pause to show the streaming effect.
await asyncio.sleep(0.1)
# Add one letter at a time to the output.
# if hasattr(item.choices[0].delta, "content"):
# answer_text = item.choices[0].delta.content
self.chats[self.current_chat][-1].answer += answer[i]
self.chats = self.chats
yield
# Toggle the processing flag.
self.processing = False

View File

@ -1,88 +0,0 @@
import reflex as rx
bg_dark_color = "#111"
bg_medium_color = "#222"
border_color = "#fff3"
accennt_light = "#6649D8"
accent_color = "#5535d4"
accent_dark = "#4c2db3"
icon_color = "#fff8"
text_light_color = "#fff"
shadow_light = "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px;"
shadow = "rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;"
message_style = dict(display="inline-block", p="4", border_radius="xl", max_w="30em")
input_style = dict(
bg=bg_medium_color,
border_color=border_color,
border_width="1px",
p="4",
)
icon_style = dict(
font_size="md",
color=icon_color,
_hover=dict(color=text_light_color),
cursor="pointer",
w="8",
)
sidebar_style = dict(
border="double 1px transparent;",
border_radius="10px;",
background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accent_dark});",
background_origin="border-box;",
background_clip="padding-box, border-box;",
p="2",
_hover=dict(
background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accennt_light});",
),
)
base_style = {
rx.chakra.Avatar: {
"shadow": shadow,
"color": text_light_color,
# "bg": border_color,
},
rx.chakra.Button: {
"shadow": shadow,
"color": text_light_color,
"_hover": {
"bg": accent_dark,
},
},
rx.chakra.Menu: {
"bg": bg_dark_color,
"border": f"red",
},
rx.chakra.MenuList: {
"bg": bg_dark_color,
"border": f"1.5px solid {bg_medium_color}",
},
rx.chakra.MenuDivider: {
"border": f"1px solid {bg_medium_color}",
},
rx.chakra.MenuItem: {
"bg": bg_dark_color,
"color": text_light_color,
},
rx.chakra.DrawerContent: {
"bg": bg_dark_color,
"color": text_light_color,
"opacity": "0.9",
},
rx.chakra.Hstack: {
"align_items": "center",
"justify_content": "space-between",
},
rx.chakra.Vstack: {
"align_items": "stretch",
"justify_content": "space-between",
},
}

View File

@ -8,11 +8,11 @@ version = "0.0.1"
description = "Reflex custom component {{ module_name }}" description = "Reflex custom component {{ module_name }}"
readme = "README.md" readme = "README.md"
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
requires-python = ">=3.8" requires-python = ">=3.10"
authors = [{ name = "", email = "YOUREMAIL@domain.com" }] authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
keywords = ["reflex","reflex-custom-components"] keywords = ["reflex","reflex-custom-components"]
dependencies = ["reflex>=0.4.2"] dependencies = ["reflex>={{ reflex_version }}"]
classifiers = ["Development Status :: 4 - Beta"] classifiers = ["Development Status :: 4 - Beta"]

View File

@ -7,6 +7,10 @@ import '/styles/styles.css'
{% block declaration %} {% block declaration %}
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js"; import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import * as React from "react";
import * as utils_context from "/utils/context.js";
import * as utils_state from "/utils/state.js";
import * as radix from "@radix-ui/themes";
{% for custom_code in custom_codes %} {% for custom_code in custom_codes %}
{{custom_code}} {{custom_code}}
@ -26,8 +30,18 @@ function AppWrap({children}) {
} }
export default function MyApp({ Component, pageProps }) { export default function MyApp({ Component, pageProps }) {
React.useEffect(() => {
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {
"react": React,
"@radix-ui/themes": radix,
"/utils/context": utils_context,
"/utils/state": utils_state,
};
window["__reflex"] = windowImports;
}, []);
return ( return (
<ThemeProvider defaultTheme={ defaultColorMode } storageKey="chakra-ui-color-mode" attribute="class"> <ThemeProvider defaultTheme={ defaultColorMode } attribute="class">
<AppWrap> <AppWrap>
<StateProvider> <StateProvider>
<EventLoopProvider> <EventLoopProvider>

View File

@ -64,11 +64,11 @@
{# Args: #} {# Args: #}
{# component: component dictionary #} {# component: component dictionary #}
{% macro render_iterable_tag(component) %} {% macro render_iterable_tag(component) %}
{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( <>{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
{% for child in component.children %} {% for child in component.children %}
{{ render(child) }} {{ render(child) }}
{% endfor %} {% endfor %}
))} ))}</>
{%- endmacro %} {%- endmacro %}
@ -85,10 +85,10 @@
{% macro render_match_tag(component) %} {% macro render_match_tag(component) %}
{ {
(() => { (() => {
switch (JSON.stringify({{ component.cond._var_name_unwrapped }})) { switch (JSON.stringify({{ component.cond._js_expr }})) {
{% for case in component.match_cases %} {% for case in component.match_cases %}
{% for condition in case[:-1] %} {% for condition in case[:-1] %}
case JSON.stringify({{ condition._var_name_unwrapped }}): case JSON.stringify({{ condition._js_expr }}):
{% endfor %} {% endfor %}
return {{ case[-1] }}; return {{ case[-1] }};
break; break;

View File

@ -1 +1 @@
export default {{ theme|json_dumps }} export default {{ theme }}

View File

@ -1,36 +0,0 @@
import { useColorMode as chakraUseColorMode } from "@chakra-ui/react";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { ColorModeContext, defaultColorMode } from "/utils/context.js";
export default function ChakraColorModeProvider({ children }) {
const { theme, resolvedTheme, setTheme } = useTheme();
const { colorMode, toggleColorMode } = chakraUseColorMode();
const [resolvedColorMode, setResolvedColorMode] = useState(colorMode);
useEffect(() => {
if (colorMode != resolvedTheme) {
toggleColorMode();
}
setResolvedColorMode(resolvedTheme);
}, [theme, resolvedTheme]);
const rawColorMode = colorMode;
const setColorMode = (mode) => {
const allowedModes = ["light", "dark", "system"];
if (!allowedModes.includes(mode)) {
console.error(
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`
);
mode = defaultColorMode;
}
setTheme(mode);
};
return (
<ColorModeContext.Provider
value={{ rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }}
>
{children}
</ColorModeContext.Provider>
);
}

View File

@ -15,6 +15,7 @@ import {
} from "utils/context.js"; } from "utils/context.js";
import debounce from "/utils/helpers/debounce"; import debounce from "/utils/helpers/debounce";
import throttle from "/utils/helpers/throttle"; import throttle from "/utils/helpers/throttle";
import * as Babel from "@babel/standalone";
// Endpoint URLs. // Endpoint URLs.
const EVENTURL = env.EVENT; const EVENTURL = env.EVENT;
@ -117,8 +118,8 @@ export const isStateful = () => {
if (event_queue.length === 0) { if (event_queue.length === 0) {
return false; return false;
} }
return event_queue.some(event => event.name.startsWith("reflex___state")); return event_queue.some((event) => event.name.startsWith("reflex___state"));
} };
/** /**
* Apply a delta to the state. * Apply a delta to the state.
@ -129,6 +130,22 @@ export const applyDelta = (state, delta) => {
return { ...state, ...delta }; return { ...state, ...delta };
}; };
/**
* Evaluate a dynamic component.
* @param component The component to evaluate.
* @returns The evaluated component.
*/
export const evalReactComponent = async (component) => {
if (!window.React && window.__reflex) {
window.React = window.__reflex.react;
}
const output = Babel.transform(component, { presets: ["react"] }).code;
const encodedJs = encodeURIComponent(output);
const dataUri = "data:text/javascript;charset=utf-8," + encodedJs;
const module = await eval(`import(dataUri)`);
return module.default;
};
/** /**
* Only Queue and process events when websocket connection exists. * Only Queue and process events when websocket connection exists.
* @param event The event to queue. * @param event The event to queue.
@ -141,7 +158,7 @@ export const queueEventIfSocketExists = async (events, socket) => {
return; return;
} }
await queueEvents(events, socket); await queueEvents(events, socket);
} };
/** /**
* Handle frontend event or send the event to the backend via Websocket. * Handle frontend event or send the event to the backend via Websocket.
@ -208,7 +225,10 @@ export const applyEvent = async (event, socket) => {
const a = document.createElement("a"); const a = document.createElement("a");
a.hidden = true; a.hidden = true;
// Special case when linking to uploaded files // Special case when linking to uploaded files
a.href = event.payload.url.replace("${getBackendURL(env.UPLOAD)}", getBackendURL(env.UPLOAD)) a.href = event.payload.url.replace(
"${getBackendURL(env.UPLOAD)}",
getBackendURL(env.UPLOAD)
);
a.download = event.payload.filename; a.download = event.payload.filename;
a.click(); a.click();
a.remove(); a.remove();
@ -249,7 +269,7 @@ export const applyEvent = async (event, socket) => {
} catch (e) { } catch (e) {
console.log("_call_script", e); console.log("_call_script", e);
if (window && window?.onerror) { if (window && window?.onerror) {
window.onerror(e.message, null, null, null, e) window.onerror(e.message, null, null, null, e);
} }
} }
return false; return false;
@ -290,10 +310,9 @@ export const applyEvent = async (event, socket) => {
export const applyRestEvent = async (event, socket) => { export const applyRestEvent = async (event, socket) => {
let eventSent = false; let eventSent = false;
if (event.handler === "uploadFiles") { if (event.handler === "uploadFiles") {
if (event.payload.files === undefined || event.payload.files.length === 0) { if (event.payload.files === undefined || event.payload.files.length === 0) {
// Submit the event over the websocket to trigger the event handler. // Submit the event over the websocket to trigger the event handler.
return await applyEvent(Event(event.name), socket) return await applyEvent(Event(event.name), socket);
} }
// Start upload, but do not wait for it, which would block other events. // Start upload, but do not wait for it, which would block other events.
@ -397,7 +416,7 @@ export const connect = async (
console.log("Disconnect backend before bfcache on navigation"); console.log("Disconnect backend before bfcache on navigation");
socket.current.disconnect(); socket.current.disconnect();
} }
} };
// Once the socket is open, hydrate the page. // Once the socket is open, hydrate the page.
socket.current.on("connect", () => { socket.current.on("connect", () => {
@ -416,7 +435,7 @@ export const connect = async (
}); });
// On each received message, queue the updates and events. // On each received message, queue the updates and events.
socket.current.on("event", (message) => { socket.current.on("event", async (message) => {
const update = JSON5.parse(message); const update = JSON5.parse(message);
for (const substate in update.delta) { for (const substate in update.delta) {
dispatch[substate](update.delta[substate]); dispatch[substate](update.delta[substate]);
@ -574,7 +593,11 @@ export const hydrateClientStorage = (client_storage) => {
} }
} }
} }
if (client_storage.cookies || client_storage.local_storage || client_storage.session_storage) { if (
client_storage.cookies ||
client_storage.local_storage ||
client_storage.session_storage
) {
return client_storage_values; return client_storage_values;
} }
return {}; return {};
@ -614,15 +637,17 @@ const applyClientStorageDelta = (client_storage, delta) => {
) { ) {
const options = client_storage.local_storage[state_key]; const options = client_storage.local_storage[state_key];
localStorage.setItem(options.name || state_key, delta[substate][key]); localStorage.setItem(options.name || state_key, delta[substate][key]);
} else if( } else if (
client_storage.session_storage && client_storage.session_storage &&
state_key in client_storage.session_storage && state_key in client_storage.session_storage &&
typeof window !== "undefined" typeof window !== "undefined"
) { ) {
const session_options = client_storage.session_storage[state_key]; const session_options = client_storage.session_storage[state_key];
sessionStorage.setItem(session_options.name || state_key, delta[substate][key]); sessionStorage.setItem(
session_options.name || state_key,
delta[substate][key]
);
} }
} }
} }
}; };
@ -651,7 +676,7 @@ export const useEventLoop = (
if (!(args instanceof Array)) { if (!(args instanceof Array)) {
args = [args]; args = [args];
} }
const _e = args.filter((o) => o?.preventDefault !== undefined)[0] const _e = args.filter((o) => o?.preventDefault !== undefined)[0];
if (event_actions?.preventDefault && _e?.preventDefault) { if (event_actions?.preventDefault && _e?.preventDefault) {
_e.preventDefault(); _e.preventDefault();
@ -671,7 +696,7 @@ export const useEventLoop = (
debounce( debounce(
combined_name, combined_name,
() => queueEvents(events, socket), () => queueEvents(events, socket),
event_actions.debounce, event_actions.debounce
); );
} else { } else {
queueEvents(events, socket); queueEvents(events, socket);
@ -696,30 +721,32 @@ export const useEventLoop = (
} }
}, [router.isReady]); }, [router.isReady]);
// Handle frontend errors and send them to the backend via websocket. // Handle frontend errors and send them to the backend via websocket.
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") {
if (typeof window === 'undefined') { return;
return; }
}
window.onerror = function (msg, url, lineNo, columnNo, error) {
addEvents([Event(`${exception_state_name}.handle_frontend_exception`, {
stack: error.stack,
})])
return false;
}
//NOTE: Only works in Chrome v49+ window.onerror = function (msg, url, lineNo, columnNo, error) {
//https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events addEvents([
window.onunhandledrejection = function (event) { Event(`${exception_state_name}.handle_frontend_exception`, {
addEvents([Event(`${exception_state_name}.handle_frontend_exception`, { stack: error.stack,
stack: event.reason.stack, }),
})]) ]);
return false; return false;
} };
},[]) //NOTE: Only works in Chrome v49+
//https://github.com/mknichel/javascript-errors?tab=readme-ov-file#promise-rejection-events
window.onunhandledrejection = function (event) {
addEvents([
Event(`${exception_state_name}.handle_frontend_exception`, {
stack: event.reason.stack,
}),
]);
return false;
};
}, []);
// Main event loop. // Main event loop.
useEffect(() => { useEffect(() => {
@ -782,11 +809,11 @@ export const useEventLoop = (
// Route after the initial page hydration. // Route after the initial page hydration.
useEffect(() => { useEffect(() => {
const change_start = () => { const change_start = () => {
const main_state_dispatch = dispatch["reflex___state____state"] const main_state_dispatch = dispatch["reflex___state____state"];
if (main_state_dispatch !== undefined) { if (main_state_dispatch !== undefined) {
main_state_dispatch({ is_hydrated: false }) main_state_dispatch({ is_hydrated: false });
} }
} };
const change_complete = () => addEvents(onLoadInternalEvent()); const change_complete = () => addEvents(onLoadInternalEvent());
router.events.on("routeChangeStart", change_start); router.events.on("routeChangeStart", change_start);
router.events.on("routeChangeComplete", change_complete); router.events.on("routeChangeComplete", change_complete);

View File

@ -140,6 +140,7 @@ RADIX_THEMES_COMPONENTS_MAPPING: dict = {
"components.radix.themes.components.radio_group": ["radio", "radio_group"], "components.radix.themes.components.radio_group": ["radio", "radio_group"],
"components.radix.themes.components.dropdown_menu": ["menu", "dropdown_menu"], "components.radix.themes.components.dropdown_menu": ["menu", "dropdown_menu"],
"components.radix.themes.components.separator": ["divider", "separator"], "components.radix.themes.components.separator": ["divider", "separator"],
"components.radix.themes.components.progress": ["progress"],
} }
RADIX_THEMES_LAYOUT_MAPPING: dict = { RADIX_THEMES_LAYOUT_MAPPING: dict = {
@ -205,7 +206,13 @@ RADIX_PRIMITIVES_MAPPING: dict = {
"components.radix.primitives.form": [ "components.radix.primitives.form": [
"form", "form",
], ],
"components.radix.primitives.progress": ["progress"], "components.radix.primitives.progress": [
"progress",
],
}
RADIX_PRIMITIVES_SHORTCUT_MAPPING: dict = {
k: v for k, v in RADIX_PRIMITIVES_MAPPING.items() if "progress" not in k
} }
COMPONENTS_CORE_MAPPING: dict = { COMPONENTS_CORE_MAPPING: dict = {
@ -248,7 +255,7 @@ RADIX_MAPPING: dict = {
**RADIX_THEMES_COMPONENTS_MAPPING, **RADIX_THEMES_COMPONENTS_MAPPING,
**RADIX_THEMES_TYPOGRAPHY_MAPPING, **RADIX_THEMES_TYPOGRAPHY_MAPPING,
**RADIX_THEMES_LAYOUT_MAPPING, **RADIX_THEMES_LAYOUT_MAPPING,
**RADIX_PRIMITIVES_MAPPING, **RADIX_PRIMITIVES_SHORTCUT_MAPPING,
} }
_MAPPING: dict = { _MAPPING: dict = {
@ -338,7 +345,6 @@ _SUBMODULES: set[str] = {
"testing", "testing",
"utils", "utils",
"vars", "vars",
"ivars",
"config", "config",
"compiler", "compiler",
} }

View File

@ -12,7 +12,6 @@ from . import compiler as compiler
from . import components as components from . import components as components
from . import config as config from . import config as config
from . import event as event from . import event as event
from . import ivars as ivars
from . import model as model from . import model as model
from . import style as style from . import style as style
from . import testing as testing from . import testing as testing
@ -71,7 +70,6 @@ from .components.plotly import plotly as plotly
from .components.radix.primitives.accordion import accordion as accordion from .components.radix.primitives.accordion import accordion as accordion
from .components.radix.primitives.drawer import drawer as drawer from .components.radix.primitives.drawer import drawer as drawer
from .components.radix.primitives.form import form as form from .components.radix.primitives.form import form as form
from .components.radix.primitives.progress import progress as progress
from .components.radix.themes.base import theme as theme from .components.radix.themes.base import theme as theme
from .components.radix.themes.base import theme_panel as theme_panel from .components.radix.themes.base import theme_panel as theme_panel
from .components.radix.themes.color_mode import color_mode as color_mode from .components.radix.themes.color_mode import color_mode as color_mode
@ -106,6 +104,7 @@ from .components.radix.themes.components.hover_card import hover_card as hover_c
from .components.radix.themes.components.icon_button import icon_button as icon_button from .components.radix.themes.components.icon_button import icon_button as icon_button
from .components.radix.themes.components.inset import inset as inset from .components.radix.themes.components.inset import inset as inset
from .components.radix.themes.components.popover import popover as popover from .components.radix.themes.components.popover import popover as popover
from .components.radix.themes.components.progress import progress as progress
from .components.radix.themes.components.radio_cards import radio_cards as radio_cards from .components.radix.themes.components.radio_cards import radio_cards as radio_cards
from .components.radix.themes.components.radio_group import radio as radio from .components.radix.themes.components.radio_group import radio as radio
from .components.radix.themes.components.radio_group import radio_group as radio_group from .components.radix.themes.components.radio_group import radio_group as radio_group
@ -132,6 +131,7 @@ from .components.radix.themes.layout.container import container as container
from .components.radix.themes.layout.flex import flex as flex from .components.radix.themes.layout.flex import flex as flex
from .components.radix.themes.layout.grid import grid as grid from .components.radix.themes.layout.grid import grid as grid
from .components.radix.themes.layout.list import list_item as list_item from .components.radix.themes.layout.list import list_item as list_item
from .components.radix.themes.layout.list import list_ns as list # noqa
from .components.radix.themes.layout.list import ordered_list as ordered_list from .components.radix.themes.layout.list import ordered_list as ordered_list
from .components.radix.themes.layout.list import unordered_list as unordered_list from .components.radix.themes.layout.list import unordered_list as unordered_list
from .components.radix.themes.layout.section import section as section from .components.radix.themes.layout.section import section as section
@ -197,6 +197,7 @@ RADIX_THEMES_COMPONENTS_MAPPING: dict
RADIX_THEMES_LAYOUT_MAPPING: dict RADIX_THEMES_LAYOUT_MAPPING: dict
RADIX_THEMES_TYPOGRAPHY_MAPPING: dict RADIX_THEMES_TYPOGRAPHY_MAPPING: dict
RADIX_PRIMITIVES_MAPPING: dict RADIX_PRIMITIVES_MAPPING: dict
RADIX_PRIMITIVES_SHORTCUT_MAPPING: dict
COMPONENTS_CORE_MAPPING: dict COMPONENTS_CORE_MAPPING: dict
COMPONENTS_BASE_MAPPING: dict COMPONENTS_BASE_MAPPING: dict
RADIX_MAPPING: dict RADIX_MAPPING: dict

View File

@ -10,6 +10,7 @@ import dataclasses
import functools import functools
import inspect import inspect
import io import io
import json
import multiprocessing import multiprocessing
import os import os
import platform import platform
@ -34,7 +35,7 @@ from typing import (
from fastapi import FastAPI, HTTPException, Request, UploadFile from fastapi import FastAPI, HTTPException, Request, UploadFile
from fastapi.middleware import cors from fastapi.middleware import cors
from fastapi.responses import StreamingResponse from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from socketio import ASGIApp, AsyncNamespace, AsyncServer from socketio import ASGIApp, AsyncNamespace, AsyncServer
@ -69,7 +70,7 @@ from reflex.components.core.upload import Upload, get_upload_dir
from reflex.components.radix import themes from reflex.components.radix import themes
from reflex.config import get_config from reflex.config import get_config
from reflex.event import Event, EventHandler, EventSpec, window_alert from reflex.event import Event, EventHandler, EventSpec, window_alert
from reflex.model import Model from reflex.model import Model, get_db_status
from reflex.page import ( from reflex.page import (
DECORATED_PAGES, DECORATED_PAGES,
) )
@ -291,13 +292,12 @@ class App(MiddlewareMixin, LifespanMixin, Base):
"`connect_error_component` is deprecated, use `overlay_component` instead" "`connect_error_component` is deprecated, use `overlay_component` instead"
) )
super().__init__(**kwargs) super().__init__(**kwargs)
base_state_subclasses = BaseState.__subclasses__()
# Special case to allow test cases have multiple subclasses of rx.BaseState. # Special case to allow test cases have multiple subclasses of rx.BaseState.
if not is_testing_env() and len(base_state_subclasses) > 1: if not is_testing_env() and BaseState.__subclasses__() != [State]:
# Only one Base State class is allowed. # Only rx.State is allowed as Base State subclass.
raise ValueError( raise ValueError(
"rx.BaseState cannot be subclassed multiple times. use rx.State instead" "rx.BaseState cannot be subclassed directly. Use rx.State instead"
) )
if "breakpoints" in self.style: if "breakpoints" in self.style:
@ -399,6 +399,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
"""Add default api endpoints (ping).""" """Add default api endpoints (ping)."""
# To test the server. # To test the server.
self.api.get(str(constants.Endpoint.PING))(ping) self.api.get(str(constants.Endpoint.PING))(ping)
self.api.get(str(constants.Endpoint.HEALTH))(health)
def _add_optional_endpoints(self): def _add_optional_endpoints(self):
"""Add optional api endpoints (_upload).""" """Add optional api endpoints (_upload)."""
@ -607,7 +608,10 @@ class App(MiddlewareMixin, LifespanMixin, Base):
for route in self.pages: for route in self.pages:
replaced_route = replace_brackets_with_keywords(route) replaced_route = replace_brackets_with_keywords(route)
for rw, r, nr in zip( for rw, r, nr in zip(
replaced_route.split("/"), route.split("/"), new_route.split("/") replaced_route.split("/"),
route.split("/"),
new_route.split("/"),
strict=False,
): ):
if rw in segments and r != nr: if rw in segments and r != nr:
# If the slugs in the segments of both routes are not the same, then the route is invalid # If the slugs in the segments of both routes are not the same, then the route is invalid
@ -825,7 +829,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
for dep in deps: for dep in deps:
if dep not in state.vars and dep not in state.backend_vars: if dep not in state.vars and dep not in state.backend_vars:
raise exceptions.VarDependencyError( raise exceptions.VarDependencyError(
f"ComputedVar {var._var_name} on state {state.__name__} has an invalid dependency {dep}" f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {dep}"
) )
for substate in state.class_subclasses: for substate in state.class_subclasses:
@ -1192,6 +1196,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
FRONTEND_ARG_SPEC, FRONTEND_ARG_SPEC,
BACKEND_ARG_SPEC, BACKEND_ARG_SPEC,
], ],
strict=False,
): ):
if hasattr(handler_fn, "__name__"): if hasattr(handler_fn, "__name__"):
_fn_name = handler_fn.__name__ _fn_name = handler_fn.__name__
@ -1343,6 +1348,38 @@ async def ping() -> str:
return "pong" return "pong"
async def health() -> JSONResponse:
"""Health check endpoint to assess the status of the database and Redis services.
Returns:
JSONResponse: A JSON object with the health status:
- "status" (bool): Overall health, True if all checks pass.
- "db" (bool or str): Database status - True, False, or "NA".
- "redis" (bool or str): Redis status - True, False, or "NA".
"""
health_status = {"status": True}
status_code = 200
db_status, redis_status = await asyncio.gather(
get_db_status(), prerequisites.get_redis_status()
)
health_status["db"] = db_status
if redis_status is None:
health_status["redis"] = False
else:
health_status["redis"] = redis_status
if not health_status["db"] or (
not health_status["redis"] and redis_status is not None
):
health_status["status"] = False
status_code = 503
return JSONResponse(content=health_status, status_code=status_code)
def upload(app: App): def upload(app: App):
"""Upload a file. """Upload a file.
@ -1523,8 +1560,9 @@ class EventNamespace(AsyncNamespace):
sid: The Socket.IO session id. sid: The Socket.IO session id.
data: The event data. data: The event data.
""" """
fields = json.loads(data)
# Get the event. # Get the event.
event = Event.parse_raw(data) event = Event(**{k: v for k, v in fields.items() if k != "handler"})
self.token_to_sid[event.token] = sid self.token_to_sid[event.token] = sid
self.sid_to_token[sid] = event.token self.sid_to_token[sid] = event.token

View File

@ -47,6 +47,9 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None
# shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True) # shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True)
pydantic_main.validate_field_name = validate_field_name # type: ignore pydantic_main.validate_field_name = validate_field_name # type: ignore
if TYPE_CHECKING:
from reflex.vars import Var
class Base(BaseModel): # pyright: ignore [reportUnboundVariable] class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
"""The base class subclassed by all Reflex classes. """The base class subclassed by all Reflex classes.
@ -92,7 +95,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
return self return self
@classmethod @classmethod
def get_fields(cls) -> dict[str, Any]: def get_fields(cls) -> dict[str, ModelField]:
"""Get the fields of the object. """Get the fields of the object.
Returns: Returns:
@ -101,7 +104,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
return cls.__fields__ return cls.__fields__
@classmethod @classmethod
def add_field(cls, var: Any, default_value: Any): def add_field(cls, var: Var, default_value: Any):
"""Add a pydantic field after class definition. """Add a pydantic field after class definition.
Used by State.add_var() to correctly handle the new variable. Used by State.add_var() to correctly handle the new variable.
@ -110,7 +113,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
var: The variable to add a pydantic field for. var: The variable to add a pydantic field for.
default_value: The default value of the field default_value: The default value of the field
""" """
var_name = var._var_name.split(".")[-1] var_name = var._var_field_name
new_field = ModelField.infer( new_field = ModelField.infer(
name=var_name, name=var_name,
value=default_value, value=default_value,
@ -133,13 +136,4 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
# Seems like this function signature was wrong all along? # Seems like this function signature was wrong all along?
# If the user wants a field that we know of, get it and pass it off to _get_value # If the user wants a field that we know of, get it and pass it off to _get_value
key = getattr(self, key) key = getattr(self, key)
return self._get_value( return key
key,
to_dict=True,
by_alias=False,
include=None,
exclude=None,
exclude_unset=False,
exclude_defaults=False,
exclude_none=False,
)

View File

@ -18,13 +18,12 @@ from reflex.components.component import (
StatefulComponent, StatefulComponent,
) )
from reflex.config import get_config from reflex.config import get_config
from reflex.ivars.base import LiteralVar
from reflex.state import BaseState from reflex.state import BaseState
from reflex.style import SYSTEM_COLOR_MODE from reflex.style import SYSTEM_COLOR_MODE
from reflex.utils.exec import is_prod_mode from reflex.utils.exec import is_prod_mode
from reflex.utils.imports import ImportVar from reflex.utils.imports import ImportVar
from reflex.utils.prerequisites import get_web_dir from reflex.utils.prerequisites import get_web_dir
from reflex.vars import Var from reflex.vars.base import LiteralVar, Var
def _compile_document_root(root: Component) -> str: def _compile_document_root(root: Component) -> str:
@ -59,7 +58,7 @@ def _compile_app(app_root: Component) -> str:
) )
def _compile_theme(theme: dict) -> str: def _compile_theme(theme: str) -> str:
"""Compile the theme. """Compile the theme.
Args: Args:
@ -379,7 +378,7 @@ def compile_theme(style: ComponentStyle) -> tuple[str, str]:
theme = utils.create_theme(style) theme = utils.create_theme(style)
# Compile the theme. # Compile the theme.
code = _compile_theme(theme) code = _compile_theme(str(LiteralVar.create(theme)))
return output_path, code return output_path, code

View File

@ -8,6 +8,7 @@ from typing import Any, Callable, Dict, Optional, Type, Union
from urllib.parse import urlparse from urllib.parse import urlparse
from reflex.utils.prerequisites import get_web_dir from reflex.utils.prerequisites import get_web_dir
from reflex.vars.base import Var
try: try:
from pydantic.v1.fields import ModelField from pydantic.v1.fields import ModelField
@ -32,7 +33,6 @@ from reflex.state import BaseState, Cookie, LocalStorage, SessionStorage
from reflex.style import Style from reflex.style import Style
from reflex.utils import console, format, imports, path_ops from reflex.utils import console, format, imports, path_ops
from reflex.utils.imports import ImportVar, ParsedImportDict from reflex.utils.imports import ImportVar, ParsedImportDict
from reflex.vars import Var
# To re-export this function. # To re-export this function.
merge_imports = imports.merge_imports merge_imports = imports.merge_imports
@ -152,7 +152,9 @@ def compile_state(state: Type[BaseState]) -> dict:
console.warn( console.warn(
f"Failed to compile initial state with computed vars, excluding them: {e}" f"Failed to compile initial state with computed vars, excluding them: {e}"
) )
initial_state = state(_reflex_internal_init=True).dict(include_computed=False) initial_state = state(_reflex_internal_init=True).dict(
initial=True, include_computed=False
)
return format.format_state(initial_state) return format.format_state(initial_state)
@ -266,7 +268,7 @@ def compile_custom_component(
} }
# Concatenate the props. # Concatenate the props.
props = [prop._var_name for prop in component.get_prop_vars()] props = [prop._js_expr for prop in component.get_prop_vars()]
# Compile the component. # Compile the component.
return ( return (

View File

@ -2,7 +2,7 @@
from reflex.components.base.fragment import Fragment from reflex.components.base.fragment import Fragment
from reflex.components.component import Component from reflex.components.component import Component
from reflex.ivars.base import ImmutableVar from reflex.vars.base import Var
class AppWrap(Fragment): class AppWrap(Fragment):
@ -15,4 +15,4 @@ class AppWrap(Fragment):
Returns: Returns:
A new AppWrap component containing {children}. A new AppWrap component containing {children}.
""" """
return super().create(ImmutableVar.create("children")) return super().create(Var(_js_expr="children"))

View File

@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
from reflex.components.base.fragment import Fragment from reflex.components.base.fragment import Fragment
from reflex.event import EventHandler, EventSpec from reflex.event import EventHandler, EventSpec
from reflex.style import Style from reflex.style import Style
from reflex.vars import Var from reflex.vars.base import Var
class AppWrap(Fragment): class AppWrap(Fragment):
@overload @overload

View File

@ -7,8 +7,7 @@ from typing import Any, Iterator
from reflex.components.component import Component from reflex.components.component import Component
from reflex.components.tags import Tag from reflex.components.tags import Tag
from reflex.components.tags.tagless import Tagless from reflex.components.tags.tagless import Tagless
from reflex.ivars.base import ImmutableVar from reflex.vars.base import Var
from reflex.vars import Var
class Bare(Component): class Bare(Component):
@ -26,16 +25,14 @@ class Bare(Component):
Returns: Returns:
The component. The component.
""" """
if isinstance(contents, ImmutableVar): if isinstance(contents, Var):
return cls(contents=contents) return cls(contents=contents)
if isinstance(contents, Var) and contents._get_all_var_data():
contents = contents.to(str)
else: else:
contents = str(contents) if contents is not None else "" contents = str(contents) if contents is not None else ""
return cls(contents=contents) # type: ignore return cls(contents=contents) # type: ignore
def _render(self) -> Tag: def _render(self) -> Tag:
if isinstance(self.contents, ImmutableVar): if isinstance(self.contents, Var):
return Tagless(contents=f"{{{str(self.contents)}}}") return Tagless(contents=f"{{{str(self.contents)}}}")
return Tagless(contents=str(self.contents)) return Tagless(contents=str(self.contents))

View File

@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
from reflex.components.component import Component from reflex.components.component import Component
from reflex.event import EventHandler, EventSpec from reflex.event import EventHandler, EventSpec
from reflex.style import Style from reflex.style import Style
from reflex.vars import Var from reflex.vars.base import Var
class Body(Component): class Body(Component):
@overload @overload

View File

@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
from reflex.components.component import Component from reflex.components.component import Component
from reflex.event import EventHandler, EventSpec from reflex.event import EventHandler, EventSpec
from reflex.style import Style from reflex.style import Style
from reflex.vars import Var from reflex.vars.base import Var
class NextDocumentLib(Component): class NextDocumentLib(Component):
@overload @overload

Some files were not shown because too many files have changed in this diff Show More