Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,16 @@ jobs:
node-version: 22

- name: Install reflex-docs dependencies
working-directory: ./docs/app
run: sfw uv sync
run: sfw uv sync --package reflex-docs-app

- name: Init Website for reflex-docs
working-directory: ./docs/app
run: uv run --active --no-sync reflex init
run: uv run --no-sync reflex init
- name: Run Website and Check for errors
run: |
# Verify sfw wrappers are on PATH
which npm && npm -v
uv run --active --no-sync bash scripts/integration.sh ./docs/app prod
uv run --no-sync bash scripts/integration.sh ./docs/app prod

- name: Upload Socket.dev Firewall report
if: always()
Expand Down Expand Up @@ -215,13 +214,12 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Install Requirements for reflex-docs
working-directory: ./docs/app
run: uv sync
run: uv sync --package reflex-docs-app
- name: Init Website for reflex-docs
working-directory: ./docs/app
run: uv run --active --no-sync reflex init
run: uv run --no-sync reflex init
- name: Run Website and Check for errors
run: |
# Check that npm is home
npm -v
uv run --active --no-sync bash scripts/integration.sh ./docs/app prod
uv run --no-sync bash scripts/integration.sh ./docs/app prod
3 changes: 3 additions & 0 deletions docs/app/reflex_docs/reflex_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import reflex_enterprise as rxe
from reflex.utils.exec import is_prod_mode
from reflex_ui_shared import styles
from reflex_ui_shared.backend.status import monitor_checkly_status
from reflex_ui_shared.constants import REFLEX_ASSETS_CDN
from reflex_ui_shared.meta.meta import favicons_links, to_cdn_image_url
from reflex_ui_shared.telemetry import get_pixel_website_trackers
Expand Down Expand Up @@ -48,6 +49,8 @@
],
)

app.register_lifespan_task(monitor_checkly_status)

# XXX: The app is TOO BIG to build on Windows, so explicitly disallow it except for testing
if sys.platform == "win32":
if not os.environ.get("REFLEX_WEB_WINDOWS_OVERRIDE"):
Expand Down
12 changes: 9 additions & 3 deletions docs/app/reflex_docs/templates/docpage/docpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
from reflex.components.radix.themes.base import LiteralAccentColor
from reflex.experimental.client_state import ClientStateVar
from reflex.utils.format import to_snake_case, to_title_case
from reflex_ui_shared.backend.status import StatusState
from reflex_ui_shared.components.blocks.code import *
from reflex_ui_shared.components.blocks.demo import *
from reflex_ui_shared.components.blocks.headings import *
from reflex_ui_shared.components.blocks.typography import *
from reflex_ui_shared.components.icons import get_icon
from reflex_ui_shared.components.marketing_button import button as marketing_button
from reflex_ui_shared.components.server_status import server_status
from reflex_ui_shared.route import Route, get_path
from reflex_ui_shared.styles.colors import c_color
from reflex_ui_shared.utils.docpage import right_sidebar_item_highlight
Expand Down Expand Up @@ -288,9 +290,13 @@ def docpage_footer(path: str):
menu_socials(),
class_name="flex flex-row gap-6 justify-between items-end w-full",
),
rx.text(
f"Copyright © {datetime.now().year} Pynecone, Inc.",
class_name="font-small text-slate-9",
rx.el.div(
rx.text(
f"Copyright © {datetime.now().year} Pynecone, Inc.",
class_name="font-small text-slate-9",
),
server_status(StatusState.status),
class_name="flex flex-row items-center gap-4 justify-between w-full",
),
class_name="flex flex-col justify-between gap-10 py-6 lg:py-8 w-full",
),
Expand Down
111 changes: 111 additions & 0 deletions packages/reflex-ui-shared/src/reflex_ui_shared/backend/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Checkly-backed service status state and polling utilities."""

import asyncio
import contextlib
from enum import StrEnum

import httpx

import reflex as rx
from reflex_ui_shared.constants import (
CHECKLY_ACCOUNT_ID,
CHECKLY_API_BASE_URL,
CHECKLY_API_KEY,
CHECKLY_CHECK_GROUP_ID,
)


class ServiceStatus(StrEnum):
"""Supported service health states exposed in the UI."""

SUCCESS = "Success"
WARNING = "Warning"
CRITICAL = "Critical"


CURRENT_STATUS = ServiceStatus.SUCCESS.value


# Check status of each check in parallel
async def check_status(check_id: str) -> dict:
"""Fetch status flags for a single Checkly check.

