Monitor App
Before diving into this code, here's a quick heads-up on what you'll need to be familiar with:
- Python Programming: It's important to have a good grasp of Python, especially with concepts
like
functions
,loops
, andclasses
, since the example utilizes these fundamentals. - Asynchronous Programming with asyncio: Familiarity with Python's asyncio for writing concurrent
code using the
async/await
syntax. - HTML/CSS: Knowledge of HTML and CSS for creating and styling web pages.
- JavaScript/TypeScript: Understanding of JavaScript and TypeScript for writing type-safe code.
Building an App: Understanding the Basics​
Every modern app typically consists of two main parts: the backend and the frontend. Let's dive into what each of these components does and how they interact with each other.
Backend:
The backend is like the brain of your app. It processes data, makes calculations, communicates with databases, and performs all the logical operations. When you hear terms like "server," "API," or "database," they're usually related to the backend.
In our example, we're using FastAPI to build our backend. FastAPI is a modern, high-performance web framework for building APIs. Coupled with the farm-ng brain services, our backend will fetch and serve data efficiently and securely to the frontend.
Key Points:
- Handles data processing, storage, and retrieval.
- Communicates with other services and databases.
- Secures data and ensures only authorized users can access it.
Frontend:
The frontend is the part of the app users see and interact with. Think of it as the face of your app. It includes everything that you can touch, click, or interact with: buttons, images, text inputs, animations, and more.
For our frontend, we're using React. React is a popular JavaScript library for building user interfaces. It allows developers to create responsive and interactive UI components easily.
Key Points:
- Displays data fetched from the backend.
- Interacts with users, capturing their inputs and preferences.
- Updates in real-time, ensuring users always see the latest data.
How They Work Together​
- A user interacts with the frontend (e.g., clicks a button to fetch information).
- The frontend sends a request to the backend, asking for specific data.
- The backend processes the request, fetches the data (from databases, other services, or the farm-ng brain services in our case).
- Once the data is retrieved, the backend sends it back to the frontend.
- The frontend then displays this data to the user in a readable and interactive manner.
In the Monitor App example we will show how to create a simple web application to stream and monitor the data from the farm-ng brain services.
The tutorial is divided in two parts:
- Backend: we will create a backend using FastAPI to serve the data from the farm-ng brain services to a frontend.
- Frontend: we will create a frontend app using React.
Backend​
To create the backend we will use FastAPI to serve the data from the farm-ng brain services leveraging WebSockets to stream the data to the frontend.
In particular, in this part we will show how to create a couple of endpoints to discover the different services running in the brain and to subscribe to the events of a particular service and stream the data to the frontend using WebSockets.
We strongly recommend go through the FastAPI tutorials to get familiar with the framework. You can start here FastAPI Tutorial.
Topics discoverability​
The first thing we need to do is to be able to discover the different topics, for this we will
create the following endpoint /list_uris
that will return a list of the available topics.
@app.get("/list_uris")
async def list_uris() -> JSONResponse:
all_uris = {}
for service_name, client in clients.items():
# get the list of uris from the event service
uris: list[Uri] = []
try:
# NOTE: some services may not be available, so we need to handle the timeout
uris = await asyncio.wait_for(client.list_uris(), timeout=0.1)
except asyncio.TimeoutError:
continue
# convert the uris to a dict, where the key is the uri full path
# and the value is the uri proto as a json string
for uri in uris:
all_uris[f"{service_name}{uri.path}"] = json.loads(MessageToJson(uri))
return JSONResponse(content=all_uris, status_code=200)
Subscribing to the events​
The next end point we want to create is the one to subscribe to the events of a particular service.
For this, we will create the following endpoint /subscribe/{service_name}/{uri_path}
that will
take the service_name
and the uri_path
as parameters and with an async generator we will
stream the data to the frontend using WebSockets.
@app.websocket("/subscribe/{service_name}/{uri_path}")
async def subscribe(websocket: WebSocket, service_name: str, uri_path: str, every_n: int = 1):
client: EventClient = clients[service_name]
await websocket.accept()
async for event, message in client.subscribe(
request=SubscribeRequest(uri=Uri(path=f"/{uri_path}"), every_n=every_n), decode=True
):
await websocket.send_json(MessageToJson(message))
await websocket.close()
In this example we use the every_n
parameter to reduce the number of messages sent to the frontend.
We also send the message as a json string, but we could also send the message as a binary string
and decode the protobuf message in the frontend.
Frontend​
In this part we will create a simple frontend using React
with TypeScript and Vite as a bundler.
The code for the frontend is located in the monitor_app/ts
directory.
The structure of the frontend is the following: