Skip to content

Commit dd7a7aa

Browse files
Miyamura80claude
andcommitted
Add improved theme toggle and enterprise-locked tabs
- Add ThemeToggle component with visual sun/moon icons and smooth animations - Replace text-based theme button with visual switch element - Replace "Refresh" text button with visual refresh icon - Add "Users" tab with enterprise lock indicator - Add "Roles" tab with enterprise lock indicator - Create EnterpriseFeature component for locked features - Display enterprise features list with CTAs for demo booking Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d352058 commit dd7a7aa

3 files changed

Lines changed: 177 additions & 8 deletions

File tree

frontend/src/App.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import Editor from '@monaco-editor/react'
44
import { useSessions } from './hooks'
55
import type { Session, OAuthServerInfo, OAuthStatusResponse, OAuthAuthorizeRequest, OAuthStatus, ToolSchemasResponse, ToolSchemaEntry } from './types'
66
import { SessionTable } from './components/SessionTable'
7-
import { Toggle } from './components/Toggle'
7+
import { Toggle, ThemeToggle } from './components/Toggle'
88
import { Modal } from './components/Modal'
99
import AgentDataflow from './components/AgentDataflow'
1010
import Stats from './components/Stats'
1111
import Kpis from './components/Kpis'
1212
import DateRangeSlider from './components/DateRangeSlider'
1313
import { ComparisonTable } from './components/ComparisonTable'
14+
import { EnterpriseFeature } from './components/EnterpriseFeature'
1415

