Skip to content

Commit 9c3ef91

Browse files
0xmadmichiosw
authored andcommitted
feat(web): add block components
- [x] Add servers list - [x] Add server form - [x] Add middleware for trailing slash - [x] Add link component
1 parent 3d4c460 commit 9c3ef91

File tree

11 files changed

+286
-30
lines changed

11 files changed

+286
-30
lines changed

internal/web/domain/server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package domain
2+
3+
type ServerItem struct {
4+
Name string
5+
IP string
6+
}

internal/web/handlers/ui.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,45 @@ package handlers
44
import (
55
"net/http"
66

7-
"github.com/co-browser/agent-browser/internal/backend"
8-
"github.com/co-browser/agent-browser/internal/backend/models"
97
"github.com/co-browser/agent-browser/internal/log"
108
"github.com/co-browser/agent-browser/internal/web/templates"
119
)
1210

1311
// UIHandler holds dependencies for UI handlers.
1412
type UIHandler struct {
1513
log log.Logger
16-
bs backend.Service // Add backend service dependency
1714
}
1815

1916
// NewUIHandler creates a handler for serving the main UI page.
20-
// It now requires a logger and the backend service.
21-
func NewUIHandler(logger log.Logger, bs backend.Service) http.Handler {
22-
h := &UIHandler{
23-
log: logger,
24-
bs: bs, // Store backend service
25-
}
26-
return http.HandlerFunc(h.serveIndex)
17+
// It now requires a logger.
18+
func NewUIHandler(logger log.Logger) http.Handler {
19+
h := &UIHandler{log: logger}
20+
mux := http.NewServeMux()
21+
22+
mux.HandleFunc("/ui", h.serveIndex)
23+
mux.HandleFunc("/ui/add", h.serveAddPage)
24+
25+
return mux
2726
}
2827

2928
// serveIndex handles requests for the main UI page.
30-
// It fetches the initial server list from the backend service.
3129
func (h *UIHandler) serveIndex(w http.ResponseWriter, r *http.Request) {
32-
// Fetch initial list of servers
33-
servers, err := h.bs.ListMCPServers()
34-
if err != nil {
35-
h.log.Error().Err(err).Msg("failed to fetch initial server list for UI")
36-
// Proceed with rendering an empty list or show an error message
37-
servers = []models.MCPServer{} // Initialize empty slice to prevent nil panic in template
38-
// Optionally: render an error message instead of the list
39-
// http.Error(w, "failed to load server list", http.StatusInternalServerError)
40-
// return
41-
}
42-
43-
// Render the main page template, passing the servers
44-
err = templates.IndexPage(servers).Render(r.Context(), w)
30+
err := templates.IndexPage(templates.IndexPageProps{
31+
Servers: []domain.ServerItem{
32+
{
33+
Name: "Server Alpha",
34+
IP: "192.168.1.10",
35+
},
36+
{
37+
Name: "Server Beta",
38+
IP: "192.168.1.11",
39+
},
40+
{
41+
Name: "Server Charlie",
42+
IP: "192.168.1.12",
43+
},
44+
},
45+
}).Render(r.Context(), w)
4546
if err != nil {
4647
// Use the injected logger
4748
h.log.Error().Err(err).Msg("failed to render index page template")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
// Package middleware provides HTTP middleware components for the web server.
22
package middleware
3+
4+
import "net/http"
5+
6+
func StripTrailingSlash(next http.Handler) http.Handler {
7+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8+
if len(r.URL.Path) > 1 && r.URL.Path[len(r.URL.Path)-1] == '/' {
9+
http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusMovedPermanently)
10+
return
11+
}
12+
13+
next.ServeHTTP(w, r)
14+
})
15+
}

internal/web/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ func NewMux(uiHandler http.Handler /* add apiHandler http.Handler later */) *htt
2323
return mux
2424
}
2525

26+
2627
// NewServer creates the main HTTP server instance.
2728
func NewServer(mux *http.ServeMux) *http.Server {
2829
// TODO: Make address configurable
2930
return &http.Server{
3031
Addr: ":8080",
31-
Handler: mux,
32+
Handler: middleware.StripTrailingSlash(mux),
3233
}
3334
}
3435

