Skip to content

Commit 3827332

Browse files
committed
refine UI and fix some iframe error
Signed-off-by: JaredforReal <[email protected]>
1 parent 13523d9 commit 3827332

File tree

8 files changed

+334
-18
lines changed

8 files changed

+334
-18
lines changed

dashboard/backend/main.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ func newReverseProxy(targetBase, stripPrefix string, forwardAuth bool) (*httputi
4949
if !forwardAuth {
5050
r.Header.Del("Authorization")
5151
}
52+
53+
// Log the proxied request for debugging
54+
log.Printf("Proxying: %s %s -> %s://%s%s", r.Method, stripPrefix, targetURL.Scheme, targetURL.Host, p)
55+
}
56+
57+
// Add error handler for proxy failures
58+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
59+
log.Printf("Proxy error for %s: %v", r.URL.Path, err)
60+
http.Error(w, fmt.Sprintf("Bad Gateway: %v", err), http.StatusBadGateway)
5261
}
5362

5463
// Sanitize response headers for iframe embedding
@@ -87,8 +96,18 @@ func newReverseProxy(targetBase, stripPrefix string, forwardAuth bool) (*httputi
8796
func staticFileServer(staticDir string) http.Handler {
8897
fs := http.FileServer(http.Dir(staticDir))
8998
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90-
// Serve index.html for root and for unknown routes (SPA)
99+
// Never serve index.html for API or embedded proxy routes
100+
// These should be handled by their respective handlers
91101
p := r.URL.Path
102+
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/embedded/") ||
103+
strings.HasPrefix(p, "/metrics/") || strings.HasPrefix(p, "/public/") ||
104+
strings.HasPrefix(p, "/avatar/") {
105+
// These paths should have been handled by other handlers
106+
// If we reach here, it means the proxy failed or route not found
107+
http.Error(w, "Service not available", http.StatusBadGateway)
108+
return
109+
}
110+
92111
full := path.Join(staticDir, path.Clean(p))
93112

94113
// Check if file exists
@@ -143,33 +162,67 @@ func main() {
143162
w.Write([]byte(`{"status":"healthy","service":"semantic-router-dashboard"}`))
144163
})
145164

146-
// Static frontend
147-
mux.Handle("/", staticFileServer(*staticDir)) // Router API proxy (forward Authorization)
165+
// Router API proxy (forward Authorization) - MUST be registered before Grafana
166+
var routerAPIProxy *httputil.ReverseProxy
148167
if *routerAPI != "" {
149168
rp, err := newReverseProxy(*routerAPI, "/api/router", true)
150169
if err != nil {
151170
log.Fatalf("router API proxy error: %v", err)
152171
}
153-
mux.Handle("/api/router", rp)
172+
routerAPIProxy = rp
154173
mux.Handle("/api/router/", rp)
174+
log.Printf("Router API proxy configured: %s", *routerAPI)
155175
}
156176

157-
// Router metrics passthrough (no rewrite, simple redirect/proxy)
158-
mux.HandleFunc("/metrics/router", func(w http.ResponseWriter, r *http.Request) {
159-
// Simple 302 redirect for now to let Prometheus UI open directly
160-
http.Redirect(w, r, *routerMetrics, http.StatusTemporaryRedirect)
161-
})
162-
163-
// Grafana proxy
177+
// Grafana proxy and static assets
178+
var grafanaStaticProxy *httputil.ReverseProxy
164179
if *grafanaURL != "" {
165180
gp, err := newReverseProxy(*grafanaURL, "/embedded/grafana", false)
166181
if err != nil {
167182
log.Fatalf("grafana proxy error: %v", err)
168183
}
169-
mux.Handle("/embedded/grafana", gp)
170184
mux.Handle("/embedded/grafana/", gp)
185+
186+
// Proxy for Grafana static assets (no prefix stripping)
187+
grafanaStaticProxy, _ = newReverseProxy(*grafanaURL, "", false)
188+
mux.Handle("/public/", grafanaStaticProxy)
189+
mux.Handle("/avatar/", grafanaStaticProxy)
190+
191+
log.Printf("Grafana proxy configured: %s", *grafanaURL)
192+
log.Printf("Grafana static assets proxied: /public/, /avatar/")
193+
} else {
194+
mux.HandleFunc("/embedded/grafana/", func(w http.ResponseWriter, r *http.Request) {
195+
w.Header().Set("Content-Type", "application/json")
196+
w.WriteHeader(http.StatusServiceUnavailable)
197+
w.Write([]byte(`{"error":"Grafana not configured","message":"TARGET_GRAFANA_URL environment variable is not set"}`))
198+
})
199+
log.Printf("Warning: Grafana URL not configured")
171200
}
172201

202+
// Smart /api/ router: route to Router API or Grafana API based on path
203+
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
204+
// If path starts with /api/router/, use Router API proxy
205+
if strings.HasPrefix(r.URL.Path, "/api/router/") && routerAPIProxy != nil {
206+
routerAPIProxy.ServeHTTP(w, r)
207+
return
208+
}
209+
// Otherwise, if Grafana is configured, proxy to Grafana API
210+
if grafanaStaticProxy != nil {
211+
grafanaStaticProxy.ServeHTTP(w, r)
212+
return
213+
}
214+
// No handler available
215+
http.Error(w, "Service not available", http.StatusBadGateway)
216+
})
217+
218+
// Router metrics passthrough
219+
mux.HandleFunc("/metrics/router", func(w http.ResponseWriter, r *http.Request) {
220+
http.Redirect(w, r, *routerMetrics, http.StatusTemporaryRedirect)
221+
})
222+
223+
// Static frontend - MUST be registered last
224+
mux.Handle("/", staticFileServer(*staticDir))
225+
173226
// Prometheus proxy (optional)
174227
if *promURL != "" {
175228
pp, err := newReverseProxy(*promURL, "/embedded/prometheus", false)
@@ -178,6 +231,14 @@ func main() {
178231
}
179232
mux.Handle("/embedded/prometheus", pp)
180233
mux.Handle("/embedded/prometheus/", pp)
234+
log.Printf("Prometheus proxy configured: %s", *promURL)
235+
} else {
236+
mux.HandleFunc("/embedded/prometheus/", func(w http.ResponseWriter, r *http.Request) {
237+
w.Header().Set("Content-Type", "application/json")
238+
w.WriteHeader(http.StatusServiceUnavailable)
239+
w.Write([]byte(`{"error":"Prometheus not configured","message":"TARGET_PROMETHEUS_URL environment variable is not set"}`))
240+
})
241+
log.Printf("Warning: Prometheus URL not configured")
181242
}
182243