1516
// Embedding/Electron detection
1617
const isEmbedded = (() => {
@@ -338,10 +339,10 @@ export function App(): React.JSX.Element {
338339

339340
const projectRoot = (globalThis as any).__PROJECT_ROOT__ || ''
340341

341-
const [view, setView] = useState<'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'comparison'>(() => {
342+
const [view, setView] = useState<'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'comparison' | 'users' | 'roles'>(() => {
342343
try {
343344
const saved = safeLocalStorage.getItem('app_view')
344-
if (saved === 'sessions' || saved === 'configs' || saved === 'manager' || saved === 'observability' || saved === 'agents' || saved === 'comparison') {
345+
if (saved === 'sessions' || saved === 'configs' || saved === 'manager' || saved === 'observability' || saved === 'agents' || saved === 'comparison' || saved === 'users' || saved === 'roles') {
345346
return saved
346347
}
347348
} catch { /* ignore */ }
@@ -350,7 +351,7 @@ export function App(): React.JSX.Element {
350351
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
351352

352353
// Handle view changes with unsaved changes warning
353-
const handleViewChange = (newView: 'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'comparison') => {
354+
const handleViewChange = (newView: 'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'comparison' | 'users' | 'roles') => {
354355
if (hasUnsavedChanges && view === 'configs') {
355356
const confirmed = window.confirm('You have unsaved changes in the JSON editor. Are you sure you want to switch views? Your changes will be lost.')
356357
if (!confirmed) return
@@ -737,24 +738,49 @@ export function App(): React.JSX.Element {
737738
{view === 'manager' && 'Manage MCP servers, tools, and permissions with a guided interface.'}
738739
{view === 'agents' && 'Monitor agent identities, sessions, and permission overrides.'}
739740
{view === 'comparison' && 'Feature comparison between OpenEdison (Open Source) and EdisonWatch (Commercial).'}
741+
{view === 'users' && 'Multi-user management and access control (Enterprise feature).'}
742+
{view === 'roles' && 'Role-based access control and permission management (Enterprise feature).'}
740743
</p>
741744
</div>
742745
<div className="flex gap-2 items-center">
743746
<div className="hidden sm:flex border border-app-border rounded overflow-hidden">
744747
<button className={`px-3 py-1 text-sm ${view === 'sessions' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('sessions')}>Sessions</button>
745748
<button className={`px-3 py-1 text-sm ${view === 'agents' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('agents')}>Agents</button>
749+
<button className={`px-3 py-1 text-sm ${view === 'users' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('users')}>
750+
<span className="inline-flex items-center gap-1">
751+
Users
752+
<svg className="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
753+
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
754+
</svg>
755+
</span>
756+
</button>
757+
<button className={`px-3 py-1 text-sm ${view === 'roles' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('roles')}>
758+
<span className="inline-flex items-center gap-1">
759+
Roles
760+
<svg className="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
761+
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
762+
</svg>
763+
</span>
764+
</button>
746765
<button className={`px-3 py-1 text-sm ${view === 'configs' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('configs')}>Raw Config</button>
747766
<button className={`px-3 py-1 text-sm ${view === 'manager' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('manager')}>Server Manager</button>
748767
<button className={`px-3 py-1 text-sm ${view === 'comparison' ? 'text-app-accent border-r border-app-border bg-app-accent/10' : ''}`} onClick={() => handleViewChange('comparison')}>Comparison</button>
749768
<button className={`px-3 py-1 text-sm ${view === 'observability' ? 'text-app-accent bg-app-accent/10' : ''}`} onClick={() => handleViewChange('observability')}>Observability</button>
750769
</div>
751770
{/* Hide theme switch when embedded in Electron (exposed via window.__ELECTRON_EMBED__) */}
752771
{!(window as any).__ELECTRON_EMBED__ && (new URLSearchParams(location.search).get('embed') !== 'electron') && (
753-
<button className="button" onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
754-
{theme === 'light' ? 'Dark' : 'Light'} mode
755-
</button>
772+
<ThemeToggle theme={theme} onChange={setTheme} />
756773
)}
757-
<button className="button" onClick={() => location.reload()}>Refresh</button>
774+
<button
775+
className="button flex items-center justify-center"
776+
onClick={() => location.reload()}
777+
aria-label="Refresh page"
778+
title="Refresh page"
779+
>
780+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
781+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
782+
</svg>
783+
</button>
758784
</div>
759785
</div>
760786

@@ -845,6 +871,16 @@ export function App(): React.JSX.Element {
845871
<AgentsView sessions={uiSessions} />
846872
) : view === 'comparison' ? (
847873
<ComparisonTable />
874+
) : view === 'users' ? (
875+
<EnterpriseFeature
876+
featureName="User Management"
877+
description="Manage multiple users, roles, and permissions across your organization with enterprise-grade access control."
878+
/>
879+
) : view === 'roles' ? (
880+
<EnterpriseFeature
881+
featureName="Role-Based Access Control"
882+
description="Define custom roles and granular permissions to control what users can access and manage across your MCP infrastructure."
883+
/>
848884
) : (
849885
<div className="space-y-4">
850886
<Kpis sessions={timeFiltered} prevSessions={prevTimeFiltered} />
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from 'react'
2+
3+
interface EnterpriseFeatureProps {
4+
featureName: string
5+
description: string
6+
}
7+
8+
export function EnterpriseFeature({ featureName, description }: EnterpriseFeatureProps) {
9+
return (
10+
<div className="card max-w-4xl mx-auto">
11+
<div className="flex flex-col items-center justify-center py-16 px-8 text-center">
12+
{/* Lock Icon */}
13+
<div className="mb-6 p-4 rounded-full bg-app-accent/10 border-2 border-app-accent/30">
14+
<svg className="w-16 h-16 text-app-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
16+
</svg>
17+
</div>
18+
19+
{/* Heading */}
20+
<div className="mb-4">
21+
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/30 mb-3">
22+
<svg className="w-4 h-4 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
23+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clipRule="evenodd" />
24+
</svg>
25+
<span className="text-xs font-semibold text-amber-400 uppercase tracking-wide">Enterprise Feature</span>
26+
</div>
27+
<h2 className="text-3xl font-bold mb-2">{featureName}</h2>
28+
<p className="text-lg text-app-muted max-w-2xl">{description}</p>
29+
</div>
30+
31+
{/* Feature List */}
32+
<div className="mt-8 p-6 bg-app-bg/30 rounded-lg border border-app-border max-w-2xl w-full">
33+
<h3 className="font-semibold mb-4 text-left">Available in EdisonWatch Enterprise</h3>
34+
<ul className="text-left space-y-3 text-app-muted">
35+
<li className="flex items-start gap-3">
36+
<svg className="w-5 h-5 text-app-accent mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
37+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
38+
</svg>
39+
<span><strong>Multi-Tenancy</strong>: Manage multiple isolated users and organizations</span>
40+
</li>
41+
<li className="flex items-start gap-3">
42+
<svg className="w-5 h-5 text-app-accent mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
43+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
44+
</svg>
45+
<span><strong>SSO Integration</strong>: Enterprise identity provider support</span>
46+
</li>
47+
<li className="flex items-start gap-3">
48+
<svg className="w-5 h-5 text-app-accent mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
49+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
50+
</svg>
51+
<span><strong>SIEM Integration</strong>: Enterprise security monitoring</span>
52+
</li>
53+
<li className="flex items-start gap-3">
54+
<svg className="w-5 h-5 text-app-accent mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
55+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
56+
</svg>
57+
<span><strong>Client Software</strong>: Auto-enforcement at the client level</span>
58+
</li>
59+
</ul>
60+
</div>
61+
62+
{/* CTA */}
63+
<div className="mt-8 flex flex-col sm:flex-row gap-4 items-center">
64+
<a
65+
href="https://cal.com/eito80/demo"
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
className="inline-flex items-center gap-2 px-6 py-3 font-semibold rounded-lg hover:opacity-90 transition-opacity shadow-lg"
69+
style={{ backgroundColor: 'var(--accent)', color: 'var(--bg)' }}
70+
>
71+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
73+
</svg>
74+
Schedule a Demo
75+
</a>
76+
<a
77+
href="https://edisonwatch.com"
78+
target="_blank"
79+
rel="noopener noreferrer"
80+
className="inline-flex items-center gap-2 px-6 py-3 font-semibold rounded-lg border-2 border-app-accent text-app-accent hover:bg-app-accent/10 transition-colors"
81+
>
82+
Learn More
83+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
84+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
85+
</svg>
86+
</a>
87+
</div>
88+
</div>
89+
</div>
90+
)
91+
}

frontend/src/components/Toggle.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,46 @@ export function Toggle({ checked, onChange }: { checked: boolean; onChange: (nex
2222
)
2323
}
2424

25+
export function ThemeToggle({ theme, onChange }: { theme: 'light' | 'dark' | 'blue'; onChange: (theme: 'light' | 'dark') => void }) {
26+
const isDark = theme === 'dark' || theme === 'blue'
27+
28+
return (
29+
<button
30+
type="button"
31+
role="switch"
32+
aria-checked={isDark}
33+
aria-label="Toggle theme"
34+
className="relative inline-flex h-8 w-16 items-center rounded-full transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-app-accent"
35+
style={{
36+
backgroundColor: isDark ? 'var(--accent)' : '#FFB84D',
37+
}}
38+
onClick={() => onChange(isDark ? 'light' : 'dark')}
39+
onKeyDown={(e) => {
40+
if (e.key === 'Enter' || e.key === ' ') {
41+
e.preventDefault()
42+
onChange(isDark ? 'light' : 'dark')
43+
}
44+
}}
45+
>
46+
{/* Slider */}
47+
<span
48+
className={`inline-flex h-6 w-6 transform items-center justify-center rounded-full shadow-md transition-transform duration-200 ${isDark ? 'translate-x-9' : 'translate-x-1'}`}
49+
style={{ backgroundColor: 'var(--card)' }}
50+
>
51+
{isDark ? (
52+
// Moon icon
53+
<svg className="h-4 w-4" style={{ color: 'var(--accent)' }} fill="currentColor" viewBox="0 0 20 20">
54+
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
55+
</svg>
56+
) : (
57+
// Sun icon
58+
<svg className="h-4 w-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
59+
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
60+
</svg>
61+
)}
62+
</span>
63+
</button>
64+
)
65+
}
66+
2567

0 commit comments

Comments
 (0)