internal/web/templates/add.templ

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package templates
2+
3+
import "github.com/co-browser/agent-browser/internal/web/templates/blocks"
4+
5+
type AddPageProps struct {
6+
}
7+
8+
templ AddPage(props AddPageProps) {
9+
@blocks.Main(blocks.AddServerForm())
10+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package blocks
2+
3+
import "github.com/co-browser/agent-browser/internal/web/templates/components"
4+
5+
css addServerForm() {
6+
padding: 20px 16px;
7+
display: flex;
8+
flex-direction: column;
9+
align-items: center;
10+
justify-content: center;
11+
min-width: 300px;
12+
}
13+
14+
css addServerFormField() {
15+
display: flex;
16+
flex-direction: column;
17+
margin-bottom: 16px;
18+
}
19+
20+
css addServerFormFieldLabel() {
21+
font-size: 18px;
22+
font-weight: bold;
23+
margin-bottom: 4px;
24+
}
25+
26+
css addServerFormFieldInput() {
27+
height: 30px;
28+
font-size: 16px;
29+
padding: 8px;
30+
border: 1px solid #ccc;
31+
border-radius: 4px;
32+
}
33+
34+
css addServerFormButton() {
35+
height: 40px;
36+
width: 100%;
37+
background-color: #007bff;
38+
color: white;
39+
border: none;
40+
border-radius: 4px;
41+
}
42+
43+
templ AddServerForm() {
44+
<section class={ addServerForm() } data-testid="add-server-form">
45+
<h2>Add New Server</h2>
46+
<form method="POST" action="/ui/add">
47+
<div class={ addServerFormField() }>
48+
<label class={ addServerFormFieldLabel() } for="name">
49+
Server Name:
50+
</label>
51+
<input
52+
class={ addServerFormFieldInput() }
53+
id="name"
54+
type="text"
55+
name="name"
56+
placeholder="Enter server name"
57+
required
58+
/>
59+
</div>
60+
<div class={ addServerFormField() }>
61+
<label class={ addServerFormFieldLabel() } for="ip">
62+
IP Address:
63+
</label>
64+
<input
65+
class={ addServerFormFieldInput() }
66+
id="ip"
67+
type="text"
68+
pattern="^((25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})$"
69+
name="ip"
70+
placeholder="Enter ip address"
71+
required
72+
/>
73+
</div>
74+
@components.Button(components.ButtonProps{Label: "Add Server", Type: "submit", Class: addServerFormButton()})
75+
</form>
76+
</section>
77+
}

internal/web/templates/blocks/header.templ

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ css header() {
66
padding: 20px 16px;
77
}
88

9-
css title() {
9+
css headerTitle() {
1010
margin: 0;
1111
}
1212

13+
css headerLink() {
14+
text-decoration: none;
15+
text-transform: none;
16+
color: inherit;
17+
}
18+
1319
templ Header(name string) {
1420
<header class={ header() } data-testid="header">
15-
<h1 class={ title() }>{ name }</h1>
21+
<a class={ headerLink() } href="/ui"><h1 class={ headerTitle() }>{ name }</h1></a>
1622
</header>
1723
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package blocks
2+
3+
templ Main(component templ.Component) {
4+
<!DOCTYPE html>
5+
<html lang="en">
6+
<head>
7+
<meta charset="UTF-8"/>
8+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
9+
<title>MCP Aggregator</title>
10+
<style>
11+
html, body {
12+
height: 100%;
13+
margin: 0;
14+
}
15+
16+
body {
17+
display: flex;
18+
flex-direction: column;
19+
min-height: 100vh;
20+
}
21+
22+
main {
23+
flex: 1;
24+
}
25+
</style>
26+
</head>
27+
<body style="margin: 0">
28+
@Header("Welcome to MCP Aggregator")
29+
<main>
30+
@component
31+
</main>
32+
@Footer("Footer")
33+
</body>
34+
</html>
35+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package blocks
2+
3+
import (
4+
"github.com/co-browser/agent-browser/internal/web/domain"
5+
"github.com/co-browser/agent-browser/internal/web/templates/components"
6+
)
7+
8+
css serverSection() {
9+
padding: 20px 16px;
10+
}
11+
12+
css serverTitle() {
13+
margin: 0;
14+
}
15+
16+
css serverList() {
17+
list-style-type: none;
18+
padding: 0;
19+
display: grid;
20+
grid-template-columns: 1fr 1fr;
21+
align-items: center;
22+
gap: 16px;
23+
}
24+
25+
css serverListItem() {
26+
background-color: #444;
27+
color: #fff;
28+
padding: 10px;
29+
margin: 5px 0;
30+
border-radius: 4px;
31+
display: grid;
32+
grid-template-columns: 1fr auto;
33+
align-items: center;
34+
gap: 16px;
35+
}
36+
37+
css serverListItemWrapper() {
38+
display: flex;
39+
flex-direction: column;
40+
}
41+
42+
css serverListItemName() {
43+
display: block;
44+
font-weight: bold;
45+
}
46+
47+
css serverListItemIP() {
48+
display: block;
49+
font-size: 0.9em;
50+
font-style: italic;
51+
}
52+
53+
css serverSectionHeader() {
54+
display: flex;
55+
justify-content: space-between;
56+
align-items: center;
57+
}
58+
59+
templ Servers(servers []domain.ServerItem) {
60+
<section class={ serverSection() }>
61+
<div class={ serverSectionHeader() }>
62+
<h2 class={ serverTitle() }>MCP servers</h2>
63+
@components.Link("Add server", "/ui/add")
64+
</div>
65+
<ul class={ serverList() } data-testid="servers">
66+
for _, item := range servers {
67+
@Server(item)
68+
}
69+
</ul>
70+
</section>
71+
}
72+
73+
templ Server(server domain.ServerItem) {
74+
<li class={ serverListItem() }>
75+
<div class={ serverListItemWrapper() }>
76+
<strong class={ serverListItemName() }>{ server.Name }</strong>
77+
<div class={ serverListItemIP() }>{ server.IP }</div>
78+
</div>
79+
@components.Button(components.ButtonProps{Label: "Remove", Type: "button"})
80+
</li>
81+
}
Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
package components
22

3-
templ Button(label string) {
4-
<button>
5-
{ label }
3+
type ButtonProps struct {
4+
Label string
5+
Type string
6+
Class templ.CSSClass
7+
}
8+
9+
css buttonElement() {
10+
cursor: pointer;
11+
}
12+
13+
templ Button(props ButtonProps) {
14+
<button class={ buttonElement(), props.Class } type={ props.Type }>
15+
{ props.Label }
616
</button>
717
}

0 commit comments

Comments
 (0)