From 923467008e02d6b0fb6dfcbfc9380618a7e9dfbf Mon Sep 17 00:00:00 2001 From: Suchith Krishna S Donni Date: Tue, 3 Sep 2024 03:08:55 +0530 Subject: [PATCH 1/2] added sandbox app and basic load testing --- integration/loadtesting/.gitignore | 2 + integration/loadtesting/README.md | 41 +++ integration/loadtesting/assets/favicon.ico | Bin 0 -> 4286 bytes integration/loadtesting/assets/github.svg | 10 + integration/loadtesting/locustfile.py | 239 ++++++++++++++++++ integration/loadtesting/rxconfig.py | 5 + integration/loadtesting/sandbox/__init__.py | 1 + .../sandbox/components/__init__.py | 0 .../loadtesting/sandbox/components/drawer.py | 60 +++++ .../loadtesting/sandbox/components/navbar.py | 40 +++ .../loadtesting/sandbox/components/output.py | 93 +++++++ .../loadtesting/sandbox/components/query.py | 213 ++++++++++++++++ integration/loadtesting/sandbox/examples.py | 9 + .../loadtesting/sandbox/pages/__init__.py | 1 + .../loadtesting/sandbox/pages/dashboard.py | 30 +++ .../loadtesting/sandbox/states/__init__.py | 0 .../loadtesting/sandbox/states/base.py | 17 ++ .../loadtesting/sandbox/states/queries.py | 180 +++++++++++++ integration/loadtesting/sandbox/styles.py | 6 + 19 files changed, 947 insertions(+) create mode 100644 integration/loadtesting/.gitignore create mode 100644 integration/loadtesting/README.md create mode 100644 integration/loadtesting/assets/favicon.ico create mode 100644 integration/loadtesting/assets/github.svg create mode 100644 integration/loadtesting/locustfile.py create mode 100644 integration/loadtesting/rxconfig.py create mode 100644 integration/loadtesting/sandbox/__init__.py create mode 100644 integration/loadtesting/sandbox/components/__init__.py create mode 100644 integration/loadtesting/sandbox/components/drawer.py create mode 100644 integration/loadtesting/sandbox/components/navbar.py create mode 100644 integration/loadtesting/sandbox/components/output.py create mode 100644 integration/loadtesting/sandbox/components/query.py create mode 100644 integration/loadtesting/sandbox/examples.py create mode 100644 integration/loadtesting/sandbox/pages/__init__.py create mode 100644 integration/loadtesting/sandbox/pages/dashboard.py create mode 100644 integration/loadtesting/sandbox/states/__init__.py create mode 100644 integration/loadtesting/sandbox/states/base.py create mode 100644 integration/loadtesting/sandbox/states/queries.py create mode 100644 integration/loadtesting/sandbox/styles.py diff --git a/integration/loadtesting/.gitignore b/integration/loadtesting/.gitignore new file mode 100644 index 000000000..9c87ebe13 --- /dev/null +++ b/integration/loadtesting/.gitignore @@ -0,0 +1,2 @@ +./env +.web \ No newline at end of file diff --git a/integration/loadtesting/README.md b/integration/loadtesting/README.md new file mode 100644 index 000000000..8e3472148 --- /dev/null +++ b/integration/loadtesting/README.md @@ -0,0 +1,41 @@ + + +# Load testing for Reflex using Locust + +Steps to build locally - + + +Create virtual env +``` +python3 -m venv env +``` + +Activate the virtual env +``` +source env/bin/activate +``` + +Install dependencies +``` +pip install -r requirements.txt +``` + +Also, install locust and locust plugin +``` +pip install locust +pip install locust-plugins[websocket] +``` + +Start the reflex app using poetry +``` +poetry run reflex run +``` + +Start locust in a new terminal window +``` +locust +``` + +This will start a web app on `http://localhost:8089` + +Configure the number of user and the ramp up. Watch results flow in. When done, terminate the locust process and get aggregate data in the terminal. \ No newline at end of file diff --git a/integration/loadtesting/assets/favicon.ico b/integration/loadtesting/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..166ae995eaa63fc96771410a758282dc30e925cf GIT binary patch literal 4286 zcmeHL>rYc>81ELdEe;}zmYd}cUgmJRfwjUwD1`#s5KZP>mMqza#Viv|_7|8f+0+bX zHuqusuw-7Ca`DTu#4U4^o2bjO#K>4%N?Wdi*wZ3Vx%~Ef4}D1`U_EMRg3u z#2#M|V>}}q-@IaO@{9R}d*u7f&~5HfxSkmHVcazU#i30H zAGxQ5Spe!j9`KuGqR@aExK`-}sH1jvqoIp3C7Vm)9Tu=UPE;j^esN~a6^a$ZILngo;^ zGLXl(ZFyY&U!li`6}y-hUQ99v?s`U4O!kgog74FPw-9g+V)qs!jFGEQyvBf><U|E2vRmx|+(VI~S=lT?@~C5pvZOd`x{Q_+3tG6H=gtdWcf z)+7-Zp=UqH^J4sk^>_G-Ufn-2Hz z2mN12|C{5}U`^eCQuFz=F%wp@}SzA1MHEaM^CtJs<{}Tzu$bx2orTKiedgmtVGM{ zdd#vX`&cuiec|My_KW;y{Ryz2kFu9}=~us6hvx1ZqQCk(d+>HP>ks>mmHCjjDh{pe zKQkKpk0SeDX#XMqf$}QV{z=xrN!mQczJAvud@;zFqaU1ocq==Py)qsa=8UKrt!J7r z{RsTo^rgtZo%$rak)DN*D)!(Y^$@yL6Nd=#eu&?unzhH8yq>v{gkt8xcG3S%H)-y_ zqQ1|v|JT$0R~Y}omg2Y+nDvR+K|kzR5i^fmKF>j~N;A35Vr`JWh4yRqKl#P|qlx?` z@|CmBiP}ysYO%m2{eBG6&ix5 zr#u((F2{vb=W4jNmTQh3M^F2o80T49?w>*rv0mt)-o1y!{hRk`E#UVPdna6jnz`rw dKpn)r^--YJZpr;bYU`N~>#v3X5BRU&{{=gv-{1fM literal 0 HcmV?d00001 diff --git a/integration/loadtesting/assets/github.svg b/integration/loadtesting/assets/github.svg new file mode 100644 index 000000000..61c9d791b --- /dev/null +++ b/integration/loadtesting/assets/github.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/integration/loadtesting/locustfile.py b/integration/loadtesting/locustfile.py new file mode 100644 index 000000000..358aa627a --- /dev/null +++ b/integration/loadtesting/locustfile.py @@ -0,0 +1,239 @@ +from locust import task,between +from locust_plugins.users.socketio import SocketIOUser +import uuid +import json +import time + +""" +Pending things to be completed - +1. Error handling +2. Code cleanup and moving variables into config file +3. Documentation +""" + +class RecursiveJSONEncoder(json.JSONEncoder): + + level = 0 + def default(self, obj): + if isinstance(obj, dict): + self.level += 1 + if self.level == 1: + return json.dumps({key: self.default(value) for key, value in obj.items()}, cls=RecursiveJSONEncoder) + else: + return {key: self.default(value) for key, value in obj.items()} + + elif isinstance(obj, list): + return [self.default(element) if not isinstance(element, (str, int, float, bool, type(None))) else element for element in obj] + elif isinstance(obj, (str, int, float, bool, type(None))): + return obj + else: + return json.JSONEncoder.default(self, obj) + + +class ReflexLoadTest(SocketIOUser): + + token = None + + wait_time = between(1, 5) + + request_start_time = None + + def validate_init_connection_message(self, message:str) -> bool: + conn_response_code = "0" + if len(message.strip()) > 0: + message = message.strip() + message_code = message[0] + if message_code == conn_response_code: + extracted_message_json = json.loads(message[1:]) + self.session_sid = extracted_message_json['sid'] + self.session_data = extracted_message_json + return True + + else: + return False + + def validate_init_event_session_message(self, message: str) -> bool: + init_event_response_code = "40/_event" + if len(message) > 0 and message.count(",") > 0: + message_code, message_body = message.split(",") + if message_code == init_event_response_code: + extracted_message_json = json.loads(message_body) + self.event_sid = extracted_message_json['sid'] + self.event_session_data = extracted_message_json + return True + + else: + return False + + def validate_event_response_message(self, message: str) -> bool: + event_response_code = "42/_event" + if len(message) > 0 and message.count(","): + message_code = message.split(",")[0] + print(message_code) + if message_code == event_response_code: + return True + + def register_listener(self, callback_func, event_name): + self.is_waiting_for_message = True + self.callback_func = callback_func + self.event_name = event_name + self.request_start_time = time.time() + print("setting request start time here", self.request_start_time) + + def remove_listener(self): + self.is_waiting_for_message = False + self.callback_func = None + self.event_name = None + self.request_start_time = None + + + def on_message(self, message): + if self.is_waiting_for_message and self.callback_func: + message_match_flag = self.callback_func(message) + if message_match_flag == True: + print(message, time.time(), self.request_start_time) + res_time = time.time() - self.request_start_time + self.fire_response_time_event(res_time, message) + self.remove_listener() + + def fire_response_time_event(self, res_time, message): + self.environment.events.request.fire( + request_type="WSR", + name=self.event_name, + response_time=res_time, + response_length=len(message), + exception=None, + context=self.context(), + ) + + def wait_for_message(self): + counter = 0 + while self.is_waiting_for_message: + #Todo Add timeout exception here + time.sleep(0.05) + counter += 0.05 + + def get_conn_route(self): + # todo put this in config file + host = "localhost" + port = 8000 + protocol = "ws" + url = f"{protocol}://{host}:{str(port)}" + + event_suffix = "/_event/?EIO=4&transport=websocket" + conn_route = url + event_suffix + return conn_route + + def on_start(self): + #todo: Add custom exceptions + try: + self.connect_to_remote() + self.send_access_event() + except: + print("something went wrong") + + def connect_to_remote(self): + conn_route = self.get_conn_route() + self.register_listener(self.validate_init_connection_message, "connection_init_response") + self.connect(conn_route) + self.wait_for_message() + + + def send_access_event(self): + message = self.construct_event_message("access_event") + self.register_listener(self.validate_init_event_session_message, "event_namespace_access_response") + self.send(message, name="_event_namespace_access_request") + self.wait_for_message() + self.token = str(uuid.uuid4()) + + # def disconnect_socket_connection(self): + # print("disconnecting connection") + # self.send("41/_event", name="disconnect") + + def construct_event_message(self, event_type): + event_code = str(self.get_event_code(event_type)) + event_message = (self.get_event_message(event_type)) + return f'{event_code}/_event,{event_message}' + + def get_event_code(self, event_type): + match event_type: + case "access_event": + return 40 + + case _: + return 42 + + def get_event_message(self, event_type): + token = self.token + match event_type: + case "access_event": + return json.dumps([]) + + case "pure_event": + payload = { + 'token': token, + 'name': 'state.set_is_hydrated', + 'router_data': {'pathname': '/', 'query': {}, 'asPath': '/'}, + 'payload': {'value': True} + } + return json.dumps(RecursiveJSONEncoder().default(["event",payload])) + + + case "state_update": + payload = { + 'name': 'state.base_state.toggle_query', + 'payload': {}, + 'handler': None, + 'token': token, + 'router_data': {'pathname': '/', 'query': {}, 'asPath': '/'} + } + return json.dumps(RecursiveJSONEncoder().default(["event",payload])) + + + case "substate_update": + payload = { + 'name': 'state.base_state.query_state.query_api.delta_limit', + 'payload': {'limit': '20'}, + 'handler': None, + 'token': token, + 'router_data': {'pathname': '/', 'query': {}, 'asPath': '/'} + } + return json.dumps(RecursiveJSONEncoder().default(["event",payload])) + + + @task + def test_pure_function(self): + message = self.construct_event_message("pure_event") + print(message) + self.register_listener(self.validate_event_response_message, "pure_function_response") + self.send(message, name="test_pure_function") + self.wait_for_message() + + @task + def test_state_update(self): + message = self.construct_event_message("state_update") + print(message) + self.register_listener(self.validate_event_response_message, "state_update_response") + self.send(message, name="test_state_update") + self.wait_for_message() + + + @task + def test_substate_update(self): + message = self.construct_event_message("substate_update") + print(message) + self.register_listener(self.validate_event_response_message, "substate_update_response") + self.send(message, name="test_substate_update") + self.wait_for_message() + + + + + @task + def test_idle_connection(self): + self.sleep_with_heartbeat(10) + + + + +# 42/_event,["event","{\"token\":\"99c13759-97a1-48eb-fe71-e3ee8f034869\",\"name\":\"state.set_is_hydrated\",\"router_data\":{\"pathname\":\"/\",\"query\":{},\"asPath\":\"/\"},\"payload\":{\"value\":true}}"] \ No newline at end of file diff --git a/integration/loadtesting/rxconfig.py b/integration/loadtesting/rxconfig.py new file mode 100644 index 000000000..6e06f3229 --- /dev/null +++ b/integration/loadtesting/rxconfig.py @@ -0,0 +1,5 @@ +import reflex as rx + +config = rx.Config( + app_name="examples", +) \ No newline at end of file diff --git a/integration/loadtesting/sandbox/__init__.py b/integration/loadtesting/sandbox/__init__.py new file mode 100644 index 000000000..e1d286346 --- /dev/null +++ b/integration/loadtesting/sandbox/__init__.py @@ -0,0 +1 @@ +"""Base template for Reflex.""" diff --git a/integration/loadtesting/sandbox/components/__init__.py b/integration/loadtesting/sandbox/components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration/loadtesting/sandbox/components/drawer.py b/integration/loadtesting/sandbox/components/drawer.py new file mode 100644 index 000000000..2487c3d23 --- /dev/null +++ b/integration/loadtesting/sandbox/components/drawer.py @@ -0,0 +1,60 @@ +import reflex as rx + +from examples.states.queries import QueryAPI + + +def render_data(data: tuple[str, str]): + return rx.vstack( + rx.text(data[0], weight="bold"), + rx.input( + value=data[1], + width="100%", + on_change=lambda value: QueryAPI.update_data(value, data), + ), + width="100%", + spacing="2", + ) + + +def render_drawer_buttons(name: str, color: str, function: callable): + return rx.badge( + rx.text(name, width="100%", text_align="center"), + color_scheme=color, + on_click=function, + variant="surface", + padding="0.75em 1.25em", + width="100%", + cursor="pointer", + ) + + +def render_drawer(): + return rx.drawer.root( + rx.drawer.overlay(z_index="5"), + rx.drawer.portal( + rx.drawer.content( + rx.vstack( + rx.foreach(QueryAPI.selected_entry, render_data), + rx.vstack( + render_drawer_buttons( + "Commit", "grass", QueryAPI.commit_changes + ), + render_drawer_buttons("Close", "ruby", QueryAPI.delta_drawer), + padding="1em 0.5em", + width="inherit", + ), + bg=rx.color_mode_cond("#faf9fb", "#1a181a"), + height="100%", + width="100%", + padding="1.25em", + ), + top="auto", + left="auto", + height="100%", + width="25em", + on_interact_outside=QueryAPI.delta_drawer(), + ), + ), + direction="right", + open=QueryAPI.is_open, + ) diff --git a/integration/loadtesting/sandbox/components/navbar.py b/integration/loadtesting/sandbox/components/navbar.py new file mode 100644 index 000000000..4f632caac --- /dev/null +++ b/integration/loadtesting/sandbox/components/navbar.py @@ -0,0 +1,40 @@ +import reflex as rx + +from examples.states.base import BaseState +from examples.styles import text + +navbar: dict[str, str] = { + "width": "100%", + "padding": "1em 1.15em", + "justify_content": "space-between", + "bg": rx.color_mode_cond( + "rgba(255, 255, 255, 0.81)", + "rgba(18, 17, 19, 0.81)", + ), + "align_items": "center", + "border_bottom": "1px solid rgba(46, 46, 46, 0.51)", +} + + +def render_navbar(): + return rx.hstack( + rx.hstack( + rx.box( + rx.text( + "REST API Admin Panel", + font_size="var(--chakra-fontSizes-lg)", + **text, + ), + ), + display="flex", + align_items="center", + ), + rx.hstack( + rx.button( + BaseState.is_request, on_click=BaseState.toggle_query, cursor="pointer" + ), + rx.color_mode.button(), + align_items="center", + ), + **navbar, + ) diff --git a/integration/loadtesting/sandbox/components/output.py b/integration/loadtesting/sandbox/components/output.py new file mode 100644 index 000000000..350bb314e --- /dev/null +++ b/integration/loadtesting/sandbox/components/output.py @@ -0,0 +1,93 @@ +import reflex as rx + +from examples.states.queries import QueryAPI +from examples.components.drawer import render_drawer + + +def create_table_header(title: str): + return rx.table.column_header_cell(title) + + +def create_query_rows(data: dict[str, str]): + def fill_rows_with_data(data_): + return rx.table.cell( + f"{data_[1]}", + on_click=QueryAPI.display_selected_row(data), + cursor="pointer", + ) + + return rx.table.row( + rx.foreach(data, fill_rows_with_data), + _hover={"bg": rx.color(color="gray", shade=4)}, + ) + + +def create_pagination(): + return rx.hstack( + rx.hstack( + rx.text("Entries per page", weight="bold"), + rx.select( + QueryAPI.limits, default_value="10", on_change=QueryAPI.delta_limit + ), + align_items="center", + ), + rx.hstack( + rx.text( + f"Page {QueryAPI.current_page}/{QueryAPI.total_pages}", + width="100px", + weight="bold", + ), + rx.chakra.button_group( + rx.icon( + tag="chevron-left", on_click=QueryAPI.previous, cursor="pointer" + ), + rx.icon(tag="chevron-right", on_click=QueryAPI.next, cursor="pointer"), + is_attached=True, + ), + align_items="center", + spacing="1", + ), + align_items="center", + spacing="4", + ) + + +def render_output(): + return rx.center( + rx.cond( + QueryAPI.get_data, + rx.vstack( + render_drawer(), + create_pagination(), + rx.table.root( + rx.table.header( + rx.table.row( + rx.foreach(QueryAPI.get_table_headers, create_table_header) + ), + ), + rx.table.body( + rx.foreach(QueryAPI.paginated_data, create_query_rows) + ), + width="100%", + variant="surface", + size="1", + ), + rx.text( + "* Click a row to edit its contents.", + weight="bold", + size="1", + ), + width="100%", + overflow="auto", + padding="2em 2em", + ), + rx.spacer(), + ), + flex="60%", + bg=rx.color_mode_cond( + "#faf9fb", + "#1a181a", + ), + border_radius="10px", + overflow="auto", + ) diff --git a/integration/loadtesting/sandbox/components/query.py b/integration/loadtesting/sandbox/components/query.py new file mode 100644 index 000000000..61fd18997 --- /dev/null +++ b/integration/loadtesting/sandbox/components/query.py @@ -0,0 +1,213 @@ +import reflex as rx + +from examples.states.base import BaseState +from examples.states.queries import QueryState, QueryAPI +from examples.styles import text + + +def item_title(title: str): + return rx.hstack( + rx.text(title, font_size="var(--chakra-fontSizes-sm)", **text), + rx.chakra.accordion_icon(), + width="100%", + justify_content="space-between", + ) + + +def item_add_event(event_trigger: callable): + return rx.button( + rx.hstack( + rx.text("+"), + rx.text("add", weight="bold"), + width="100%", + justify_content="space-between", + ), + size="1", + on_click=event_trigger, + padding="0.35em 0.75em", + cursor="pointer", + color_scheme="gray", + ) + + +def form_item_entry(data: dict[str, str]): + + def create_entry(title: str, function: callable): + return ( + rx.input( + placeholder=title, + width="100%", + on_change=function, + variant="surface", + ), + ) + + return rx.hstack( + create_entry("key", lambda key: QueryState.update_keyy(key, data)), + create_entry("value", lambda value: QueryState.update_value(value, data)), + rx.button( + "DEL", + on_click=QueryState.remove_entry(data), + color_scheme="ruby", + variant="surface", + cursor="pointer", + ), + width="100%", + spacing="1", + ) + + +def form_item( + title: str, state: list[dict[str, str]], func: callable, event_trigger: callable +): + return rx.chakra.accordion( + rx.chakra.accordion_item( + rx.chakra.accordion_button(item_title(title)), + rx.chakra.accordion_panel( + item_add_event(event_trigger), + width="100%", + display="flex", + justify_content="end", + ), + rx.chakra.accordion_panel( + rx.vstack(rx.foreach(state, func), width="100%", spacing="1") + ), + ), + allow_toggle=True, + width="100%", + border="transparent", + ) + + +def form_body_param_item( + state: list[dict[str, str]], func: callable, event_trigger: callable +): + return rx.chakra.accordion( + rx.chakra.accordion_item( + rx.chakra.accordion_button(item_title("Body")), + rx.chakra.accordion_panel( + rx.match( + QueryState.current_req, + ( + "GET", + rx.select( + QueryState.get_params_body, + default_value="None", + width="100%", + ), + ), + ( + "POST", + rx.vstack( + rx.hstack( + item_add_event(event_trigger), + width="100%", + justify_content="end", + ), + rx.select( + QueryState.post_params_body, + default_value="JSON", + width="100%", + ), + rx.vstack( + rx.foreach(state, func), width="100%", spacing="1" + ), + width="100%", + ), + ), + ), + ), + ), + allow_toggle=True, + width="100%", + border="transparent", + ) + + +def form_request_item(): + return rx.chakra.accordion( + rx.chakra.accordion_item( + rx.chakra.accordion_button(item_title("Requests")), + rx.chakra.accordion_panel( + rx.hstack( + rx.select( + QueryState.req_methods, + width="120px", + default_value="GET", + on_change=QueryState.get_request, + ), + rx.input( + value=QueryState.req_url, + width="100%", + on_change=QueryState.set_req_url, + placeholder="https://example_site.com/api/v2/endpoint.json", + ), + width="100%", + ) + ), + ), + allow_toggle=True, + width="100%", + border="transparent", + ) + + +def render_query_form(): + return rx.vstack( + # form_request_item(), + form_item( + "Headers", QueryState.headers, form_item_entry, QueryState.add_header + ), + form_body_param_item(QueryState.body, form_item_entry, QueryState.add_body), + form_item( + "Cookies", QueryState.cookies, form_item_entry, QueryState.add_cookies + ), + width="100%", + spacing="2", + padding="0em 0.75em", + ) + + +def render_query_header(): + return rx.hstack( + rx.hstack( + rx.select( + QueryState.req_methods, + width="100px", + default_value="GET", + on_change=QueryState.get_request, + ), + rx.input( + value=QueryState.req_url, + width="100%", + on_change=QueryState.set_req_url, + placeholder="https://example_site.com/api/v2/endpoint.json", + ), + width="100%", + spacing="1", + ), + rx.button( + "Send", size="2", on_click=QueryAPI.run_get_request, cursor="pointer" + ), + width="100%", + border_bottom=rx.color_mode_cond( + "1px solid rgba(45, 45, 45, 0.05)", "1px solid rgba(45, 45, 45, 0.51)" + ), + padding="1em 0.75em", + justify_content="end", + ) + + +def render_query_component(): + return rx.vstack( + render_query_header(), + render_query_form(), + flex=["100%", "100%", "100%", "100%", "30%"], + display=BaseState.query_component_toggle, + padding_bottom="0.75em", + border_radius="10px", + bg=rx.color_mode_cond( + "#faf9fb", + "#1a181a", + ), + ) diff --git a/integration/loadtesting/sandbox/examples.py b/integration/loadtesting/sandbox/examples.py new file mode 100644 index 000000000..0ae6ef324 --- /dev/null +++ b/integration/loadtesting/sandbox/examples.py @@ -0,0 +1,9 @@ +import reflex as rx +from examples.pages import * + + +class State(rx.State): + """Define empty state to allow access to rx.State.router.""" + + +app = rx.App() diff --git a/integration/loadtesting/sandbox/pages/__init__.py b/integration/loadtesting/sandbox/pages/__init__.py new file mode 100644 index 000000000..093f101ae --- /dev/null +++ b/integration/loadtesting/sandbox/pages/__init__.py @@ -0,0 +1 @@ +from .dashboard import dashboard diff --git a/integration/loadtesting/sandbox/pages/dashboard.py b/integration/loadtesting/sandbox/pages/dashboard.py new file mode 100644 index 000000000..cddcc5ecd --- /dev/null +++ b/integration/loadtesting/sandbox/pages/dashboard.py @@ -0,0 +1,30 @@ +"""The dashboard page.""" + +import reflex as rx + +from examples.components.navbar import render_navbar +from examples.components.query import render_query_component +from examples.components.output import render_output +from examples.states.queries import QueryAPI + + +@rx.page("/", on_load=QueryAPI.run_get_request) +def dashboard() -> rx.Component: + """The dashboard page. + + Returns: + The UI for the dashboard page. + """ + return rx.vstack( + render_navbar(), + rx.hstack( + render_query_component(), + render_output(), + width="100%", + display="flex", + flex_wrap="wrap", + spacing="6", + padding="2em 1em", + ), + spacing="4", + ) diff --git a/integration/loadtesting/sandbox/states/__init__.py b/integration/loadtesting/sandbox/states/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration/loadtesting/sandbox/states/base.py b/integration/loadtesting/sandbox/states/base.py new file mode 100644 index 000000000..31addfa1c --- /dev/null +++ b/integration/loadtesting/sandbox/states/base.py @@ -0,0 +1,17 @@ +import reflex as rx + + +class BaseState(rx.State): + + query_component_toggle: str = "none" + + is_request: str = "New Request" + + def toggle_query(self): + self.query_component_toggle = ( + "flex" if self.query_component_toggle == "none" else "none" + ) + + self.is_request = ( + "New Request" if self.query_component_toggle == "none" else "Close Request" + ) diff --git a/integration/loadtesting/sandbox/states/queries.py b/integration/loadtesting/sandbox/states/queries.py new file mode 100644 index 000000000..f7f3bec98 --- /dev/null +++ b/integration/loadtesting/sandbox/states/queries.py @@ -0,0 +1,180 @@ +import uuid +import httpx +from examples.states.base import BaseState + + +# test URL: str = https://jsonplaceholder.typicode.com/posts +# test URL: str = https://jsonplaceholder.typicode.com/todos + + +class QueryState(BaseState): + + # vars for handling request calls ... + req_methods: list[str] = ["GET", "POST"] + req_url: str = "https://jsonplaceholder.typicode.com/posts" + current_req: str = "GET" + get_params_body: list[str] = ["JSON", "Raw", "None"] + post_params_body: list[str] = [ + "JSON", + "Raw", + "x-www-form-urlencoded", + "Form Data", + "Binary", + "None", + ] + + # params. for triggering API calls ... + url_param: dict[str, str] + headers: list[dict[str, str]] + body: list[dict[str, str]] + cookies: list[dict[str, str]] + + # vars for GET request ... + get_data: list[dict[str, str]] + get_table_headers: list[str] + paginated_data: list[dict[str, str]] + + # vars for pagination ... + number_of_rows: int + limits: list[str] = ["10", "20", "50"] + current_limit: int = 10 + offset: int = 0 + current_page: int = 1 + total_pages: int = 1 + formatted_headers: dict + + def get_request(self, method: str): + self.current_req = method + + def add_header(self): + self.headers.append( + {"id": str(uuid.uuid4()), "identifier": "headers", "key": "", "value": ""} + ) + + def add_body(self): + self.body.append( + {"id": str(uuid.uuid4()), "identifier": "body", "key": "", "value": ""} + ) + + def add_cookies(self): + self.cookies.append( + {"id": str(uuid.uuid4()), "identifier": "cookies", "key": "", "value": ""} + ) + + def pure(self): + return + + def remove_entry(self, data: dict[str, str]): + if data["identifier"] == "headers": + self.headers = [item for item in self.headers if item["id"] != data["id"]] + + if data["identifier"] == "body": + self.body = [item for item in self.body if item["id"] != data["id"]] + + if data["identifier"] == "cookies": + self.cookies = [item for item in self.cookies if item["id"] != data["id"]] + + async def update_attribute(self, data: dict[str, str], attribute: str, value: str): + data[attribute] = value + + if data["identifier"] == "headers": + self.headers = [ + data if item["id"] == data["id"] else item for item in self.headers + ] + + if data["identifier"] == "body": + self.body = [ + data if item["id"] == data["id"] else item for item in self.body + ] + + if data["identifier"] == "cookies": + self.cookies = [ + data if item["id"] == data["id"] else item for item in self.cookies + ] + + async def update_keyy(self, key: str, data: dict[str, str]): + await self.update_attribute(data, "key", key) + + async def update_value(self, value: str, data: dict[str, str]): + await self.update_attribute(data, "value", value) + + +class QueryAPI(QueryState): + + # vars to update row entries ... + is_open: bool = False + selected_entry: dict[str, str] + original_entry: dict[str, str] + + async def process_headers(self): + for item in self.headers: + if item["key"]: + self.formatted_headers[item["key"]] = item["value"] + + async def run_get_request(self): + await self.process_headers() + async with httpx.AsyncClient() as client: + res = await client.get(self.req_url, headers=self.formatted_headers) + + self.get_data = res.json() + self.number_of_rows = len(self.get_data) + self.get_table_headers = list(self.get_data[0].keys()) + + # Calculate the total number of pages + self.total_pages = ( + self.number_of_rows + self.current_limit - 1 + ) // self.current_limit + + # Initialize the data to the first page + self.paginate() + + def paginate(self): + start = self.offset + end = start + self.current_limit + self.paginated_data = self.get_data[start:end] + self.current_page = (self.offset // self.current_limit) + 1 + + def delta_limit(self, limit: str): + self.current_limit = int(limit) + self.offset = 0 + self.total_pages = ( + self.number_of_rows + self.current_limit - 1 + ) // self.current_limit + self.paginate() + + def pure(self): + return + + def previous(self): + if self.offset >= self.current_limit: + self.offset -= self.current_limit + else: + self.offset = 0 + + self.paginate() + + def next(self): + if self.offset + self.current_limit < self.number_of_rows: + self.offset += self.current_limit + + self.paginate() + + def delta_drawer(self): + self.is_open = not self.is_open + + def display_selected_row(self, data: dict[str, str]): + self.delta_drawer() + self.selected_entry = data.copy() + self.original_entry = data + + def update_data(self, value: str, data: tuple[str, str]): + self.selected_entry[data[0]] = value + + def commit_changes(self): + self.get_data = [ + self.selected_entry if item == self.original_entry else item + for item in self.get_data + ] + + self.paginate() + self.delta_drawer() diff --git a/integration/loadtesting/sandbox/styles.py b/integration/loadtesting/sandbox/styles.py new file mode 100644 index 000000000..9f1acb813 --- /dev/null +++ b/integration/loadtesting/sandbox/styles.py @@ -0,0 +1,6 @@ +"""Styles for the app.""" + +text: dict[str, str] = { + "font_family": "var(--chakra-fonts-branding)", + "font_weight": "var(--chakra-fontWeights-black)", +} From 8e5fd6e150525a6378955bc6303aaaa106e4c13f Mon Sep 17 00:00:00 2001 From: Suchith Krishna S Donni Date: Tue, 3 Sep 2024 03:12:46 +0530 Subject: [PATCH 2/2] added sandbox application and basic loadtests --- integration/loadtesting/.gitignore | 7 +++++-- integration/loadtesting/rxconfig.py | 2 +- integration/loadtesting/sandbox/components/drawer.py | 2 +- integration/loadtesting/sandbox/components/navbar.py | 4 ++-- integration/loadtesting/sandbox/components/output.py | 4 ++-- integration/loadtesting/sandbox/components/query.py | 6 +++--- integration/loadtesting/sandbox/pages/dashboard.py | 8 ++++---- .../loadtesting/sandbox/{examples.py => sandbox.py} | 2 +- integration/loadtesting/sandbox/states/queries.py | 2 +- 9 files changed, 20 insertions(+), 17 deletions(-) rename integration/loadtesting/sandbox/{examples.py => sandbox.py} (81%) diff --git a/integration/loadtesting/.gitignore b/integration/loadtesting/.gitignore index 9c87ebe13..33cc43fc4 100644 --- a/integration/loadtesting/.gitignore +++ b/integration/loadtesting/.gitignore @@ -1,2 +1,5 @@ -./env -.web \ No newline at end of file +*.db +*.py[cod] +.web +__pycache__/ +env/ \ No newline at end of file diff --git a/integration/loadtesting/rxconfig.py b/integration/loadtesting/rxconfig.py index 6e06f3229..96ca2d582 100644 --- a/integration/loadtesting/rxconfig.py +++ b/integration/loadtesting/rxconfig.py @@ -1,5 +1,5 @@ import reflex as rx config = rx.Config( - app_name="examples", + app_name="sandbox", ) \ No newline at end of file diff --git a/integration/loadtesting/sandbox/components/drawer.py b/integration/loadtesting/sandbox/components/drawer.py index 2487c3d23..b0f10754f 100644 --- a/integration/loadtesting/sandbox/components/drawer.py +++ b/integration/loadtesting/sandbox/components/drawer.py @@ -1,6 +1,6 @@ import reflex as rx -from examples.states.queries import QueryAPI +from sandbox.states.queries import QueryAPI def render_data(data: tuple[str, str]): diff --git a/integration/loadtesting/sandbox/components/navbar.py b/integration/loadtesting/sandbox/components/navbar.py index 4f632caac..3a05db603 100644 --- a/integration/loadtesting/sandbox/components/navbar.py +++ b/integration/loadtesting/sandbox/components/navbar.py @@ -1,7 +1,7 @@ import reflex as rx -from examples.states.base import BaseState -from examples.styles import text +from sandbox.states.base import BaseState +from sandbox.styles import text navbar: dict[str, str] = { "width": "100%", diff --git a/integration/loadtesting/sandbox/components/output.py b/integration/loadtesting/sandbox/components/output.py index 350bb314e..d7be6577a 100644 --- a/integration/loadtesting/sandbox/components/output.py +++ b/integration/loadtesting/sandbox/components/output.py @@ -1,7 +1,7 @@ import reflex as rx -from examples.states.queries import QueryAPI -from examples.components.drawer import render_drawer +from sandbox.states.queries import QueryAPI +from sandbox.components.drawer import render_drawer def create_table_header(title: str): diff --git a/integration/loadtesting/sandbox/components/query.py b/integration/loadtesting/sandbox/components/query.py index 61fd18997..5333b93ca 100644 --- a/integration/loadtesting/sandbox/components/query.py +++ b/integration/loadtesting/sandbox/components/query.py @@ -1,8 +1,8 @@ import reflex as rx -from examples.states.base import BaseState -from examples.states.queries import QueryState, QueryAPI -from examples.styles import text +from sandbox.states.base import BaseState +from sandbox.states.queries import QueryState, QueryAPI +from sandbox.styles import text def item_title(title: str): diff --git a/integration/loadtesting/sandbox/pages/dashboard.py b/integration/loadtesting/sandbox/pages/dashboard.py index cddcc5ecd..7a64752c0 100644 --- a/integration/loadtesting/sandbox/pages/dashboard.py +++ b/integration/loadtesting/sandbox/pages/dashboard.py @@ -2,10 +2,10 @@ import reflex as rx -from examples.components.navbar import render_navbar -from examples.components.query import render_query_component -from examples.components.output import render_output -from examples.states.queries import QueryAPI +from sandbox.components.navbar import render_navbar +from sandbox.components.query import render_query_component +from sandbox.components.output import render_output +from sandbox.states.queries import QueryAPI @rx.page("/", on_load=QueryAPI.run_get_request) diff --git a/integration/loadtesting/sandbox/examples.py b/integration/loadtesting/sandbox/sandbox.py similarity index 81% rename from integration/loadtesting/sandbox/examples.py rename to integration/loadtesting/sandbox/sandbox.py index 0ae6ef324..82faa906a 100644 --- a/integration/loadtesting/sandbox/examples.py +++ b/integration/loadtesting/sandbox/sandbox.py @@ -1,5 +1,5 @@ import reflex as rx -from examples.pages import * +from sandbox.pages import * class State(rx.State): diff --git a/integration/loadtesting/sandbox/states/queries.py b/integration/loadtesting/sandbox/states/queries.py index f7f3bec98..ac47f933f 100644 --- a/integration/loadtesting/sandbox/states/queries.py +++ b/integration/loadtesting/sandbox/states/queries.py @@ -1,6 +1,6 @@ import uuid import httpx -from examples.states.base import BaseState +from sandbox.states.base import BaseState # test URL: str = https://jsonplaceholder.typicode.com/posts