Returns:
A dictionary with failure and degraded flags.
"""
status_url = f"{CHECKLY_API_BASE_URL}/check-statuses/{check_id}"
async with httpx.AsyncClient() as client:
status_response = await client.get(
status_url,
headers={
"Authorization": f"Bearer {CHECKLY_API_KEY}",
"X-Checkly-Account": CHECKLY_ACCOUNT_ID,
},
)
if status_response.status_code == 200:
status_data = status_response.json()
return {
"has_failures": status_data.get("hasFailures", False),
"is_degraded": status_data.get("isDegraded", False),
}

return {"has_failures": False, "is_degraded": False}


async def monitor_checkly_status() -> None:
"""Continuously monitor Checkly check group status.

Updates the global STATUS variable every 60 seconds.
- Critical: if any check has failures
- Warning: if no failures but some checks are degraded
- Success: all checks are healthy

"""
if not all((CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID, CHECKLY_CHECK_GROUP_ID)):
return

headers = {
"Authorization": f"Bearer {CHECKLY_API_KEY}",
"X-Checkly-Account": CHECKLY_ACCOUNT_ID,
}

try:
while True:
with contextlib.suppress(Exception):
global CURRENT_STATUS

# Get checks in this group
checks_url = f"{CHECKLY_API_BASE_URL}/check-groups/{CHECKLY_CHECK_GROUP_ID}/checks"
async with httpx.AsyncClient(timeout=httpx.Timeout(30)) as client:
checks_response = await client.get(checks_url, headers=headers)
if checks_response.status_code == 200:
checks = checks_response.json()

check_ids = [check.get("id") for check in checks if check.get("id")]
results = await asyncio.gather(*[
check_status(check_id) for check_id in check_ids
])

# Determine overall status
has_any_failures = any(r["has_failures"] for r in results)
has_any_degraded = any(r["is_degraded"] for r in results)

if has_any_failures:
CURRENT_STATUS = ServiceStatus.CRITICAL.value
elif has_any_degraded:
CURRENT_STATUS = ServiceStatus.WARNING.value
else:
CURRENT_STATUS = ServiceStatus.SUCCESS.value

await asyncio.sleep(60)
Comment thread
carlosabadia marked this conversation as resolved.
except asyncio.CancelledError:
pass
Comment thread
carlosabadia marked this conversation as resolved.


class StatusState(rx.State):
"""Reflex state that exposes the current service status."""

@rx.var(interval=60)
def status(self) -> str:
"""Return the current status value for the status pill."""
return CURRENT_STATUS
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,10 @@
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66797 10.0002C6.66797 9.63197 6.96645 9.3335 7.33464 9.3335H8.66797C9.03616 9.3335 9.33464 9.63197 9.33464 10.0002C9.33464 10.3684 9.03616 10.6668 8.66797 10.6668H7.33464C6.96645 10.6668 6.66797 10.3684 6.66797 10.0002Z" fill="currentColor"/>
<path d="M9.37207 0.666992C10.4331 0.666982 11.2825 0.666772 11.9609 0.738281C12.659 0.811908 13.2527 0.967495 13.7705 1.33008C14.1209 1.57541 14.4256 1.8801 14.6709 2.23047C15.0335 2.7483 15.1891 3.34194 15.2627 4.04004C15.3342 4.71845 15.334 5.56792 15.334 6.62891V6.7041C15.334 7.76509 15.3342 8.61456 15.2627 9.29297C15.1891 9.99122 15.0335 10.5856 14.6709 11.1035C14.4256 11.4537 14.1207 11.7587 13.7705 12.0039C13.2528 12.3663 12.6589 12.5211 11.9609 12.5947C11.3138 12.6629 10.5111 12.6659 9.51758 12.666C9.5059 12.7659 9.49909 12.8662 9.50098 12.9658C9.5068 13.2736 9.51005 13.4276 9.80176 13.7139C10.0934 13.9999 10.3572 14 10.8848 14H11.334C11.7022 14 12.001 14.2988 12.001 14.667C12.0008 15.035 11.7021 15.333 11.334 15.333H4.66797C4.29989 15.333 4.00115 15.035 4.00098 14.667C4.00098 14.2988 4.29978 14 4.66797 14H5.11719C5.64471 14 5.9086 13.9999 6.2002 13.7139C6.4919 13.4276 6.49516 13.2736 6.50098 12.9658C6.50286 12.8661 6.49509 12.766 6.4834 12.666C5.49036 12.6659 4.68793 12.6629 4.04102 12.5947C3.34306 12.5211 2.7492 12.3663 2.23145 12.0039C1.88121 11.7587 1.57634 11.4537 1.33105 11.1035C0.968403 10.5856 0.812872 9.99122 0.739258 9.29297C0.667749 8.61456 0.667959 7.7651 0.667969 6.7041V6.62891C0.667959 5.56791 0.667749 4.71845 0.739258 4.04004C0.812885 3.34194 0.968471 2.7483 1.33105 2.23047C1.57639 1.8801 1.88108 1.57541 2.23145 1.33008C2.74928 0.967495 3.34291 0.811908 4.04102 0.738281C4.71943 0.666772 5.56889 0.666982 6.62988 0.666992H9.37207ZM6.66797 2C5.56069 2 4.78193 2.00121 4.18164 2.06445C3.59328 2.12647 3.25295 2.24206 2.99609 2.42188C2.77313 2.57799 2.57897 2.77215 2.42285 2.99512C2.24304 3.25198 2.12745 3.5923 2.06543 4.18066C2.00219 4.78095 2.00098 5.55972 2.00098 6.66699C2.00098 7.77431 2.00216 8.55305 2.06543 9.15332C2.12744 9.74134 2.24316 10.0811 2.42285 10.3379C2.57897 10.5608 2.77314 10.755 2.99609 10.9111C3.25297 11.091 3.59318 11.2075 4.18164 11.2695C4.78192 11.3328 5.56074 11.333 6.66797 11.333H9.33398C10.4412 11.333 11.22 11.3328 11.8203 11.2695C12.4088 11.2075 12.749 11.091 13.0059 10.9111C13.2288 10.755 13.423 10.5608 13.5791 10.3379C13.7588 10.0811 13.8745 9.74134 13.9365 9.15332C13.9998 8.55305 14.001 7.77431 14.001 6.66699C14.001 5.55972 13.9998 4.78095 13.9365 4.18066C13.8745 3.5923 13.7589 3.25198 13.5791 2.99512C13.423 2.77215 13.2288 2.57799 13.0059 2.42188C12.749 2.24206 12.4087 2.12647 11.8203 2.06445C11.22 2.00121 10.4413 2 9.33398 2H6.66797Z" fill="currentColor"/>
</svg>"""

