Skip to content

Commit 7e79619

Browse files
authored
ENG-9279: Add status to reflex.dev footer (#6351)
* ENG-9279: Add status to reflex.dev footer * update tests * thanks greptile * update * change those * revert that * remove this * revert uv lock
1 parent ab3550b commit 7e79619

7 files changed

Lines changed: 234 additions & 8 deletions

File tree

docs/app/reflex_docs/reflex_docs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import reflex as rx
77
import reflex_enterprise as rxe
88
from reflex_site_shared import styles
9+
from reflex_site_shared.backend.status import monitor_checkly_status
910
from reflex_site_shared.constants import REFLEX_ASSETS_CDN
1011
from reflex_site_shared.meta.meta import favicons_links, to_cdn_image_url
1112
from reflex_site_shared.telemetry import get_pixel_website_trackers
@@ -47,6 +48,8 @@
4748
],
4849
)
4950

51+
app.register_lifespan_task(monitor_checkly_status)
52+
5053
# XXX: The app is TOO BIG to build on Windows, so explicitly disallow it except for testing
5154
if sys.platform == "win32":
5255
if not os.environ.get("REFLEX_WEB_WINDOWS_OVERRIDE"):

docs/app/reflex_docs/templates/docpage/docpage.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
from reflex.components.radix.themes.base import LiteralAccentColor
1010
from reflex.experimental.client_state import ClientStateVar
1111
from reflex.utils.format import to_snake_case, to_title_case
12+
from reflex_site_shared.backend.status import StatusState
1213
from reflex_site_shared.components.blocks.code import *
1314
from reflex_site_shared.components.blocks.demo import *
1415
from reflex_site_shared.components.blocks.headings import *
1516
from reflex_site_shared.components.blocks.typography import *
1617
from reflex_site_shared.components.icons import get_icon
1718
from reflex_site_shared.components.marketing_button import button as marketing_button
19+
from reflex_site_shared.components.server_status import server_status
1820
from reflex_site_shared.route import Route, get_path
1921
from reflex_site_shared.styles.colors import c_color
2022
from reflex_site_shared.utils.docpage import right_sidebar_item_highlight
@@ -284,9 +286,13 @@ def docpage_footer(path: str):
284286
menu_socials(),
285287
class_name="flex flex-row gap-6 justify-between items-end w-full",
286288
),
287-
rx.text(
288-
f"Copyright © {datetime.now().year} Pynecone, Inc.",
289-
class_name="font-small text-slate-9",
289+
rx.el.div(
290+
rx.text(
291+
f"Copyright © {datetime.now().year} Pynecone, Inc.",
292+
class_name="font-small text-slate-9",
293+
),
294+
server_status(StatusState.status),
295+
class_name="flex flex-row items-center gap-4 justify-between w-full",
290296
),
291297
class_name="flex flex-col justify-between gap-10 py-6 lg:py-8 w-full",
292298
),
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Checkly-backed service status state and polling utilities."""
2+
3+
import asyncio
4+
import contextlib
5+
from enum import StrEnum
6+
7+
import httpx
8+
9+
import reflex as rx
10+
from reflex_site_shared.constants import (
11+
CHECKLY_ACCOUNT_ID,
12+
CHECKLY_API_BASE_URL,
13+
CHECKLY_API_KEY,
14+
CHECKLY_CHECK_GROUP_ID,
15+
)
16+
17+
18+
class ServiceStatus(StrEnum):
19+
"""Supported service health states exposed in the UI."""
20+
21+
SUCCESS = "Success"
22+
WARNING = "Warning"
23+
CRITICAL = "Critical"
24+
25+
26+
CURRENT_STATUS = ServiceStatus.SUCCESS.value
27+
28+
29+
# Check status of each check in parallel
30+
async def check_status(check_id: str) -> dict:
31+
"""Fetch status flags for a single Checkly check.
32+
33+
Returns:
34+
A dictionary with failure and degraded flags.
35+
"""
36+
status_url = f"{CHECKLY_API_BASE_URL}/check-statuses/{check_id}"
37+
async with httpx.AsyncClient() as client:
38+
status_response = await client.get(
39+
status_url,
40+
headers={
41+
"Authorization": f"Bearer {CHECKLY_API_KEY}",
42+
"X-Checkly-Account": CHECKLY_ACCOUNT_ID,
43+
},
44+
)
45+
if status_response.status_code == 200:
46+
status_data = status_response.json()
47+
return {
48+
"has_failures": status_data.get("hasFailures", False),
49+
"is_degraded": status_data.get("isDegraded", False),
50+
}
51+
52+
return {"has_failures": False, "is_degraded": False}
53+
54+
55+
async def monitor_checkly_status() -> None:
56+
"""Continuously monitor Checkly check group status.
57+
58+
Updates the global STATUS variable every 60 seconds.
59+
- Critical: if any check has failures
60+
- Warning: if no failures but some checks are degraded
61+
- Success: all checks are healthy
62+
63+
"""
64+
if not all((CHECKLY_API_KEY, CHECKLY_ACCOUNT_ID, CHECKLY_CHECK_GROUP_ID)):
65+
return
66+
67+
headers = {
68+
"Authorization": f"Bearer {CHECKLY_API_KEY}",
69+
"X-Checkly-Account": CHECKLY_ACCOUNT_ID,
70+
}
71+
72+
try:
73+
while True:
74+
with contextlib.suppress(Exception):
75+
global CURRENT_STATUS
76+
77+
# Get checks in this group
78+
checks_url = f"{CHECKLY_API_BASE_URL}/check-groups/{CHECKLY_CHECK_GROUP_ID}/checks"
79+
async with httpx.AsyncClient(timeout=httpx.Timeout(30)) as client:
80+
checks_response = await client.get(checks_url, headers=headers)
81+
if checks_response.status_code == 200:
82+
checks = checks_response.json()
83+
84+
check_ids = [check.get("id") for check in checks if check.get("id")]
85+
results = await asyncio.gather(*[
86+
check_status(check_id) for check_id in check_ids
87+
])
88+
89+
# Determine overall status
90+
has_any_failures = any(r["has_failures"] for r in results)
91+
has_any_degraded = any(r["is_degraded"] for r in results)
92+
93+
if has_any_failures:
94+
CURRENT_STATUS = ServiceStatus.CRITICAL.value
95+
elif has_any_degraded:
96+
CURRENT_STATUS = ServiceStatus.WARNING.value
97+
else:
98+
CURRENT_STATUS = ServiceStatus.SUCCESS.value
99+
100+
await asyncio.sleep(60)
101+
except asyncio.CancelledError:
102+
pass
103+
104+
105+
class StatusState(rx.State):
106+
"""Reflex state that exposes the current service status."""
107+
108+
@rx.var(interval=60)
109+
def status(self) -> str:
110+
"""Return the current status value for the status pill."""
111+
return CURRENT_STATUS

packages/reflex-site-shared/src/reflex_site_shared/components/icons.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,10 @@
568568
<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"/>
569569
<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"/>
570570
</svg>"""
571+
572+
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>
573+
"""
574+
571575
ICONS = {
572576
# Socials
573577
"github": github,
@@ -658,6 +662,7 @@
658662
"moon_footer": moon_footer,
659663
"sun_footer": sun_footer,
660664
"computer_footer": computer_footer,
665+
"circle": circle,
661666
}
662667

663668

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Server status badge component used in site footers."""
2+
3+
from typing import Literal
4+
5+
import reflex as rx
6+
from reflex_site_shared.components.icons import get_icon
7+
from reflex_site_shared.constants import STATUS_WEB_URL
8+
9+
StatusVariant = Literal["Success", "Warning", "Critical"]
10+
11+
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"
12+
13+
STATUS_TEXT_COLORS: dict[StatusVariant, str] = {
14+
"Success": "text-success-9",
15+
"Warning": "text-warning-11",
16+
"Critical": "text-destructive-10",
17+
}
18+
19+
20+
STATUS_VARIANT_TEXT: dict[StatusVariant, str] = {
21+
"Success": "All servers are operational",
22+
"Warning": "Some servers are unavailable",
23+
"Critical": "All servers are down",
24+
}
25+
26+
STATUS_ICON_COLORS: dict[StatusVariant, str] = {
27+
"Success": "!text-success-8",
28+
"Warning": "!text-warning-8",
29+
"Critical": "!text-destructive-9",
30+
}
31+
32+
33+
def _status_icon(color: str) -> rx.Component:
34+
"""Create a fresh status icon component for each render branch.
35+
36+
Returns:
37+
A new circle icon component with the given color class.
38+
"""
39+
return get_icon("circle", class_name=color)
40+
41+
42+
def server_status(status: StatusVariant | rx.Var[str]) -> rx.Component:
43+
"""Create a server status component.
44+
45+
Args:
46+
status: The status of the server.
47+
48+
Returns:
49+
A linked status badge that points to the public status page.
50+
51+
"""
52+
return rx.el.a(
53+
rx.match(
54+
status,
55+
(
56+
"Success",
57+
rx.el.div(
58+
_status_icon(STATUS_ICON_COLORS["Success"]),
59+
STATUS_VARIANT_TEXT["Success"],
60+
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
61+
),
62+
),
63+
(
64+
"Warning",
65+
rx.el.div(
66+
_status_icon(STATUS_ICON_COLORS["Warning"]),
67+
STATUS_VARIANT_TEXT["Warning"],
68+
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Warning']}",
69+
),
70+
),
71+
(
72+
"Critical",
73+
rx.el.div(
74+
_status_icon(STATUS_ICON_COLORS["Critical"]),
75+
STATUS_VARIANT_TEXT["Critical"],
76+
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Critical']}",
77+
),
78+
),
79+
rx.el.div(
80+
_status_icon(STATUS_ICON_COLORS["Success"]),
81+
STATUS_VARIANT_TEXT["Success"],
82+
class_name=f"{DEFAULT_CLASS_NAME} {STATUS_TEXT_COLORS['Success']}",
83+
),
84+
),
85+
href=STATUS_WEB_URL,
86+
target="_blank",
87+
rel="noopener noreferrer",
88+
)

