Merge 8e5fd6e150
into 98f50811f9
This commit is contained in:
commit
4863e42c7c
5
integration/loadtesting/.gitignore
vendored
Normal file
5
integration/loadtesting/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
*.db
|
||||
*.py[cod]
|
||||
.web
|
||||
__pycache__/
|
||||
env/
|
41
integration/loadtesting/README.md
Normal file
41
integration/loadtesting/README.md
Normal file
@ -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.
|
BIN
integration/loadtesting/assets/favicon.ico
Normal file
BIN
integration/loadtesting/assets/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
10
integration/loadtesting/assets/github.svg
Normal file
10
integration/loadtesting/assets/github.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<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>
|
After Width: | Height: | Size: 1.4 KiB |
239
integration/loadtesting/locustfile.py
Normal file
239
integration/loadtesting/locustfile.py
Normal file
@ -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}}"]
|
5
integration/loadtesting/rxconfig.py
Normal file
5
integration/loadtesting/rxconfig.py
Normal file
@ -0,0 +1,5 @@
|
||||
import reflex as rx
|
||||
|
||||
config = rx.Config(
|
||||
app_name="sandbox",
|
||||
)
|
1
integration/loadtesting/sandbox/__init__.py
Normal file
1
integration/loadtesting/sandbox/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Base template for Reflex."""
|
60
integration/loadtesting/sandbox/components/drawer.py
Normal file
60
integration/loadtesting/sandbox/components/drawer.py
Normal file
@ -0,0 +1,60 @@
|
||||
import reflex as rx
|
||||
|
||||
from sandbox.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,
|
||||
)
|
40
integration/loadtesting/sandbox/components/navbar.py
Normal file
40
integration/loadtesting/sandbox/components/navbar.py
Normal file
@ -0,0 +1,40 @@
|
||||
import reflex as rx
|
||||
|
||||
from sandbox.states.base import BaseState
|
||||
from sandbox.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,
|
||||
)
|
93
integration/loadtesting/sandbox/components/output.py
Normal file
93
integration/loadtesting/sandbox/components/output.py
Normal file
@ -0,0 +1,93 @@
|
||||
import reflex as rx
|
||||
|
||||
from sandbox.states.queries import QueryAPI
|
||||
from sandbox.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",
|
||||
)
|
213
integration/loadtesting/sandbox/components/query.py
Normal file
213
integration/loadtesting/sandbox/components/query.py
Normal file
@ -0,0 +1,213 @@
|
||||
import reflex as rx
|
||||
|
||||
from sandbox.states.base import BaseState
|
||||
from sandbox.states.queries import QueryState, QueryAPI
|
||||
from sandbox.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",
|
||||
),
|
||||
)
|
1
integration/loadtesting/sandbox/pages/__init__.py
Normal file
1
integration/loadtesting/sandbox/pages/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .dashboard import dashboard
|
30
integration/loadtesting/sandbox/pages/dashboard.py
Normal file
30
integration/loadtesting/sandbox/pages/dashboard.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""The dashboard page."""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
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)
|
||||
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",
|
||||
)
|
9
integration/loadtesting/sandbox/sandbox.py
Normal file
9
integration/loadtesting/sandbox/sandbox.py
Normal file
@ -0,0 +1,9 @@
|
||||
import reflex as rx
|
||||
from sandbox.pages import *
|
||||
|
||||
|
||||
class State(rx.State):
|
||||
"""Define empty state to allow access to rx.State.router."""
|
||||
|
||||
|
||||
app = rx.App()
|
0
integration/loadtesting/sandbox/states/__init__.py
Normal file
0
integration/loadtesting/sandbox/states/__init__.py
Normal file
17
integration/loadtesting/sandbox/states/base.py
Normal file
17
integration/loadtesting/sandbox/states/base.py
Normal file
@ -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"
|
||||
)
|
180
integration/loadtesting/sandbox/states/queries.py
Normal file
180
integration/loadtesting/sandbox/states/queries.py
Normal file
@ -0,0 +1,180 @@
|
||||
import uuid
|
||||
import httpx
|
||||
from sandbox.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()
|
6
integration/loadtesting/sandbox/styles.py
Normal file
6
integration/loadtesting/sandbox/styles.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Styles for the app."""
|
||||
|
||||
text: dict[str, str] = {
|
||||
"font_family": "var(--chakra-fonts-branding)",
|
||||
"font_weight": "var(--chakra-fontWeights-black)",
|
||||
}
|
Loading…
Reference in New Issue
Block a user