Skip to content

Commit 1b9fcbe

Browse files
feat: add playground page and embed grpcui (#35)
1 parent 501d5cc commit 1b9fcbe

20 files changed

Lines changed: 1542 additions & 27 deletions

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
FROM node:19-alpine AS build
22

3+
ARG GRPCUI_URL=http://127.0.0.1:8081
4+
ENV GRPCUI_URL=$GRPCUI_URL
5+
36
WORKDIR /code
47

58
COPY ./package.json ./package.json

Dockerfile.grpcui

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Build grpcui from source
2+
FROM golang:1.24-alpine AS grpcui-build
3+
4+
RUN apk add --no-cache git make
5+
6+
RUN go install github.com/fullstorydev/grpcui/cmd/grpcui@aa3edefac370a32da4524893ffdca9c591ba0aeb
7+
8+
# Final runtime image
9+
FROM alpine:3.21
10+
11+
RUN apk add --no-cache ca-certificates
12+
13+
COPY --from=grpcui-build /go/bin/grpcui /usr/local/bin/grpcui
14+
COPY ./public/grpcui /app/static/
15+
16+
WORKDIR /app
17+
18+
ENV GRPC_TARGET=localhost:50051
19+
ENV GRPC_PORT=8081
20+
ENV USE_PLAINTEXT=false
21+
ENV API_KEY=""
22+
ENV API_KEY_HEADER="dmtr-api-key"
23+
ENV STATIC_DIR=/app/static
24+
ENV CSS_FILE=/app/static/grpcui.css
25+
ENV JS_FILE=/app/static/grpcui.js
26+
27+
EXPOSE 8081
28+
29+
CMD if [ "$USE_PLAINTEXT" = "true" ]; then \
30+
if [ -n "$API_KEY" ]; then \
31+
grpcui -plaintext -bind 0.0.0.0 -port $GRPC_PORT -also-serve $STATIC_DIR -extra-css $CSS_FILE -extra-js $JS_FILE -rpc-header "$API_KEY_HEADER:$API_KEY" $GRPC_TARGET; \
32+
else \
33+
grpcui -plaintext -bind 0.0.0.0 -port $GRPC_PORT -also-serve $STATIC_DIR -extra-css $CSS_FILE -extra-js $JS_FILE $GRPC_TARGET; \
34+
fi; \
35+
else \
36+
if [ -n "$API_KEY" ]; then \
37+
grpcui -bind 0.0.0.0 -port $GRPC_PORT -also-serve $STATIC_DIR -extra-css $CSS_FILE -extra-js $JS_FILE -rpc-header "$API_KEY_HEADER:$API_KEY" $GRPC_TARGET; \
38+
else \
39+
grpcui -bind 0.0.0.0 -port $GRPC_PORT -also-serve $STATIC_DIR -extra-css $CSS_FILE -extra-js $JS_FILE $GRPC_TARGET; \
40+
fi; \
41+
fi

INFRASTRUCTURE.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# UTxO RPC Documentation Infrastructure
2+
3+
This document describes the deployment architecture and infrastructure setup for the UTxO RPC documentation site.
4+
5+
## Architecture Overview
6+
7+
The application consists of two main services:
8+
9+
1. **Nextra Documentation Site** - The main documentation site built with Next.js and Nextra
10+
2. **gRPC UI Service** - An embedded gRPC web interface for interactive API exploration
11+
12+
## Services
13+
14+
### Nextra Documentation (`nextra`)
15+
- **Port**: 8080
16+
- **Framework**: Next.js with Nextra theme
17+
- **Purpose**: Serves the main documentation website
18+
- **Proxy**: Routes `/grpcui/*` requests to the gRPC UI service
19+
20+
### gRPC UI Service (`grpcui`)
21+
- **Port**: 8081
22+
- **Purpose**: Provides an interactive web UI for gRPC API exploration
23+
- **Integration**: Embedded as an iframe in the documentation site
24+
25+
## Docker Configuration
26+
27+
### Multi-Container Setup
28+
29+
The application uses Docker Compose to orchestrate both services:
30+
31+
```yaml
32+
services:
33+
nextra:
34+
build:
35+
context: .
36+
dockerfile: Dockerfile
37+
args:
38+
- GRPCUI_URL=http://grpcui:8081
39+
ports:
40+
- "8080:8080"
41+
environment:
42+
- PORT=8080
43+
44+
grpcui:
45+
build:
46+
context: .
47+
dockerfile: Dockerfile.grpcui
48+
ports:
49+
- "8081:8081"
50+
environment:
51+
- USE_PLAINTEXT=false
52+
- GRPC_TARGET=mainnet.utxorpc-v0.demeter.run:443
53+
- API_KEY=utxorpc13rfqzhrntg93wh7uwg8
54+
- API_KEY_HEADER=dmtr-api-key
55+
```
56+
57+
### Build Arguments & Environment Variables
58+
59+
#### Nextra Service
60+
- `GRPCUI_URL` (build arg): URL for proxying gRPC UI requests (configured at build time)
61+
- `PORT`: Port the Next.js server listens on
62+
63+
#### gRPC UI Service
64+
- `USE_PLAINTEXT`: Whether to use plaintext connection (false for SSL/TLS)
65+
- `GRPC_TARGET`: Target gRPC server endpoint
66+
- `API_KEY`: Authentication key for the gRPC service
67+
- `API_KEY_HEADER`: Header name for the API key (default: "dmtr-api-key")
68+
- `GRPC_PORT`: Port for the gRPC UI service (default: 8081)
69+
70+
## Deployment
71+
72+
### Local Development
73+
74+
1. **Start both services**:
75+
```bash
76+
docker-compose up --build
77+
```
78+
79+
2. **Access the application**:
80+
- Documentation: http://localhost:8080
81+
- gRPC UI (direct): http://localhost:8081
82+
- gRPC UI (embedded): http://localhost:8080/grpcui
83+
84+
### Production Deployment
85+
86+
For production deployments:
87+
88+
1. **Update environment variables** in `docker-compose.yml`:
89+
```yaml
90+
environment:
91+
- GRPC_TARGET=your-production-grpc-endpoint:443
92+
- API_KEY=your-production-api-key
93+
- API_KEY_HEADER=your-auth-header-name
94+
```
95+
96+
2. **Configure build arguments** for internal service communication:
97+
```yaml
98+
args:
99+
- GRPCUI_URL=http://grpcui:8081
100+
```
101+
102+
3. **Deploy with Docker Compose**:
103+
```bash
104+
docker-compose -f docker-compose.yml up -d
105+
```
106+
107+
## Version Pinning
108+
109+
The infrastructure uses pinned versions for reproducibility:
110+
111+
- **Go**: `golang:1.24-alpine`
112+
- **Alpine Linux**: `alpine:3.21`
113+
- **Node.js**: `node:19-alpine`
114+
- **gRPC UI**: Commit hash `aa3edefac370a32da4524893ffdca9c591ba0aeb`
115+
116+
## Network Configuration
117+
118+
### Internal Communication
119+
- Services communicate via Docker's internal networking
120+
- The `nextra` service proxies requests to `grpcui:8081`
121+
- gRPC UI binds to `0.0.0.0:8081` to accept connections from other containers
122+
123+
### External Access
124+
- **Port 8080**: Main documentation site
125+
- **Port 8081**: Direct access to gRPC UI (optional)
126+
127+
## Configuration Examples
128+
129+
### Different gRPC Endpoints
130+
131+
**Local Development (Plaintext)**:
132+
```yaml
133+
environment:
134+
- USE_PLAINTEXT=true
135+
- GRPC_TARGET=localhost:50051
136+
- API_KEY=""
137+
```
138+
139+
**Demeter Mainnet (SSL)**:
140+
```yaml
141+
environment:
142+
- USE_PLAINTEXT=false
143+
- GRPC_TARGET=mainnet.utxorpc-v0.demeter.run:443
144+
- API_KEY=utxorpc13rfqzhrntg93wh7uwg8
145+
- API_KEY_HEADER=dmtr-api-key
146+
```
147+
148+
**Custom Endpoint**:
149+
```yaml
150+
environment:
151+
- USE_PLAINTEXT=false
152+
- GRPC_TARGET=api.example.com:443
153+
- API_KEY=your-api-key
154+
- API_KEY_HEADER=authorization
155+
```
156+
157+
## Troubleshooting
158+
159+
### Common Issues
160+
161+
1. **Connection Refused Errors**:
162+
- Ensure gRPC UI service binds to `0.0.0.0` not `127.0.0.1`
163+
- Check Docker network connectivity between services
164+
165+
2. **Build-time vs Runtime Configuration**:
166+
- `GRPCUI_URL` must be set as a build argument for Next.js rewrites
167+
- gRPC connection settings can be runtime environment variables
168+
169+
3. **SSL/TLS Issues**:
170+
- Set `USE_PLAINTEXT=false` for encrypted endpoints
171+
- Ensure the target endpoint supports TLS
172+
173+
### Logs
174+
175+
View service logs:
176+
```bash
177+
# All services
178+
docker-compose logs
179+
180+
# Specific service
181+
docker-compose logs nextra
182+
docker-compose logs grpcui
183+
```
184+
185+
## Security Considerations
186+
187+
- API keys are passed as environment variables
188+
- Use Docker secrets for sensitive production deployments
189+
- The gRPC UI service only accepts connections from the documentation service
190+
- SSL/TLS is enforced for production gRPC endpoints

components/button.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Link from "next/link";
2+
import React from "react";
3+
4+
export function Button(props: { service: string; method: string }) {
5+
return (
6+
<div className="inline-flex ml-4 self-end">
7+
<Link
8+
href={{
9+
pathname: "/playground",
10+
query: { service: props.service, method: props.method },
11+
}}
12+
className="inline-flex items-center text-center gap-2 bg-[#00696D] hover:bg-[#27A0A1] active:bg-[#004C4E] transition-bg duration-150 text-white text-sm font-medium rounded-md border border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-800 py-1.5 px-2"
13+
>
14+
15+
Try it out
16+
<svg
17+
className="w-3 h-3"
18+
width="16"
19+
height="16"
20+
viewBox="0 0 16 16"
21+
fill="none"
22+
>
23+
<path
24+
d="M5.27921 2L10.9257 7.64645C11.1209 7.84171 11.1209 8.15829
25+
10.9257 8.35355L5.27921 14"
26+
stroke="currentColor"
27+
strokeWidth="2"
28+
strokeLinecap="round"
29+
/>
30+
</svg>
31+
</Link>
32+
</div>
33+
);
34+
}

components/playground_runner.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useState, useRef, useEffect, useCallback } from "react";
2+
import { useRouter } from "next/router";
3+
import React from "react";
4+
5+
type GrpcUiMessageData = {
6+
type: "grpcui-ready" | "grpc-is-ready";
7+
};
8+
9+
export default function PlaygroundRunner() {
10+
const { query } = useRouter();
11+
const [grpcUiIsReady, setGrpcUiIsReady] = useState(false);
12+
const [loaded, setLoaded] = useState(false);
13+
const iframeRef = useRef<HTMLIFrameElement>(null);
14+
15+
const service = query.service as string | undefined;
16+
const method = query.method as string | undefined;
17+
18+
// On Component Initialize
19+
useEffect(() => {
20+
function onMessage(e: MessageEvent<GrpcUiMessageData>) {
21+
if (e.data.type === "grpcui-ready") {
22+
setGrpcUiIsReady(true);
23+
clearInterval(intervalId);
24+
}
25+
}
26+
window.addEventListener("message", onMessage);
27+
28+
// Send a message to the iframe to check if it's ready
29+
const intervalId = setInterval(() => {
30+
if (grpcUiIsReady) return;
31+
iframeRef.current?.contentWindow?.postMessage(
32+
{ type: "grpc-is-ready" } as GrpcUiMessageData,
33+
window.location.origin
34+
);
35+
}, 100);
36+
37+
// Cleanup
38+
return () => window.removeEventListener("message", onMessage);
39+
}, []);
40+
41+
useEffect(() => {
42+
if (!grpcUiIsReady || service == undefined || method == undefined) return;
43+
44+
const iframeContentWindow = iframeRef.current?.contentWindow;
45+
if (!iframeContentWindow) return;
46+
47+
iframeContentWindow.postMessage(
48+
{ type: "grpc-select", service, method },
49+
window.location.origin
50+
);
51+
// Remove the loading UI
52+
}, [grpcUiIsReady, service, method]);
53+
54+
useEffect(() => {
55+
if (grpcUiIsReady) {
56+
setLoaded(true);
57+
}
58+
}, [grpcUiIsReady]);
59+
60+
return (
61+
<div className="relative h-[160vh] rounded-lg overflow-hidden">
62+
{!loaded && (
63+
<div className="absolute inset-0 bg-white dark:bg-[#111111] pt-6">
64+
<div className="flex flex-col gap-4">
65+
<div className="w-[400px] h-10 animate-pulse rounded-full bg-[#1F2929]" />
66+
<div className="w-[400px] h-10 animate-pulse rounded-full bg-[#1F2929]" />
67+
<div className="w-full h-56 rounded-xl animate-pulse rounded-2xl bg-[#1F2929]" />
68+
</div>
69+
<div className="mt-4">
70+
<div className="flex gap-2">
71+
<div className="bg-[#1F2929] w-20 h-7 rounded-full animate-pulse" />
72+
<div className="bg-[#1F2929] w-20 h-7 rounded-full animate-pulse" />
73+
<div className="bg-[#1F2929] w-20 h-7 rounded-full animate-pulse" />
74+
<div className="bg-[#1F2929] w-20 h-7 rounded-full animate-pulse" />
75+
</div>
76+
<div className="bg-[#1F2929] w-full h-[147px] rounded-2xl animate-pulse mt-2" />
77+
<div className="bg-[#1F2929] w-full h-[200px] rounded-2xl animate-pulse mt-10" />
78+
</div>
79+
</div>
80+
)}
81+
82+
<iframe
83+
ref={iframeRef}
84+
src="/grpcui/"
85+
title={`gRPC Playground — ${service ?? "-"}.${method ?? "-"}`}
86+
className="w-full h-full border-0"
87+
/>
88+
</div>
89+
);
90+
}

docker-compose.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
services:
2+
nextra:
3+
build:
4+
context: .
5+
dockerfile: Dockerfile
6+
args:
7+
- GRPCUI_URL=http://grpcui:8081
8+
ports:
9+
- "8080:8080"
10+
environment:
11+
- PORT=8080
12+
container_name: utxorpc-docs
13+
14+
grpcui:
15+
build:
16+
context: .
17+
dockerfile: Dockerfile.grpcui
18+
ports:
19+
- "8081:8081"
20+
environment:
21+
- USE_PLAINTEXT=false
22+
- GRPC_TARGET=mainnet.utxorpc-v0.demeter.run:443
23+
- GRPC_PORT=8081
24+
- API_KEY=utxorpc13rfqzhrntg93wh7uwg8
25+
- API_KEY_HEADER=dmtr-api-key
26+
container_name: utxorpc-grpcui

0 commit comments

Comments
 (0)