packages/reflex-site-shared/src/reflex_site_shared/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
TWITTER_URL = "https://twitter.com/getreflex"
2222
DISCORD_URL = "https://discord.gg/T5WSbC2YtQ"
2323
ROADMAP_URL = "https://github.com/reflex-dev/reflex/issues/2727"
24+
STATUS_WEB_URL = "https://status.reflex.dev"
2425

2526
REFLEX_URL = "https://reflex.dev/"
2627
REFLEX_DOMAIN_URL = "https://reflex.dev/"
@@ -36,3 +37,9 @@
3637
RECENT_BLOGS_API_URL: str = os.environ.get(
3738
"RECENT_BLOGS_API_URL", "https://reflex.dev/api/v1/recent-blogs"
3839
)
40+
41+
42+
CHECKLY_API_BASE_URL: str = "https://api.checklyhq.com/v1"
43+
CHECKLY_ACCOUNT_ID = os.environ.get("CHECKLY_ACCOUNT_ID", "")
44+
CHECKLY_API_KEY = os.environ.get("CHECKLY_API_KEY", "")
45+
CHECKLY_CHECK_GROUP_ID = os.environ.get("CHECKLY_CHECK_GROUP_ID", "")

packages/reflex-site-shared/src/reflex_site_shared/views/footer.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import reflex as rx
99
from reflex.style import color_mode, set_color_mode
1010
from reflex_site_shared.backend.signup import IndexState
11+
from reflex_site_shared.backend.status import StatusState
1112
from reflex_site_shared.components.icons import get_icon
13+
from reflex_site_shared.components.server_status import server_status
1214
from reflex_site_shared.constants import (
1315
CHANGELOG_URL,
1416
DISCORD_URL,
@@ -62,7 +64,7 @@ def logo() -> rx.Component:
6264
class_name="shrink-0 hidden dark:block",
6365
),
6466
href="/",
65-
class_name="block shrink-0 mr-[7rem] md:hidden xl:block",
67+
class_name="block shrink-0 mr-[7rem] md:hidden xl:block h-fit",
6668
)
6769

6870

@@ -297,11 +299,15 @@ def footer_index(class_name: str = "", grid_class_name: str = "") -> rx.Componen
297299
class_name="flex flex-col max-lg:gap-6 lg:flex-row w-full",
298300
),
299301
rx.el.div(
300-
rx.el.span(
301-
f"Copyright © {datetime.now().year} Pynecone, Inc.",
302-
class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
302+
server_status(StatusState.status),
303+
rx.el.div(
304+
rx.el.span(
305+
f"Copyright © {datetime.now().year} Pynecone, Inc.",
306+
class_name="text-xs text-m-slate-7 dark:text-m-slate-6 font-medium",
307+
),
308+
menu_socials(),
309+
class_name="flex flex-row items-center gap-6",
303310
),
304-
menu_socials(),
305311
rx.el.div(
306312
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"
307313
),

0 commit comments

Comments
 (0)