merging
This commit is contained in:
commit
825463c494
@ -11,7 +11,7 @@ omit =
|
||||
[report]
|
||||
show_missing = true
|
||||
# TODO bump back to 79
|
||||
fail_under = 60
|
||||
fail_under = 70
|
||||
precision = 2
|
||||
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
14
.github/workflows/benchmarks.yml
vendored
14
.github/workflows/benchmarks.yml
vendored
@ -81,19 +81,11 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
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:
|
||||
- os: windows-latest
|
||||
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
|
||||
- os: macos-latest
|
||||
python-version: '3.8.18'
|
||||
- os: macos-latest
|
||||
python-version: '3.9.18'
|
||||
- os: macos-latest
|
||||
python-version: '3.10.13'
|
||||
- os: macos-12
|
||||
@ -101,10 +93,6 @@ jobs:
|
||||
include:
|
||||
- os: windows-latest
|
||||
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 }}
|
||||
steps:
|
||||
|
@ -23,7 +23,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
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
|
||||
services:
|
||||
# Label used to access the service container
|
||||
|
11
.github/workflows/integration_tests.yml
vendored
11
.github/workflows/integration_tests.yml
vendored
@ -43,21 +43,14 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
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:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
- os: windows-latest
|
||||
python-version: '3.8.18'
|
||||
include:
|
||||
- os: windows-latest
|
||||
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 }}
|
||||
steps:
|
||||
|
2
.github/workflows/integration_tests_wsl.yml
vendored
2
.github/workflows/integration_tests_wsl.yml
vendored
@ -37,6 +37,8 @@ jobs:
|
||||
path: reflex-examples
|
||||
|
||||
- uses: Vampire/setup-wsl@v3
|
||||
with:
|
||||
distribution: Ubuntu-24.04
|
||||
|
||||
- name: Install Python
|
||||
shell: wsl-bash {0}
|
||||
|
10
.github/workflows/unit_tests.yml
vendored
10
.github/workflows/unit_tests.yml
vendored
@ -28,22 +28,14 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
- os: windows-latest
|
||||
python-version: '3.8.18'
|
||||
include:
|
||||
- os: windows-latest
|
||||
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 }}
|
||||
# Service containers to run with `runner-job`
|
||||
services:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
||||
assets/external/*
|
||||
dist/*
|
||||
examples/
|
||||
.web
|
||||
.idea
|
||||
.vscode
|
||||
.coverage
|
||||
|
@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib
|
||||
|
||||
**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).
|
||||
|
||||
**1. Fork this repository:**
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -35,7 +34,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
Open a terminal and run (Requires Python 3.8+):
|
||||
Open a terminal and run (Requires Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -130,7 +130,6 @@ def render_multiple_pages(app, num: int):
|
||||
|
||||
def AppWithOnePage():
|
||||
"""A reflex app with one page."""
|
||||
import reflex_chakra as rc
|
||||
from rxconfig import config # type: ignore
|
||||
|
||||
import reflex as rx
|
||||
@ -145,7 +144,7 @@ def AppWithOnePage():
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.center(
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="token", value=State.router.session.client_token, is_read_only=True
|
||||
),
|
||||
rx.vstack(
|
||||
|
@ -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
|
||||
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.
|
||||
## `simple-two-port`
|
||||
|
||||
## 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
|
||||
the Reflex frontend and backend together, exposing ports 3000 and 8000.
|
||||
## `simple-one-port`
|
||||
|
||||
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
|
||||
docker build -t reflex-app:latest .
|
||||
```
|
||||
## `production-compose`
|
||||
|
||||
## 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
|
||||
docker run -it --rm -p 3000:3000 -p 8000:8000 --name app reflex-app:latest
|
||||
```
|
||||
|
||||
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.
|
||||
This example deployment is intended for use with App hosting platforms, like
|
||||
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.
|
5
docker-example/production-app-platform/.dockerignore
Normal file
5
docker-example/production-app-platform/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
65
docker-example/production-app-platform/Dockerfile
Normal file
65
docker-example/production-app-platform/Dockerfile
Normal 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}
|
113
docker-example/production-app-platform/README.md
Normal file
113
docker-example/production-app-platform/README.md
Normal 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
|
@ -42,10 +42,11 @@ 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"
|
||||
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 reflex db migrate && reflex run --env prod --backend-only
|
||||
CMD [ -d alembic ] && reflex db migrate; \
|
||||
exec reflex run --env prod --backend-only
|
75
docker-example/production-compose/README.md
Normal file
75
docker-example/production-compose/README.md
Normal 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
|
||||
```
|
@ -12,7 +12,6 @@ services:
|
||||
DB_URL: sqlite:///data/reflex.db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: prod.Dockerfile
|
||||
volumes:
|
||||
- db-data:/app/data
|
||||
- upload-data:/app/uploaded_files
|
@ -1 +0,0 @@
|
||||
reflex
|
5
docker-example/simple-one-port/.dockerignore
Normal file
5
docker-example/simple-one-port/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
14
docker-example/simple-one-port/Caddyfile
Normal file
14
docker-example/simple-one-port/Caddyfile
Normal 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
|
||||
}
|
@ -10,31 +10,13 @@ FROM python:3.11
|
||||
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.
|
||||
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
|
||||
RUN apt-get update -y && apt-get install -y caddy && rm -rf /var/lib/apt/lists/*
|
||||
# Install Caddy and redis server inside image
|
||||
RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
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 . .
|
||||
|
||||
@ -54,4 +36,6 @@ EXPOSE $PORT
|
||||
|
||||
# Apply migrations before starting the backend.
|
||||
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
|
36
docker-example/simple-one-port/README.md
Normal file
36
docker-example/simple-one-port/README.md
Normal 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.
|
5
docker-example/simple-two-port/.dockerignore
Normal file
5
docker-example/simple-two-port/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
@ -1,5 +1,8 @@
|
||||
# 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)
|
||||
WORKDIR /app
|
||||
@ -18,4 +21,6 @@ RUN reflex export --frontend-only --no-zip
|
||||
STOPSIGNAL SIGKILL
|
||||
|
||||
# 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
|
44
docker-example/simple-two-port/README.md
Normal file
44
docker-example/simple-two-port/README.md
Normal 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
|
||||
```
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -35,7 +34,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
|
||||
|
||||
## ⚙️ 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
|
||||
pip install reflex
|
||||
|
@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
|
||||
|
||||
## ⚙️ Instalación
|
||||
|
||||
Abra un terminal y ejecute (Requiere Python 3.8+):
|
||||
Abra un terminal y ejecute (Requiere Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,7 +11,6 @@ Pynecone की तलाश हैं? आप सही रेपो में
|
||||
### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -36,7 +35,7 @@ Reflex के अंदर के कामकाज को जानने क
|
||||
|
||||
## ⚙️ इंस्टॉलेशन (Installation)
|
||||
|
||||
एक टर्मिनल खोलें और चलाएं (Python 3.8+ की आवश्यकता है):
|
||||
एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -23,7 +22,7 @@
|
||||
|
||||
## ⚙️ Installazione
|
||||
|
||||
Apri un terminale ed esegui (Richiede Python 3.8+):
|
||||
Apri un terminale ed esegui (Richiede Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,7 +11,6 @@
|
||||
### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -21,7 +20,7 @@
|
||||
---
|
||||
## ⚙️ 설치
|
||||
|
||||
터미널을 열고 실행하세요. (Python 3.8+ 필요):
|
||||
터미널을 열고 실행하세요. (Python 3.10+ 필요):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) میشود. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -35,7 +34,7 @@
|
||||
|
||||
## ⚙️ Installation - نصب و راه اندازی
|
||||
|
||||
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.8+):
|
||||
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -21,7 +21,7 @@
|
||||
---
|
||||
## ⚙️ Instalação
|
||||
|
||||
Abra um terminal e execute (Requer Python 3.8+):
|
||||
Abra um terminal e execute (Requer Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,7 +11,6 @@
|
||||
### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -25,7 +24,7 @@
|
||||
|
||||
## ⚙️ 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
|
||||
pip install reflex
|
||||
|
@ -10,7 +10,6 @@
|
||||
|
||||
### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
|
@ -11,7 +11,6 @@
|
||||
**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -37,7 +36,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
|
||||
|
||||
## ⚙️ 安裝
|
||||
|
||||
開啟一個終端機並且執行 (需要 Python 3.8+):
|
||||
開啟一個終端機並且執行 (需要 Python 3.10+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.8
|
||||
FROM python:3.10
|
||||
|
||||
ARG USERNAME=kerrigan
|
||||
RUN useradd -m $USERNAME
|
||||
|
@ -13,7 +13,6 @@ def BackgroundTask():
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
from reflex.state import ImmutableStateError
|
||||
@ -87,6 +86,13 @@ def BackgroundTask():
|
||||
third_state = await self.get_state(ThirdState)
|
||||
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):
|
||||
@rx.background
|
||||
async def get_other_state(self):
|
||||
@ -109,11 +115,11 @@ def BackgroundTask():
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.vstack(
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="token", value=State.router.session.client_token, is_read_only=True
|
||||
),
|
||||
rx.heading(State.counter, id="counter"),
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="iterations",
|
||||
placeholder="Iterations",
|
||||
value=State.iterations.to_string(), # type: ignore
|
||||
@ -155,6 +161,11 @@ def BackgroundTask():
|
||||
on_click=OtherState.get_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"),
|
||||
)
|
||||
|
||||
@ -334,3 +345,30 @@ def test_get_state(
|
||||
|
||||
increment_button.click()
|
||||
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)
|
||||
|
@ -17,8 +17,6 @@ from . import utils
|
||||
|
||||
def ClientSide():
|
||||
"""App for testing client-side state."""
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class ClientSideState(rx.State):
|
||||
@ -72,18 +70,18 @@ def ClientSide():
|
||||
|
||||
def index():
|
||||
return rx.fragment(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=ClientSideState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rc.input(
|
||||
rx.input(
|
||||
placeholder="state var",
|
||||
value=ClientSideState.state_var,
|
||||
on_change=ClientSideState.set_state_var, # type: ignore
|
||||
id="state_var",
|
||||
),
|
||||
rc.input(
|
||||
rx.input(
|
||||
placeholder="input value",
|
||||
value=ClientSideState.input_value,
|
||||
on_change=ClientSideState.set_input_value, # type: ignore
|
||||
@ -313,7 +311,6 @@ async def test_client_side_state(
|
||||
# no cookies should be set yet!
|
||||
assert not driver.get_cookies()
|
||||
local_storage_items = local_storage.items()
|
||||
local_storage_items.pop("chakra-ui-color-mode", None)
|
||||
local_storage_items.pop("last_compiled_time", None)
|
||||
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)
|
||||
|
||||
local_storage_items = local_storage.items()
|
||||
local_storage_items.pop("chakra-ui-color-mode", 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}.l2") == "l2 value"
|
||||
|
@ -17,13 +17,10 @@ def DynamicRoute():
|
||||
"""App for testing dynamic routes."""
|
||||
from typing import List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class DynamicState(rx.State):
|
||||
order: List[str] = []
|
||||
page_id: str = ""
|
||||
|
||||
def on_load(self):
|
||||
self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
|
||||
@ -42,13 +39,13 @@ def DynamicRoute():
|
||||
|
||||
def index():
|
||||
return rx.fragment(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=DynamicState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rc.input(value=DynamicState.page_id, is_read_only=True, id="page_id"),
|
||||
rc.input(
|
||||
rx.input(value=rx.State.page_id, is_read_only=True, id="page_id"), # type: ignore
|
||||
rx.input(
|
||||
value=DynamicState.router.page.raw_path,
|
||||
is_read_only=True,
|
||||
id="raw_path",
|
||||
@ -61,22 +58,80 @@ def DynamicRoute():
|
||||
id="link_page_next", # type: ignore
|
||||
),
|
||||
rx.link("missing", href="/missing", id="link_missing"),
|
||||
rc.list(
|
||||
rx.list( # type: ignore
|
||||
rx.foreach(
|
||||
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
|
||||
def redirect_page():
|
||||
return rx.fragment(rx.text("redirecting..."))
|
||||
|
||||
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="/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
|
||||
|
||||
|
||||
@ -305,3 +360,50 @@ async def test_on_load_navigate_non_dynamic(
|
||||
link.click()
|
||||
assert urlsplit(driver.current_url).path == "/static/x/"
|
||||
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")
|
||||
|
@ -16,8 +16,6 @@ def TestEventAction():
|
||||
"""App for testing event_actions."""
|
||||
from typing import List, Optional
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class EventActionState(rx.State):
|
||||
@ -55,7 +53,7 @@ def TestEventAction():
|
||||
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=EventActionState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
@ -148,10 +146,10 @@ def TestEventAction():
|
||||
200
|
||||
).stop_propagation,
|
||||
),
|
||||
rc.list(
|
||||
rx.list( # type: ignore
|
||||
rx.foreach(
|
||||
EventActionState.order, # type: ignore
|
||||
rc.list_item,
|
||||
rx.list_item,
|
||||
),
|
||||
),
|
||||
on_click=EventActionState.on_click("outer"), # type: ignore
|
||||
|
@ -18,8 +18,6 @@ def EventChain():
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
# 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)
|
||||
|
||||
token_input = rc.input(
|
||||
token_input = rx.input(
|
||||
value=State.router.session.client_token, is_read_only=True, id="token"
|
||||
)
|
||||
|
||||
@ -137,7 +135,7 @@ def EventChain():
|
||||
def index():
|
||||
return rx.fragment(
|
||||
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(
|
||||
"Return Event",
|
||||
id="return_event",
|
||||
|
@ -20,8 +20,6 @@ def FormSubmit(form_component):
|
||||
"""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class FormState(rx.State):
|
||||
@ -37,28 +35,29 @@ def FormSubmit(form_component):
|
||||
@app.add_page
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=FormState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
eval(form_component)(
|
||||
rx.vstack(
|
||||
rc.input(id="name_input"),
|
||||
rx.hstack(rc.pin_input(length=4, id="pin_input")),
|
||||
rc.number_input(id="number_input"),
|
||||
rx.input(id="name_input"),
|
||||
rx.checkbox(id="bool_input"),
|
||||
rx.switch(id="bool_input2"),
|
||||
rx.checkbox(id="bool_input3"),
|
||||
rx.switch(id="bool_input4"),
|
||||
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(FormState.var_options, id="radio_input_var"),
|
||||
rc.select(["option1", "option2"], id="select_input"),
|
||||
rc.select(FormState.var_options, id="select_input_var"),
|
||||
rx.select(
|
||||
["option1", "option2"],
|
||||
name="select_input",
|
||||
default_value="option1",
|
||||
),
|
||||
rx.select(FormState.var_options, id="select_input_var"),
|
||||
rx.text_area(id="text_area_input"),
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="debounce_input",
|
||||
debounce_timeout=0,
|
||||
on_change=rx.console_log,
|
||||
@ -81,8 +80,6 @@ def FormSubmitName(form_component):
|
||||
"""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class FormState(rx.State):
|
||||
@ -98,22 +95,19 @@ def FormSubmitName(form_component):
|
||||
@app.add_page
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=FormState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
eval(form_component)(
|
||||
rx.vstack(
|
||||
rc.input(name="name_input"),
|
||||
rx.hstack(rc.pin_input(length=4, name="pin_input")),
|
||||
rc.number_input(name="number_input"),
|
||||
rx.input(name="name_input"),
|
||||
rx.checkbox(name="bool_input"),
|
||||
rx.switch(name="bool_input2"),
|
||||
rx.checkbox(name="bool_input3"),
|
||||
rx.switch(name="bool_input4"),
|
||||
rx.slider(name="slider_input", default_value=[50], width="100%"),
|
||||
rc.range_slider(name="range_input"),
|
||||
rx.radio(FormState.options, name="radio_input"),
|
||||
rx.select(
|
||||
FormState.options,
|
||||
@ -121,21 +115,13 @@ def FormSubmitName(form_component):
|
||||
default_value=FormState.options[0],
|
||||
),
|
||||
rx.text_area(name="text_area_input"),
|
||||
rc.input_group(
|
||||
rc.input_left_element(rx.icon(tag="chevron_right")),
|
||||
rc.input(
|
||||
rx.input(
|
||||
name="debounce_input",
|
||||
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,
|
||||
),
|
||||
),
|
||||
on_submit=FormState.form_submit,
|
||||
custom_attrs={"action": "/invalid"},
|
||||
@ -152,16 +138,12 @@ def FormSubmitName(form_component):
|
||||
functools.partial(FormSubmitName, form_component="rx.form.root"),
|
||||
functools.partial(FormSubmit, 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=[
|
||||
"id-radix",
|
||||
"name-radix",
|
||||
"id-html",
|
||||
"name-html",
|
||||
"id-chakra",
|
||||
"name-chakra",
|
||||
],
|
||||
)
|
||||
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.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.click()
|
||||
|
||||
@ -275,15 +247,12 @@ async def test_submit(driver, form_submit: AppHarness):
|
||||
print(form_data)
|
||||
|
||||
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_input2"]
|
||||
assert not form_data.get("bool_input3", False)
|
||||
assert not form_data.get("bool_input4", False)
|
||||
|
||||
assert form_data["slider_input"] == "50"
|
||||
assert form_data["range_input"] == ["25", "75"]
|
||||
assert form_data["radio_input"] == "option2"
|
||||
assert form_data["select_input"] == "option1"
|
||||
assert form_data["text_area_input"] == "Some\nText"
|
||||
|
@ -11,8 +11,6 @@ from reflex.testing import AppHarness
|
||||
|
||||
def ServerSideEvent():
|
||||
"""App with inputs set via event handlers and set_value."""
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class SSState(rx.State):
|
||||
@ -41,12 +39,12 @@ def ServerSideEvent():
|
||||
@app.add_page
|
||||
def index():
|
||||
return rx.fragment(
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="token", value=SSState.router.session.client_token, is_read_only=True
|
||||
),
|
||||
rc.input(default_value="a", id="a"),
|
||||
rc.input(default_value="b", id="b"),
|
||||
rc.input(default_value="c", id="c"),
|
||||
rx.input(default_value="a", id="a"),
|
||||
rx.input(default_value="b", id="b"),
|
||||
rx.input(default_value="c", id="c"),
|
||||
rx.button(
|
||||
"Clear Immediate",
|
||||
id="clear_immediate",
|
||||
|
@ -10,89 +10,45 @@ from reflex.testing import AppHarness
|
||||
|
||||
def Table():
|
||||
"""App using table component."""
|
||||
from typing import List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
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.add_page
|
||||
def index():
|
||||
return rx.center(
|
||||
rc.input(
|
||||
rx.input(
|
||||
id="token",
|
||||
value=TableState.router.session.client_token,
|
||||
value=rx.State.router.session.client_token,
|
||||
is_read_only=True,
|
||||
),
|
||||
rc.table_container(
|
||||
rc.table(
|
||||
headers=TableState.headers,
|
||||
rows=TableState.rows,
|
||||
footers=TableState.footers,
|
||||
caption=TableState.caption,
|
||||
variant="striped",
|
||||
color_scheme="blue",
|
||||
rx.table.root(
|
||||
rx.table.header(
|
||||
rx.table.row(
|
||||
rx.table.column_header_cell("Name"),
|
||||
rx.table.column_header_cell("Age"),
|
||||
rx.table.column_header_cell("Location"),
|
||||
),
|
||||
),
|
||||
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",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -138,23 +94,20 @@ def driver(table: AppHarness):
|
||||
driver.quit()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("route", ["", "/another"])
|
||||
def test_table(driver, table: AppHarness, route):
|
||||
def test_table(driver, table: AppHarness):
|
||||
"""Test that a table component is rendered properly.
|
||||
|
||||
Args:
|
||||
driver: Selenium WebDriver open to the 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"
|
||||
|
||||
thead = driver.find_element(By.TAG_NAME, "thead")
|
||||
# poll till page is fully loaded.
|
||||
table.poll_for_content(element=thead)
|
||||
# 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
|
||||
assert (
|
||||
driver.find_element(By.TAG_NAME, "tbody")
|
||||
@ -162,12 +115,3 @@ def test_table(driver, table: AppHarness, route):
|
||||
.text
|
||||
== "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"
|
||||
|
@ -27,8 +27,6 @@ def TailwindApp(
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class UnusedState(rx.State):
|
||||
@ -36,7 +34,7 @@ def TailwindApp(
|
||||
|
||||
def index():
|
||||
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.text(paragraph_text, as_="p", class_name=paragraph_class_name),
|
||||
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
|
||||
for p in paragraphs:
|
||||
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:
|
||||
# expect default color, not "text-red-500" from tailwind utility class
|
||||
assert p.value_of_css_property("color") not in TEXT_RED_500_COLOR
|
||||
|
@ -16,8 +16,6 @@ def UploadFile():
|
||||
"""App for testing dynamic routes."""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class UploadState(rx.State):
|
||||
@ -46,7 +44,7 @@ def UploadFile():
|
||||
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rc.input(
|
||||
rx.input(
|
||||
value=UploadState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
|
@ -14,11 +14,12 @@ def VarOperations():
|
||||
"""App with var operations."""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
from reflex.ivars.base import LiteralVar
|
||||
from reflex.ivars.sequence import ArrayVar
|
||||
from reflex.vars.base import LiteralVar
|
||||
from reflex.vars.sequence import ArrayVar
|
||||
|
||||
class Object(rx.Base):
|
||||
str: str = "hello"
|
||||
|
||||
class VarOperationState(rx.State):
|
||||
int_var1: int = 10
|
||||
@ -29,6 +30,7 @@ def VarOperations():
|
||||
list1: List = [1, 2]
|
||||
list2: List = [3, 4]
|
||||
list3: List = ["first", "second", "third"]
|
||||
list4: List = [Object(name="obj_1"), Object(name="obj_2")]
|
||||
str_var1: str = "first"
|
||||
str_var2: str = "second"
|
||||
str_var3: str = "ThIrD"
|
||||
@ -474,6 +476,7 @@ def VarOperations():
|
||||
rx.text(
|
||||
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"),
|
||||
# LIST, INT
|
||||
rx.text(
|
||||
@ -547,10 +550,7 @@ def VarOperations():
|
||||
VarOperationState.html_str,
|
||||
id="html_str",
|
||||
),
|
||||
rc.highlight(
|
||||
"second",
|
||||
query=[VarOperationState.str_var2],
|
||||
),
|
||||
rx.el.mark("second"),
|
||||
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(5, 0, -1).join(","), id="list_join_range3"),
|
||||
@ -588,6 +588,16 @@ def VarOperations():
|
||||
int_var2=VarOperationState.int_var2,
|
||||
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_or_list", "[1,2]"),
|
||||
("list_contains", "true"),
|
||||
("list_pluck", '["obj_1","obj_2"]'),
|
||||
("list_reverse", "[2,1]"),
|
||||
("list_join", "firstsecondthird"),
|
||||
("list_join_comma", "first,second,third"),
|
||||
@ -784,6 +795,8 @@ def test_var_operations(driver, var_operations: AppHarness):
|
||||
# rx.memo component with state
|
||||
("memo_comp", "1210"),
|
||||
("memo_comp_nested", "345"),
|
||||
# foreach in a match
|
||||
("foreach_in_match", "first\nsecond\nthird"),
|
||||
]
|
||||
|
||||
for tag, expected in tests:
|
||||
|
1429
poetry.lock
generated
1429
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -26,10 +26,10 @@ packages = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
python = "^3.10"
|
||||
dill = ">=0.3.8,<0.4"
|
||||
fastapi = ">=0.96.0,<0.111.0"
|
||||
gunicorn = ">=20.1.0,<23.0"
|
||||
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
|
||||
gunicorn = ">=20.1.0,<24.0"
|
||||
jinja2 = ">=3.1.2,<4.0"
|
||||
psutil = ">=5.9.4,<7.0"
|
||||
pydantic = ">=1.10.2,<3.0"
|
||||
@ -40,8 +40,6 @@ rich = ">=13.0.0,<14.0"
|
||||
sqlmodel = ">=0.0.14,<0.1"
|
||||
typer = ">=0.4.2,<1.0"
|
||||
uvicorn = ">=0.20.0"
|
||||
watchdog = ">=2.3.1,<5.0"
|
||||
watchfiles = ">=0.19.0,<1.0"
|
||||
starlette-admin = ">=0.11.0,<1.0"
|
||||
alembic = ">=1.11.1,<2.0"
|
||||
platformdirs = ">=3.10.0,<5.0"
|
||||
@ -61,7 +59,7 @@ httpx = ">=0.25.1,<1.0"
|
||||
twine = ">=4.0.0,<6.0"
|
||||
tomlkit = ">=0.12.4,<1.0"
|
||||
lazy_loader = ">=0.4"
|
||||
reflex-chakra = ">=0.6.0a"
|
||||
reflex-chakra = ">=0.6.0a6"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
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-cov = ">=4.0.0,<5.0"
|
||||
ruff = "^0.4.9"
|
||||
pandas = [
|
||||
{version = ">=2.1.1,<3.0", python = ">=3.9,<3.13"},
|
||||
{version = ">=1.5.3,<2.0", python = ">=3.8,<3.9"},
|
||||
]
|
||||
pillow = [
|
||||
{version = ">=10.0.0,<11.0", python = ">=3.8,<4.0"}
|
||||
]
|
||||
pandas = ">=2.1.1,<3.0"
|
||||
pillow = ">=10.0.0,<11.0"
|
||||
plotly = ">=5.13.0,<6.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"
|
||||
pytest-benchmark = ">=4.0.0,<5.0"
|
||||
|
||||
@ -95,7 +88,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
[tool.pyright]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py38"
|
||||
target-version = "py310"
|
||||
lint.select = ["B", "D", "E", "F", "I", "SIM", "W"]
|
||||
lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"]
|
||||
lint.pydocstyle.convention = "google"
|
||||
|
4
reflex/.templates/apps/demo/.gitignore
vendored
4
reflex/.templates/apps/demo/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
*.db
|
||||
*.py[cod]
|
||||
.web
|
||||
__pycache__/
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@ -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 |
@ -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 > 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 |
@ -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 |
@ -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 |
@ -1 +0,0 @@
|
||||
"""Base template for Reflex."""
|
@ -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)
|
@ -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
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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",
|
||||
)
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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
|
@ -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}")
|
@ -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
|
@ -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",
|
||||
},
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
from .loading_icon import loading_icon
|
||||
from .modal import modal
|
||||
from .navbar import navbar
|
||||
from .sidebar import sidebar
|
@ -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%",
|
||||
)
|
@ -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
|
@ -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,
|
||||
)
|
@ -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",
|
||||
)
|
@ -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,
|
||||
)
|
@ -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
|
@ -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",
|
||||
},
|
||||
}
|
@ -8,11 +8,11 @@ version = "0.0.1"
|
||||
description = "Reflex custom component {{ module_name }}"
|
||||
readme = "README.md"
|
||||
license = { text = "Apache-2.0" }
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
|
||||
keywords = ["reflex","reflex-custom-components"]
|
||||
|
||||
dependencies = ["reflex>=0.4.2"]
|
||||
dependencies = ["reflex>={{ reflex_version }}"]
|
||||
|
||||
classifiers = ["Development Status :: 4 - Beta"]
|
||||
|
||||
|
@ -7,6 +7,10 @@ import '/styles/styles.css'
|
||||
{% block declaration %}
|
||||
import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
|
||||
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 %}
|
||||
{{custom_code}}
|
||||
@ -26,8 +30,18 @@ function AppWrap({children}) {
|
||||
}
|
||||
|
||||
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 (
|
||||
<ThemeProvider defaultTheme={ defaultColorMode } storageKey="chakra-ui-color-mode" attribute="class">
|
||||
<ThemeProvider defaultTheme={ defaultColorMode } attribute="class">
|
||||
<AppWrap>
|
||||
<StateProvider>
|
||||
<EventLoopProvider>
|
||||
|
@ -64,11 +64,11 @@
|
||||
{# Args: #}
|
||||
{# component: component dictionary #}
|
||||
{% 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 %}
|
||||
{{ render(child) }}
|
||||
{% endfor %}
|
||||
))}
|
||||
))}</>
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
@ -85,10 +85,10 @@
|
||||
{% 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 condition in case[:-1] %}
|
||||
case JSON.stringify({{ condition._var_name_unwrapped }}):
|
||||
case JSON.stringify({{ condition._js_expr }}):
|
||||
{% endfor %}
|
||||
return {{ case[-1] }};
|
||||
break;
|
||||
|
@ -1 +1 @@
|
||||
export default {{ theme|json_dumps }}
|
||||
export default {{ theme }}
|
@ -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>
|
||||
);
|
||||
}
|
@ -15,6 +15,7 @@ import {
|
||||
} from "utils/context.js";
|
||||
import debounce from "/utils/helpers/debounce";
|
||||
import throttle from "/utils/helpers/throttle";
|
||||
import * as Babel from "@babel/standalone";
|
||||
|
||||
// Endpoint URLs.
|
||||
const EVENTURL = env.EVENT;
|
||||
@ -117,8 +118,8 @@ export const isStateful = () => {
|
||||
if (event_queue.length === 0) {
|
||||
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.
|
||||
@ -129,6 +130,22 @@ export const applyDelta = (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.
|
||||
* @param event The event to queue.
|
||||
@ -141,7 +158,7 @@ export const queueEventIfSocketExists = async (events, socket) => {
|
||||
return;
|
||||
}
|
||||
await queueEvents(events, socket);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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");
|
||||
a.hidden = true;
|
||||
// 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.click();
|
||||
a.remove();
|
||||
@ -249,7 +269,7 @@ export const applyEvent = async (event, socket) => {
|
||||
} catch (e) {
|
||||
console.log("_call_script", e);
|
||||
if (window && window?.onerror) {
|
||||
window.onerror(e.message, null, null, null, e)
|
||||
window.onerror(e.message, null, null, null, e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@ -290,10 +310,9 @@ export const applyEvent = async (event, socket) => {
|
||||
export const applyRestEvent = async (event, socket) => {
|
||||
let eventSent = false;
|
||||
if (event.handler === "uploadFiles") {
|
||||
|
||||
if (event.payload.files === undefined || event.payload.files.length === 0) {
|
||||
// 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.
|
||||
@ -397,7 +416,7 @@ export const connect = async (
|
||||
console.log("Disconnect backend before bfcache on navigation");
|
||||
socket.current.disconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Once the socket is open, hydrate the page.
|
||||
socket.current.on("connect", () => {
|
||||
@ -416,7 +435,7 @@ export const connect = async (
|
||||
});
|
||||
|
||||
// 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);
|
||||
for (const substate in update.delta) {
|
||||
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 {};
|
||||
@ -614,15 +637,17 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
||||
) {
|
||||
const options = client_storage.local_storage[state_key];
|
||||
localStorage.setItem(options.name || state_key, delta[substate][key]);
|
||||
} else if(
|
||||
} else if (
|
||||
client_storage.session_storage &&
|
||||
state_key in client_storage.session_storage &&
|
||||
typeof window !== "undefined"
|
||||
) {
|
||||
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)) {
|
||||
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) {
|
||||
_e.preventDefault();
|
||||
@ -671,7 +696,7 @@ export const useEventLoop = (
|
||||
debounce(
|
||||
combined_name,
|
||||
() => queueEvents(events, socket),
|
||||
event_actions.debounce,
|
||||
event_actions.debounce
|
||||
);
|
||||
} else {
|
||||
queueEvents(events, socket);
|
||||
@ -698,28 +723,30 @@ export const useEventLoop = (
|
||||
|
||||
// Handle frontend errors and send them to the backend via websocket.
|
||||
useEffect(() => {
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
addEvents([Event(`${exception_state_name}.handle_frontend_exception`, {
|
||||
addEvents([
|
||||
Event(`${exception_state_name}.handle_frontend_exception`, {
|
||||
stack: error.stack,
|
||||
})])
|
||||
}),
|
||||
]);
|
||||
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`, {
|
||||
addEvents([
|
||||
Event(`${exception_state_name}.handle_frontend_exception`, {
|
||||
stack: event.reason.stack,
|
||||
})])
|
||||
}),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
},[])
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Main event loop.
|
||||
useEffect(() => {
|
||||
@ -782,11 +809,11 @@ export const useEventLoop = (
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_start = () => {
|
||||
const main_state_dispatch = dispatch["reflex___state____state"]
|
||||
const main_state_dispatch = dispatch["reflex___state____state"];
|
||||
if (main_state_dispatch !== undefined) {
|
||||
main_state_dispatch({ is_hydrated: false })
|
||||
}
|
||||
main_state_dispatch({ is_hydrated: false });
|
||||
}
|
||||
};
|
||||
const change_complete = () => addEvents(onLoadInternalEvent());
|
||||
router.events.on("routeChangeStart", change_start);
|
||||
router.events.on("routeChangeComplete", change_complete);
|
||||
|
@ -140,6 +140,7 @@ RADIX_THEMES_COMPONENTS_MAPPING: dict = {
|
||||
"components.radix.themes.components.radio_group": ["radio", "radio_group"],
|
||||
"components.radix.themes.components.dropdown_menu": ["menu", "dropdown_menu"],
|
||||
"components.radix.themes.components.separator": ["divider", "separator"],
|
||||
"components.radix.themes.components.progress": ["progress"],
|
||||
}
|
||||
|
||||
RADIX_THEMES_LAYOUT_MAPPING: dict = {
|
||||
@ -205,7 +206,13 @@ RADIX_PRIMITIVES_MAPPING: dict = {
|
||||
"components.radix.primitives.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 = {
|
||||
@ -248,7 +255,7 @@ RADIX_MAPPING: dict = {
|
||||
**RADIX_THEMES_COMPONENTS_MAPPING,
|
||||
**RADIX_THEMES_TYPOGRAPHY_MAPPING,
|
||||
**RADIX_THEMES_LAYOUT_MAPPING,
|
||||
**RADIX_PRIMITIVES_MAPPING,
|
||||
**RADIX_PRIMITIVES_SHORTCUT_MAPPING,
|
||||
}
|
||||
|
||||
_MAPPING: dict = {
|
||||
@ -338,7 +345,6 @@ _SUBMODULES: set[str] = {
|
||||
"testing",
|
||||
"utils",
|
||||
"vars",
|
||||
"ivars",
|
||||
"config",
|
||||
"compiler",
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ from . import compiler as compiler
|
||||
from . import components as components
|
||||
from . import config as config
|
||||
from . import event as event
|
||||
from . import ivars as ivars
|
||||
from . import model as model
|
||||
from . import style as style
|
||||
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.drawer import drawer as drawer
|
||||
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_panel as theme_panel
|
||||
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.inset import inset as inset
|
||||
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_group import radio as radio
|
||||
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.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_ns as list # noqa
|
||||
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.section import section as section
|
||||
@ -197,6 +197,7 @@ RADIX_THEMES_COMPONENTS_MAPPING: dict
|
||||
RADIX_THEMES_LAYOUT_MAPPING: dict
|
||||
RADIX_THEMES_TYPOGRAPHY_MAPPING: dict
|
||||
RADIX_PRIMITIVES_MAPPING: dict
|
||||
RADIX_PRIMITIVES_SHORTCUT_MAPPING: dict
|
||||
COMPONENTS_CORE_MAPPING: dict
|
||||
COMPONENTS_BASE_MAPPING: dict
|
||||
RADIX_MAPPING: dict
|
||||
|
@ -10,6 +10,7 @@ import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import io
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import platform
|
||||
@ -34,7 +35,7 @@ from typing import (
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, UploadFile
|
||||
from fastapi.middleware import cors
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
|
||||
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.config import get_config
|
||||
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 (
|
||||
DECORATED_PAGES,
|
||||
)
|
||||
@ -291,13 +292,12 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
"`connect_error_component` is deprecated, use `overlay_component` instead"
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
base_state_subclasses = BaseState.__subclasses__()
|
||||
|
||||
# Special case to allow test cases have multiple subclasses of rx.BaseState.
|
||||
if not is_testing_env() and len(base_state_subclasses) > 1:
|
||||
# Only one Base State class is allowed.
|
||||
if not is_testing_env() and BaseState.__subclasses__() != [State]:
|
||||
# Only rx.State is allowed as Base State subclass.
|
||||
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:
|
||||
@ -399,6 +399,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
"""Add default api endpoints (ping)."""
|
||||
# To test the server.
|
||||
self.api.get(str(constants.Endpoint.PING))(ping)
|
||||
self.api.get(str(constants.Endpoint.HEALTH))(health)
|
||||
|
||||
def _add_optional_endpoints(self):
|
||||
"""Add optional api endpoints (_upload)."""
|
||||
@ -607,7 +608,10 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
for route in self.pages:
|
||||
replaced_route = replace_brackets_with_keywords(route)
|
||||
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 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:
|
||||
if dep not in state.vars and dep not in state.backend_vars:
|
||||
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:
|
||||
@ -1192,6 +1196,7 @@ class App(MiddlewareMixin, LifespanMixin, Base):
|
||||
FRONTEND_ARG_SPEC,
|
||||
BACKEND_ARG_SPEC,
|
||||
],
|
||||
strict=False,
|
||||
):
|
||||
if hasattr(handler_fn, "__name__"):
|
||||
_fn_name = handler_fn.__name__
|
||||
@ -1343,6 +1348,38 @@ async def ping() -> str:
|
||||
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):
|
||||
"""Upload a file.
|
||||
|
||||
@ -1523,8 +1560,9 @@ class EventNamespace(AsyncNamespace):
|
||||
sid: The Socket.IO session id.
|
||||
data: The event data.
|
||||
"""
|
||||
fields = json.loads(data)
|
||||
# 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.sid_to_token[sid] = event.token
|
||||
|
@ -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)
|
||||
pydantic_main.validate_field_name = validate_field_name # type: ignore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from reflex.vars import Var
|
||||
|
||||
|
||||
class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
||||
"""The base class subclassed by all Reflex classes.
|
||||
@ -92,7 +95,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls) -> dict[str, Any]:
|
||||
def get_fields(cls) -> dict[str, ModelField]:
|
||||
"""Get the fields of the object.
|
||||
|
||||
Returns:
|
||||
@ -101,7 +104,7 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
||||
return cls.__fields__
|
||||
|
||||
@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.
|
||||
|
||||
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.
|
||||
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(
|
||||
name=var_name,
|
||||
value=default_value,
|
||||
@ -133,13 +136,4 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable]
|
||||
# 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
|
||||
key = getattr(self, key)
|
||||
return self._get_value(
|
||||
key,
|
||||
to_dict=True,
|
||||
by_alias=False,
|
||||
include=None,
|
||||
exclude=None,
|
||||
exclude_unset=False,
|
||||
exclude_defaults=False,
|
||||
exclude_none=False,
|
||||
)
|
||||
return key
|
||||
|
@ -18,13 +18,12 @@ from reflex.components.component import (
|
||||
StatefulComponent,
|
||||
)
|
||||
from reflex.config import get_config
|
||||
from reflex.ivars.base import LiteralVar
|
||||
from reflex.state import BaseState
|
||||
from reflex.style import SYSTEM_COLOR_MODE
|
||||
from reflex.utils.exec import is_prod_mode
|
||||
from reflex.utils.imports import ImportVar
|
||||
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:
|
||||
@ -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.
|
||||
|
||||
Args:
|
||||
@ -379,7 +378,7 @@ def compile_theme(style: ComponentStyle) -> tuple[str, str]:
|
||||
theme = utils.create_theme(style)
|
||||
|
||||
# Compile the theme.
|
||||
code = _compile_theme(theme)
|
||||
code = _compile_theme(str(LiteralVar.create(theme)))
|
||||
return output_path, code
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ from typing import Any, Callable, Dict, Optional, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from reflex.utils.prerequisites import get_web_dir
|
||||
from reflex.vars.base import Var
|
||||
|
||||
try:
|
||||
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.utils import console, format, imports, path_ops
|
||||
from reflex.utils.imports import ImportVar, ParsedImportDict
|
||||
from reflex.vars import Var
|
||||
|
||||
# To re-export this function.
|
||||
merge_imports = imports.merge_imports
|
||||
@ -152,7 +152,9 @@ def compile_state(state: Type[BaseState]) -> dict:
|
||||
console.warn(
|
||||
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)
|
||||
|
||||
|
||||
@ -266,7 +268,7 @@ def compile_custom_component(
|
||||
}
|
||||
|
||||
# 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.
|
||||
return (
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.components.component import Component
|
||||
from reflex.ivars.base import ImmutableVar
|
||||
from reflex.vars.base import Var
|
||||
|
||||
|
||||
class AppWrap(Fragment):
|
||||
@ -15,4 +15,4 @@ class AppWrap(Fragment):
|
||||
Returns:
|
||||
A new AppWrap component containing {children}.
|
||||
"""
|
||||
return super().create(ImmutableVar.create("children"))
|
||||
return super().create(Var(_js_expr="children"))
|
||||
|
@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.event import EventHandler, EventSpec
|
||||
from reflex.style import Style
|
||||
from reflex.vars import Var
|
||||
from reflex.vars.base import Var
|
||||
|
||||
class AppWrap(Fragment):
|
||||
@overload
|
||||
|
@ -7,8 +7,7 @@ from typing import Any, Iterator
|
||||
from reflex.components.component import Component
|
||||
from reflex.components.tags import Tag
|
||||
from reflex.components.tags.tagless import Tagless
|
||||
from reflex.ivars.base import ImmutableVar
|
||||
from reflex.vars import Var
|
||||
from reflex.vars.base import Var
|
||||
|
||||
|
||||
class Bare(Component):
|
||||
@ -26,16 +25,14 @@ class Bare(Component):
|
||||
Returns:
|
||||
The component.
|
||||
"""
|
||||
if isinstance(contents, ImmutableVar):
|
||||
if isinstance(contents, Var):
|
||||
return cls(contents=contents)
|
||||
if isinstance(contents, Var) and contents._get_all_var_data():
|
||||
contents = contents.to(str)
|
||||
else:
|
||||
contents = str(contents) if contents is not None else ""
|
||||
return cls(contents=contents) # type: ignore
|
||||
|
||||
def _render(self) -> Tag:
|
||||
if isinstance(self.contents, ImmutableVar):
|
||||
if isinstance(self.contents, Var):
|
||||
return Tagless(contents=f"{{{str(self.contents)}}}")
|
||||
return Tagless(contents=str(self.contents))
|
||||
|
||||
|
@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
|
||||
from reflex.components.component import Component
|
||||
from reflex.event import EventHandler, EventSpec
|
||||
from reflex.style import Style
|
||||
from reflex.vars import Var
|
||||
from reflex.vars.base import Var
|
||||
|
||||
class Body(Component):
|
||||
@overload
|
||||
|
@ -8,7 +8,7 @@ from typing import Any, Callable, Dict, Optional, Union, overload
|
||||
from reflex.components.component import Component
|
||||
from reflex.event import EventHandler, EventSpec
|
||||
from reflex.style import Style
|
||||
from reflex.vars import Var
|
||||
from reflex.vars.base import Var
|
||||
|
||||
class NextDocumentLib(Component):
|
||||
@overload
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user