Skip to content

Commit

Permalink
Merge pull request #2405 from andrewbaldwin44/feature/2396
Browse files Browse the repository at this point in the history
Add a modern web UI based on React, MaterialUI and Vite
  • Loading branch information
cyberw authored Oct 10, 2023
2 parents 17d020e + 804a19a commit 704a015
Show file tree
Hide file tree
Showing 86 changed files with 6,725 additions and 9 deletions.
18 changes: 18 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ jobs:
- run: python -m pip install tox
- run: python -m tox -e ${{ matrix.tox }}

lint_typecheck_webui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: borales/actions-yarn@v3
with:
cmd: webui:install
- uses: borales/actions-yarn@v3
with:
cmd: webui:lint
- uses: borales/actions-yarn@v3
with:
cmd: webui:type-check

verify_docker_build:
name: Always - Docker verify, push to tag 'master' if on master
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
FROM node:18.0.0-alpine as webui-builder

ADD locust/webui locust/webui
ADD package.json .

RUN yarn webui:install --production
RUN yarn webui:build

FROM python:3.11-slim as base

FROM base as builder
Expand All @@ -11,6 +19,7 @@ RUN pip install /build/

FROM base
COPY --from=builder /opt/venv /opt/venv
COPY --from=webui-builder locust/webui/dist locust/webui/dist
ENV PATH="/opt/venv/bin:$PATH"
# turn off python output buffering
ENV PYTHONUNBUFFERED=1
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ include LICENSE
include locust/py.typed
recursive-include locust/static *
recursive-include locust/templates *
recursive-include locust/webui/dist *
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ test:
build:
rm -f dist/* && python3 -m pip install --upgrade build && python3 -m build .

frontend_build:
yarn webui:install && yarn webui:build

release: build
twine upload dist/*

Expand Down
68 changes: 68 additions & 0 deletions docs/developing-locust.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,71 @@ Or you can make sass watch for changes to the ``.sass`` files and automatically
$ make sass_watch
The CSS files that are generated by SASS should be checked into version control.


Making changes to Locust's Modern Web UI
========================================

The modern Web UI is built using React and Typescript

## Setup

### Node

Install node using nvm to easily switch between node version

- Copy and run the install line from [nvm](https://github.com/nvm-sh/nvm) (starts with curl/wget ...)

- Verify nvm was installed correctly

```bash
nvm --version
```

- Install the proper Node version according to engines in the locust/webui/package.json

```bash
nvm install {version}
nvm alias default {version}
```

### Yarn

- Install Yarn from their official website (avoid installing through Node if possible)
- Verify yarn was installed correctly

```bash
yarn --version
```

- Next in web, install all dependencies

```bash
cd locust/webui
yarn
```

## Developing

To develop the frontend, run `yarn dev`. This will start the Vite dev server and allow for viewing and editing the frontend, without needing to a run a locust web server

To develop while running a locust instance, run `yarn dev:watch`. This will output the static files to the `dist` directory. Then simply a locust instance using the `--modern-ui` flag. Vite will automatically detect any changed files and re-build as needed. Simply refresh the page to view the changes

To compile the webui, run `yarn build`

The frontend can additionally be built using make:
```bash
make frontend_build
```

## Linting

Run `yarn lint` to detect lint failures in the frontend project. Running `yarn lint --fix` will resolve any issues that are automatically resolvable. Your IDE can aditionally be configured with ESLint to resolve these issues on save.

## Formatting

Run `yarn format` to fix any formatting issues in the frontend project. Once again your IDE can be configured to automatically format on save.

## Typechecking

We use Typescript in the frontend project. Run `yarn type-check` to find any issues.
Binary file added docs/images/modern-webui-splash-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ The following screenshots show what it might look like when running this test us

If you need some help digging into server side problems, or you're having trouble generating enough load to saturate your system, have a look at the `Locust FAQ <https://github.com/locustio/locust/wiki/FAQ#increase-my-request-raterps>`_.

There is now a modern version of the Web UI available! Try it out by setting the ``--modern-ui`` flag.

.. image:: images/modern-webui-splash-screenshot.png

.. note::

This feature is experimental and you may experience breaking changes.

Direct command line usage / headless
====================================

Expand Down Expand Up @@ -89,7 +97,7 @@ To run Locust distributed across multiple Python processes or machines, you star
with the ``--master`` command line parameter, and then any number of Locust worker processes using the ``--worker``
command line parameter. See :ref:`running-distributed` for more info.

To see all available options type: ``locust --help`` or check :ref:`configuration`.
To see all available options type: ```locust --help`` or check :ref:`configuration`.

Next steps
==========
Expand Down
163 changes: 163 additions & 0 deletions examples/extend_modern_web_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
This is an example of a locustfile that uses Locust's built in event and web
UI extension hooks to track the sum of the content-length header in all
successful HTTP responses and display them in the web UI.
"""

import os
import json
from time import time
from html import escape
from locust import HttpUser, TaskSet, task, web, between, events
from flask import Blueprint, render_template, jsonify, make_response, request


class MyTaskSet(TaskSet):
@task(2)
def index(l):
l.client.get("/")

@task(1)
def stats(l):
l.client.get("/stats/requests")


class WebsiteUser(HttpUser):
host = "http://127.0.0.1:8089"
wait_time = between(2, 5)
tasks = [MyTaskSet]


stats = {}
path = os.path.dirname(os.path.abspath(__file__))
extend = Blueprint(
"extend",
"extend_web_ui",
static_folder=f"{path}/static/",
static_url_path="/extend/static/",
template_folder=f"{path}/templates/",
)


@events.init.add_listener
def locust_init(environment, **kwargs):
"""
We need somewhere to store the stats.
On the master node stats will contain the aggregated sum of all content-lengths,
while on the worker nodes this will be the sum of the content-lengths since the
last stats report was sent to the master
"""
if environment.web_ui:

def get_content_length_stats():
"""
This is used by the Content Length tab in the
extended web UI to show the stats.
"""
if stats:
stats_tmp = []

for name, inner_stats in stats.items():
content_length = inner_stats["content-length"]

stats_tmp.append(
{"name": name, "safe_name": escape(name, quote=False), "content_length": content_length}
)

# Truncate the total number of stats and errors displayed since a large number of rows will cause the app
# to render extremely slowly.
return stats_tmp[:500]
return stats

@environment.web_ui.app.after_request
def extend_stats_response(response):
if request.path != "/stats/requests":
return response

response.set_data(
json.dumps(
{**response.json, "extended_stats": [{"key": "content-length", "data": get_content_length_stats()}]}
)
)

return response

@extend.route("/extend")
def extend_web_ui():
"""
Add route to access the extended web UI with our new tab.
"""
# ensure the template_args are up to date before using them
environment.web_ui.update_template_args()
# set the static paths to use the modern ui
environment.web_ui.set_static_modern_ui()

return render_template(
"index.html",
template_args={
**environment.web_ui.template_args,
"extended_tabs": [{"title": "Content Length", "key": "content-length"}],
"extended_tables": [
{
"key": "content-length",
"structure": [
{"key": "name", "title": "Name"},
{"key": "content_length", "title": "Total content length"},
],
}
],
"extended_csv_files": [
{"href": "/content-length/csv", "title": "Download content length statistics CSV"}
],
},
)

@extend.route("/content-length/csv")
def request_content_length_csv():
"""
Add route to enable downloading of content-length stats as CSV
"""
response = make_response(content_length_csv())
file_name = f"content_length{time()}.csv"
disposition = f"attachment;filename={file_name}"
response.headers["Content-type"] = "text/csv"
response.headers["Content-disposition"] = disposition
return response

def content_length_csv():
"""Returns the content-length stats as CSV."""
rows = [
",".join(
[
'"Name"',
'"Total content-length"',
]
)
]

if stats:
for url, inner_stats in stats.items():
rows.append(f"\"{url}\",{inner_stats['content-length']:.2f}")
return "\n".join(rows)

# register our new routes and extended UI with the Locust web UI
environment.web_ui.app.register_blueprint(extend)


@events.request.add_listener
def on_request(request_type, name, response_time, response_length, exception, context, **kwargs):
"""
Event handler that get triggered on every request
"""
stats.setdefault(name, {"content-length": 0})
stats[name]["content-length"] += response_length


@events.reset_stats.add_listener
def on_reset_stats():
"""
Event handler that get triggered on click of web UI Reset Stats button
"""
global stats
stats = {}
14 changes: 10 additions & 4 deletions locust/argument_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import platform
import sys
import textwrap
from typing import Dict, List, NamedTuple, Optional

from typing import Dict, List, NamedTuple, Optional, Any
import configargparse

import locust
Expand Down Expand Up @@ -359,6 +358,13 @@ def setup_parser_arguments(parser):
help="Enable select boxes in the web interface to choose from all available User classes and Shape classes",
env_var="LOCUST_USERCLASS_PICKER",
)
web_ui_group.add_argument(
"--modern-ui",
default=False,
action="store_true",
help="*Experimental* enable using a modern React frontend as the Web UI",
env_var="LOCUST_MODERN_UI",
)

master_group = parser.add_argument_group(
"Master options",
Expand Down Expand Up @@ -622,7 +628,7 @@ class UIExtraArgOptions(NamedTuple):
choices: Optional[List[str]] = None


def ui_extra_args_dict(args=None) -> Dict[str, UIExtraArgOptions]:
def ui_extra_args_dict(args=None) -> Dict[str, Dict[str, Any]]:
"""Get all the UI visible arguments"""
locust_args = default_args_dict()

Expand All @@ -635,7 +641,7 @@ def ui_extra_args_dict(args=None) -> Dict[str, UIExtraArgOptions]:
is_secret=k in parser.secret_args_included_in_web_ui,
help_text=parser.args_included_in_web_ui[k].help,
choices=parser.args_included_in_web_ui[k].choices,
)
)._asdict()
for k, v in all_args.items()
if k not in locust_args and k in parser.args_included_in_web_ui
}
Expand Down
2 changes: 2 additions & 0 deletions locust/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def create_web_ui(
stats_csv_writer: Optional[StatsCSV] = None,
delayed_start=False,
userclass_picker_is_active=False,
modern_ui=False,
) -> WebUI:
"""
Creates a :class:`WebUI <locust.web.WebUI>` instance for this Environment and start running the web server
Expand All @@ -191,6 +192,7 @@ def create_web_ui(
stats_csv_writer=stats_csv_writer,
delayed_start=delayed_start,
userclass_picker_is_active=userclass_picker_is_active,
modern_ui=modern_ui,
)
return self.web_ui

Expand Down
1 change: 1 addition & 0 deletions locust/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ def is_valid_percentile(parameter):
stats_csv_writer=stats_csv_writer,
delayed_start=True,
userclass_picker_is_active=options.class_picker,
modern_ui=options.modern_ui,
)
except AuthCredentialsError:
logger.error("Credentials supplied with --web-auth should have the format: username:password")
Expand Down
2 changes: 1 addition & 1 deletion locust/test/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def _(parser, **kw):
self.assertIn("a1", extra_args)
self.assertNotIn("a2", extra_args)
self.assertIn("a3", extra_args)
self.assertEqual("v1", extra_args["a1"].default_value)
self.assertEqual("v1", extra_args["a1"]["default_value"])


class TestFindLocustfiles(LocustTestCase):
Expand Down
Loading

0 comments on commit 704a015

Please sign in to comment.