183244
// Open WebUI proxy (optional)
@@ -188,6 +249,14 @@ func main() {
188249
}
189250
mux.Handle("/embedded/openwebui", op)
190251
mux.Handle("/embedded/openwebui/", op)
252+
log.Printf("Open WebUI proxy configured: %s", *openwebuiURL)
253+
} else {
254+
mux.HandleFunc("/embedded/openwebui/", func(w http.ResponseWriter, r *http.Request) {
255+
w.Header().Set("Content-Type", "application/json")
256+
w.WriteHeader(http.StatusServiceUnavailable)
257+
w.Write([]byte(`{"error":"Open WebUI not configured","message":"TARGET_OPENWEBUI_URL environment variable is not set or empty"}`))
258+
})
259+
log.Printf("Info: Open WebUI not configured (optional)")
191260
}
192261

193262
addr := ":" + *port

dashboard/frontend/src/App.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,70 @@
1-
import React from 'react'
1+
import React, { useEffect, useState } from 'react'
22
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
33
import Layout from './components/Layout'
44
import MonitoringPage from './pages/MonitoringPage'
55
import ConfigPage from './pages/ConfigPage'
66
import PlaygroundPage from './pages/PlaygroundPage'
77

88
const App: React.FC = () => {
9+
const [isInIframe, setIsInIframe] = useState(false)
10+
11+
useEffect(() => {
12+
// Detect if we're running inside an iframe (potential loop)
13+
if (window.self !== window.top) {
14+
setIsInIframe(true)
15+
console.warn('Dashboard detected it is running inside an iframe - this may indicate a loop')
16+
}
17+
}, [])
18+
19+
// If we're in an iframe, show a warning instead of rendering the full app
20+
if (isInIframe) {
21+
return (
22+
<div
23+
style={{
24+
display: 'flex',
25+
flexDirection: 'column',
26+
alignItems: 'center',
27+
justifyContent: 'center',
28+
height: '100vh',
29+
padding: '2rem',
30+
textAlign: 'center',
31+
backgroundColor: 'var(--color-bg)',
32+
color: 'var(--color-text)',
33+
}}
34+
>
35+
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>⚠️</div>
36+
<h1 style={{ fontSize: '1.5rem', marginBottom: '1rem', color: 'var(--color-danger)' }}>
37+
Nested Dashboard Detected
38+
</h1>
39+
<p style={{ maxWidth: '600px', lineHeight: '1.6', color: 'var(--color-text-secondary)' }}>
40+
The dashboard has detected that it is running inside an iframe. This usually indicates a
41+
configuration error where the dashboard is trying to embed itself.
42+
</p>
43+
<p style={{ marginTop: '1rem', color: 'var(--color-text-secondary)' }}>
44+
Please check your Grafana dashboard path and backend proxy configuration.
45+
</p>
46+
<button
47+
onClick={() => {
48+
window.top?.location.reload()
49+
}}
50+
style={{
51+
marginTop: '1.5rem',
52+
padding: '0.75rem 1.5rem',
53+
backgroundColor: 'var(--color-primary)',
54+
color: 'white',
55+
border: 'none',
56+
borderRadius: 'var(--radius-md)',
57+
fontSize: '0.875rem',
58+
fontWeight: '500',
59+
cursor: 'pointer',
60+
}}
61+
>
62+
Open Dashboard in New Tab
63+
</button>
64+
</div>
65+
)
66+
}
67+
968
return (
1069
<BrowserRouter>
1170
<Layout>

dashboard/frontend/src/components/Layout.module.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
.container {
22
display: flex;
33
flex-direction: column;
4-
min-height: 100vh;
4+
height: 100vh;
5+
overflow: hidden;
56
}
67

78
.header {
@@ -12,6 +13,8 @@
1213
background-color: var(--color-bg-secondary);
1314
border-bottom: 1px solid var(--color-border);
1415
box-shadow: 0 1px 3px var(--color-shadow);
16+
flex-shrink: 0;
17+
height: 64px;
1518
}
1619

1720
.headerLeft {
@@ -86,4 +89,5 @@
8689
display: flex;
8790
flex-direction: column;
8891
overflow: hidden;
92+
min-height: 0;
8993
}

dashboard/frontend/src/pages/ConfigPage.module.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
height: 100%;
55
padding: 1rem;
66
gap: 1rem;
7+
overflow: hidden;
78
}
89

910
.header {
@@ -14,6 +15,7 @@
1415
background-color: var(--color-bg-secondary);
1516
border: 1px solid var(--color-border);
1617
border-radius: var(--radius-lg);
18+
flex-shrink: 0;
1719
}
1820

1921
.headerLeft {
@@ -71,6 +73,7 @@
7173
border-radius: var(--radius-lg);
7274
overflow: auto;
7375
padding: 1rem;
76+
min-height: 0;
7477
}
7578

7679
.loading {

dashboard/frontend/src/pages/MonitoringPage.module.css

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
height: 100%;
55
padding: 1rem;
66
gap: 1rem;
7+
overflow: hidden;
78
}
89

910
.controls {
1011
background-color: var(--color-bg-secondary);
1112
border: 1px solid var(--color-border);
1213
border-radius: var(--radius-lg);
1314
padding: 1rem;
15+
flex-shrink: 0;
1416
}
1517

1618
.controlGroup {
@@ -63,19 +65,100 @@
6365
display: flex;
6466
gap: 1rem;
6567
margin-top: 0.5rem;
68+
flex-wrap: wrap;
6669
}
6770

6871
.hint {
6972
font-size: 0.75rem;
7073
color: var(--color-text-secondary);
7174
}
7275

76+
.errorBanner {
77+
display: flex;
78+
align-items: center;
79+
gap: 0.75rem;
80+
padding: 0.75rem 1rem;
81+
background-color: var(--color-danger);
82+
color: white;
83+
border-radius: var(--radius-md);
84+
font-size: 0.875rem;
85+
animation: slideIn 0.3s ease;
86+
}
87+
88+
@keyframes slideIn {
89+
from {
90+
opacity: 0;
91+
transform: translateY(-10px);
92+
}
93+
94+
to {
95+
opacity: 1;
96+
transform: translateY(0);
97+
}
98+
}
99+
100+
.errorIcon {
101+
font-size: 1.25rem;
102+
}
103+
104+
.retryButton {
105+
margin-left: auto;
106+
padding: 0.375rem 0.75rem;
107+
background-color: rgba(255, 255, 255, 0.2);
108+
color: white;
109+
border-radius: var(--radius-sm);
110+
font-size: 0.75rem;
111+
font-weight: 500;
112+
transition: background-color var(--transition-fast);
113+
}
114+
115+
.retryButton:hover {
116+
background-color: rgba(255, 255, 255, 0.3);
117+
}
118+
73119
.iframeContainer {
120+
position: relative;
74121
flex: 1;
75122
background-color: var(--color-bg-secondary);
76123
border: 1px solid var(--color-border);
77124
border-radius: var(--radius-lg);
78125
overflow: hidden;
126+
min-height: 0;
127+
}
128+
129+
.loadingOverlay {
130+
position: absolute;
131+
top: 0;
132+
left: 0;
133+
right: 0;
134+
bottom: 0;
135+
display: flex;
136+
flex-direction: column;
137+
align-items: center;
138+
justify-content: center;
139+
background-color: var(--color-bg);
140+
z-index: 10;
141+
gap: 1rem;
142+
}
143+
144+
.spinner {
145+
width: 40px;
146+
height: 40px;
147+
border: 3px solid var(--color-border);
148+
border-top-color: var(--color-primary);
149+
border-radius: 50%;
150+
animation: spin 0.8s linear infinite;
151+
}
152+
153+
@keyframes spin {
154+
to {
155+
transform: rotate(360deg);
156+
}
157+
}
158+
159+
.loadingOverlay p {
160+
color: var(--color-text-secondary);
161+
font-size: 0.875rem;
79162
}
80163

81164
.iframe {

0 commit comments

Comments
 (0)