circle = """<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><circle cx="8" cy="8" r="4" fill="currentColor"/></svg>
"""

ICONS = {
# Socials
"github": github,
Expand Down Expand Up @@ -658,6 +662,7 @@
"moon_footer": moon_footer,
"sun_footer": sun_footer,
"computer_footer": computer_footer,
"circle": circle,
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Server status badge component used in site footers."""

from typing import Literal

import reflex as rx
from reflex_ui_shared.components.icons import get_icon
from reflex_ui_shared.constants import STATUS_WEB_URL

StatusVariant = Literal["Success", "Warning", "Critical"]

DEFAULT_CLASS_NAME = "inline-flex flex-row gap-1.5 items-center font-medium text-sm px-2.5 rounded-[10px] h-9 hover:bg-secondary-3 transition-bg"

STATUS_TEXT_COLORS: dict[StatusVariant, str] = {
"Success": "text-success-9",
"Warning": "text-warning-11",
"Critical": "text-destructive-10",
}


STATUS_VARIANT_TEXT: dict[StatusVariant, str] = {
"Success": "All servers are operational",
"Warning": "Some servers are unavailable",
"Critical": "All servers are down",
}

STATUS_ICON_COLORS: dict[StatusVariant, str] = {
"Success": "!text-success-8",
"Warning": "!text-warning-8",
"Critical": "!text-destructive-9",
}


def _status_icon(color: str) -> rx.Component:
"""Create a fresh status icon component for each render branch.

Returns:
A new circle icon component with the given color class.
"""
return get_icon("circle", class_name=color)


def server_status(status: StatusVariant | rx.Var[str]) -> rx.Component:
"""Create a server status component.

Args:
status: The status of the server.

Returns:
A linked status badge that points to the public status page.

"""
return rx.el.a(
rx.match(
status,
(
"Success",
rx.el.div(
_status_icon(STATUS_ICON_COLORS["Success"]),
STATUS_VARIANT_TEXT["Success"],
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
),
),
(
"Warning",
rx.el.div(
_status_icon(STATUS_ICON_COLORS["Warning"]),
STATUS_VARIANT_TEXT["Warning"],
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Warning']}",
),
),
(
"Critical",
rx.el.div(
_status_icon(STATUS_ICON_COLORS["Critical"]),
STATUS_VARIANT_TEXT["Critical"],
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Critical']}",
),
),
rx.el.div(
_status_icon(STATUS_ICON_COLORS["Success"]),
STATUS_VARIANT_TEXT["Success"],
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
),
),
href=STATUS_WEB_URL,
target="_blank",
rel="noopener noreferrer",
)
7 changes: 7 additions & 0 deletions packages/reflex-ui-shared/src/reflex_ui_shared/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
TWITTER_URL = "https://twitter.com/getreflex"
DISCORD_URL = "https://discord.gg/T5WSbC2YtQ"
ROADMAP_URL = "https://github.com/reflex-dev/reflex/issues/2727"
STATUS_WEB_URL = "https://status.reflex.dev"

REFLEX_URL = "https://reflex.dev/"
REFLEX_DOMAIN_URL = "https://reflex.dev/"
Expand All @@ -36,3 +37,9 @@
RECENT_BLOGS_API_URL: str = os.environ.get(
"RECENT_BLOGS_API_URL", "https://reflex.dev/api/v1/recent-blogs"
)


CHECKLY_API_BASE_URL: str = "https://api.checklyhq.com/v1"
CHECKLY_ACCOUNT_ID = os.environ.get("CHECKLY_ACCOUNT_ID", "")
CHECKLY_API_KEY = os.environ.get("CHECKLY_API_KEY", "")
CHECKLY_CHECK_GROUP_ID = os.environ.get("CHECKLY_CHECK_GROUP_ID", "")
16 changes: 11 additions & 5 deletions packages/reflex-ui-shared/src/reflex_ui_shared/views/footer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import reflex as rx
from reflex.style import color_mode, set_color_mode
from reflex_ui_shared.backend.signup import IndexState
from reflex_ui_shared.backend.status import StatusState
from reflex_ui_shared.components.icons import get_icon
from reflex_ui_shared.components.server_status import server_status
from reflex_ui_shared.constants import (
CHANGELOG_URL,
DISCORD_URL,
Expand Down Expand Up @@ -62,7 +64,7 @@ def logo() -> rx.Component:
class_name="shrink-0 hidden dark:block",
),
href="/",
class_name="block shrink-0 mr-[7rem] md:hidden xl:block",
class_name="block shrink-0 mr-[7rem] md:hidden xl:block h-fit",
)


Expand Down Expand Up @@ -297,11 +299,15 @@ def footer_index(class_name: str = "", grid_class_name: str = "") -> rx.Componen
class_name="flex flex-col max-lg:gap-6 lg:flex-row w-full",
),
rx.el.div(
rx.el.span(
f"Copyright © {datetime.now().year} Pynecone, Inc.",
class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
server_status(StatusState.status),
rx.el.div(
rx.el.span(
f"Copyright © {datetime.now().year} Pynecone, Inc.",
class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
),
menu_socials(),
class_name="flex flex-row items-center gap-6",
),
menu_socials(),
rx.el.div(
class_name="absolute -top-px -right-24 w-24 h-px bg-gradient-to-l from-transparent to-current text-m-slate-4 dark:text-m-slate-10 max-lg:hidden"
),
Expand Down
Loading
Loading