Skip to content

Commit c462f2f

Browse files
committed
feat(dashboard): enhance UI with navigation improvements and layout optimizations
- Flatten navigation structure by removing Configuration parent level - Move all configuration sections to same level as Playground and Monitoring - Add clickable brand logo linking to homepage - Add GitHub and documentation links in sidebar footer - Move URL configuration controls to bottom of Playground and Monitoring pages - Convert threshold inputs from decimal (0-1) to percentage (0-100) format - Limit tools grid to maximum 3 columns per row - Auto-load Playground page on mount - Remove embedded mode option from Playground - Improve navigation consistency across all pages These changes provide a cleaner, more intuitive user interface with better navigation flow and improved user experience. Signed-off-by: bitliu <[email protected]>
1 parent 15753b0 commit c462f2f

File tree

8 files changed

+377
-160
lines changed

8 files changed

+377
-160
lines changed

dashboard/frontend/src/App.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import LandingPage from './pages/LandingPage'
55
import MonitoringPage from './pages/MonitoringPage'
66
import ConfigPage from './pages/ConfigPage'
77
import PlaygroundPage from './pages/PlaygroundPage'
8+
import { ConfigSection } from './components/ConfigNav'
89

910
const App: React.FC = () => {
1011
const [isInIframe, setIsInIframe] = useState(false)
12+
const [configSection, setConfigSection] = useState<ConfigSection>('models')
1113

1214
useEffect(() => {
1315
// Detect if we're running inside an iframe (potential loop)
@@ -70,9 +72,39 @@ const App: React.FC = () => {
7072
<BrowserRouter>
7173
<Routes>
7274
<Route path="/" element={<LandingPage />} />
73-
<Route path="/monitoring" element={<Layout><MonitoringPage /></Layout>} />
74-
<Route path="/config" element={<Layout><ConfigPage /></Layout>} />
75-
<Route path="/playground" element={<Layout><PlaygroundPage /></Layout>} />
75+
<Route
76+
path="/monitoring"
77+
element={
78+
<Layout
79+
configSection={configSection}
80+
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
81+
>
82+
<MonitoringPage />
83+
</Layout>
84+
}
85+
/>
86+
<Route
87+
path="/config"
88+
element={
89+
<Layout
90+
configSection={configSection}
91+
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
92+
>
93+
<ConfigPage activeSection={configSection} />
94+
</Layout>
95+
}
96+
/>
97+
<Route
98+
path="/playground"
99+
element={
100+
<Layout
101+
configSection={configSection}
102+
onConfigSectionChange={(section) => setConfigSection(section as ConfigSection)}
103+
>
104+
<PlaygroundPage />
105+
</Layout>
106+
}
107+
/>
76108
</Routes>
77109
</BrowserRouter>
78110
)

dashboard/frontend/src/components/EditModal.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ interface EditModalProps {
1414
export interface FieldConfig {
1515
name: string
1616
label: string
17-
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'textarea' | 'json'
17+
type: 'text' | 'number' | 'boolean' | 'select' | 'multiselect' | 'textarea' | 'json' | 'percentage'
1818
required?: boolean
1919
options?: string[]
2020
placeholder?: string
2121
description?: string
22+
min?: number
23+
max?: number
24+
step?: number
2225
}
2326

2427
const EditModal: React.FC<EditModalProps> = ({
@@ -36,10 +39,17 @@ const EditModal: React.FC<EditModalProps> = ({
3639

3740
useEffect(() => {
3841
if (isOpen) {
39-
setFormData(data || {})
42+
// Convert percentage fields from 0-1 to 0-100 for display
43+
const convertedData = { ...data }
44+
fields.forEach(field => {
45+
if (field.type === 'percentage' && convertedData[field.name] !== undefined) {
46+
convertedData[field.name] = Math.round(convertedData[field.name] * 100)
47+
}
48+
})
49+
setFormData(convertedData || {})
4050
setError(null)
4151
}
42-
}, [isOpen, data])
52+
}, [isOpen, data, fields])
4353

4454
const handleChange = (fieldName: string, value: any) => {
4555
setFormData((prev: any) => ({
@@ -54,7 +64,14 @@ const EditModal: React.FC<EditModalProps> = ({
5464
setError(null)
5565

5666
try {
57-
await onSave(formData)
67+
// Convert percentage fields from 0-100 back to 0-1 before saving
68+
const convertedData = { ...formData }
69+
fields.forEach(field => {
70+
if (field.type === 'percentage' && convertedData[field.name] !== undefined) {
71+
convertedData[field.name] = convertedData[field.name] / 100
72+
}
73+
})
74+
await onSave(convertedData)
5875
onClose()
5976
} catch (err) {
6077
setError(err instanceof Error ? err.message : 'Failed to save')
@@ -106,7 +123,9 @@ const EditModal: React.FC<EditModalProps> = ({
106123
{field.type === 'number' && (
107124
<input
108125
type="number"
109-
step="any"
126+
step={field.step !== undefined ? field.step : "any"}
127+
min={field.min}
128+
max={field.max}
110129
className={styles.input}
111130
value={formData[field.name] || ''}
112131
onChange={(e) => handleChange(field.name, parseFloat(e.target.value))}
@@ -115,6 +134,37 @@ const EditModal: React.FC<EditModalProps> = ({
115134
/>
116135
)}
117136

137+
{field.type === 'percentage' && (
138+
<div style={{ position: 'relative' }}>
139+
<input
140+
type="number"
141+
step={field.step !== undefined ? field.step : 1}
142+
min={0}
143+
max={100}
144+
className={styles.input}
145+
value={formData[field.name] !== undefined ? formData[field.name] : ''}
146+
onChange={(e) => {
147+
const val = e.target.value
148+
handleChange(field.name, val === '' ? '' : parseFloat(val))
149+
}}
150+
placeholder={field.placeholder}
151+
required={field.required}
152+
style={{ paddingRight: '2.5rem' }}
153+
/>
154+
<span style={{
155+
position: 'absolute',
156+
right: '0.75rem',
157+
top: '50%',
158+
transform: 'translateY(-50%)',
159+
color: 'var(--color-text-secondary)',
160+
fontSize: '0.875rem',
161+
pointerEvents: 'none'
162+
}}>
163+
%
164+
</span>
165+
</div>
166+
)}
167+
118168
{field.type === 'boolean' && (
119169
<label className={styles.checkbox}>
120170
<input

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
align-items: center;
2121
gap: 0.5rem;
2222
padding: 0 0.5rem;
23+
text-decoration: none;
24+
border-radius: var(--radius-md);
25+
transition: background-color var(--transition-fast);
26+
cursor: pointer;
27+
}
28+
29+
.brand:hover {
30+
background-color: var(--color-bg-tertiary);
2331
}
2432

2533
.logo {
@@ -78,22 +86,102 @@
7886
white-space: nowrap;
7987
}
8088

89+
/* Sub Navigation (Configuration sections) - Match parent nav style */
90+
.subNav {
91+
display: flex;
92+
flex-direction: column;
93+
gap: 0.25rem;
94+
margin-top: 0.25rem;
95+
margin-bottom: 0.25rem;
96+
padding-left: 1.75rem;
97+
}
98+
99+
.subNavLink {
100+
display: flex;
101+
align-items: center;
102+
gap: 0.5rem;
103+
padding: 0.5rem 0.625rem;
104+
border-radius: var(--radius-md);
105+
color: var(--color-text-secondary);
106+
font-size: 0.9rem;
107+
font-weight: 500;
108+
transition: all var(--transition-fast);
109+
background: transparent;
110+
border: none;
111+
cursor: pointer;
112+
text-align: left;
113+
width: 100%;
114+
}
115+
116+
.subNavLink:hover {
117+
background-color: var(--color-bg-tertiary);
118+
color: var(--color-text);
119+
}
120+
121+
.subNavLinkActive {
122+
background-color: var(--color-primary);
123+
color: white;
124+
}
125+
126+
.subNavLinkActive:hover {
127+
background-color: var(--color-primary-dark);
128+
color: white;
129+
}
130+
131+
.subNavIcon {
132+
font-size: 1rem;
133+
line-height: 1;
134+
width: 1.25rem;
135+
text-align: center;
136+
flex-shrink: 0;
137+
}
138+
139+
.subNavText {
140+
white-space: nowrap;
141+
overflow: hidden;
142+
text-overflow: ellipsis;
143+
}
144+
81145
.sidebarFooter {
82146
margin-top: auto;
83147
padding: 0 0.5rem;
148+
display: flex;
149+
align-items: center;
150+
gap: 0.5rem;
84151
}
85152

86153
.themeToggle {
87154
padding: 0.5rem;
88155
font-size: 1.25rem;
89156
border-radius: var(--radius-md);
90157
transition: background-color var(--transition-fast);
158+
background: transparent;
159+
border: none;
160+
cursor: pointer;
161+
color: var(--color-text);
91162
}
92163

93164
.themeToggle:hover {
94165
background-color: var(--color-bg-tertiary);
95166
}
96167

168+
.iconButton {
169+
display: flex;
170+
align-items: center;
171+
justify-content: center;
172+
padding: 0.5rem;
173+
border-radius: var(--radius-md);
174+
transition: background-color var(--transition-fast);
175+
color: var(--color-text-secondary);
176+
text-decoration: none;
177+
cursor: pointer;
178+
}
179+
180+
.iconButton:hover {
181+
background-color: var(--color-bg-tertiary);
182+
color: var(--color-text);
183+
}
184+
97185
.main {
98186
flex: 1;
99187
display: flex;

dashboard/frontend/src/components/Layout.tsx

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React, { useState, useEffect, ReactNode } from 'react'
2-
import { NavLink } from 'react-router-dom'
2+
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
33
import styles from './Layout.module.css'
44

55
interface LayoutProps {
66
children: ReactNode
7+
configSection?: string
8+
onConfigSectionChange?: (section: string) => void
79
}
810

9-
const Layout: React.FC<LayoutProps> = ({ children }) => {
11+
const Layout: React.FC<LayoutProps> = ({ children, configSection, onConfigSectionChange }) => {
1012
const [theme, setTheme] = useState<'light' | 'dark'>('dark')
13+
const location = useLocation()
14+
const navigate = useNavigate()
15+
const isConfigPage = location.pathname === '/config'
1116

1217
useEffect(() => {
1318
// Check system preference or stored preference
@@ -28,10 +33,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
2833
return (
2934
<div className={styles.container}>
3035
<aside className={styles.sidebar}>
31-
<div className={styles.brand}>
36+
<NavLink to="/" className={styles.brand}>
3237
<img src="/vllm.png" alt="vLLM" className={styles.logo} />
3338
<span className={styles.brandText}>Semantic Router</span>
34-
</div>
39+
</NavLink>
3540
<nav className={styles.nav}>
3641
<NavLink
3742
to="/playground"
@@ -42,15 +47,34 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
4247
<span className={styles.navIcon}>🎮</span>
4348
<span className={styles.navText}>Playground</span>
4449
</NavLink>
45-
<NavLink
46-
to="/config"
47-
className={({ isActive }) =>
48-
isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink
49-
}
50-
>
51-
<span className={styles.navIcon}>⚙️</span>
52-
<span className={styles.navText}>Configuration</span>
53-
</NavLink>
50+
51+
{/* Configuration sections - Same level as other nav items */}
52+
{onConfigSectionChange && (
53+
<>
54+
{[
55+
{ id: 'models', icon: '🤖', title: 'Models' },
56+
{ id: 'prompt-guard', icon: '🛡️', title: 'Prompt Guard' },
57+
{ id: 'similarity-cache', icon: '⚡', title: 'Similarity Cache' },
58+
{ id: 'intelligent-routing', icon: '🧠', title: 'Intelligent Routing' },
59+
{ id: 'tools-selection', icon: '🔧', title: 'Tools Selection' },
60+
{ id: 'observability', icon: '👁️', title: 'Observability' },
61+
{ id: 'classification-api', icon: '🔌', title: 'Classification API' }
62+
].map((section) => (
63+
<button
64+
key={section.id}
65+
className={`${styles.navLink} ${isConfigPage && configSection === section.id ? styles.navLinkActive : ''}`}
66+
onClick={() => {
67+
onConfigSectionChange(section.id)
68+
navigate('/config')
69+
}}
70+
>
71+
<span className={styles.navIcon}>{section.icon}</span>
72+
<span className={styles.navText}>{section.title}</span>
73+
</button>
74+
))}
75+
</>
76+
)}
77+
5478
<NavLink
5579
to="/monitoring"
5680
className={({ isActive }) =>
@@ -70,6 +94,31 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
7094
>
7195
{theme === 'light' ? '🌙' : '☀️'}
7296
</button>
97+
<a
98+
href="https://github.com/vllm-project/vllm"
99+
target="_blank"
100+
rel="noopener noreferrer"
101+
className={styles.iconButton}
102+
aria-label="GitHub"
103+
title="GitHub Repository"
104+
>
105+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
106+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
107+
</svg>
108+
</a>
109+
<a
110+
href="https://docs.vllm.ai"
111+
target="_blank"
112+
rel="noopener noreferrer"
113+
className={styles.iconButton}
114+
aria-label="Documentation"
115+
title="Documentation"
116+
>
117+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
118+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
119+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
120+
</svg>
121+
</a>
73122
</div>
74123
</aside>
75124
<main className={styles.main}>{children}</main>

0 commit comments

Comments
 (0)