Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 45 additions & 9 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import Editor from '@monaco-editor/react'
import { useSessions } from './hooks'
import type { Session, OAuthServerInfo, OAuthStatusResponse, OAuthAuthorizeRequest, OAuthStatus, ToolSchemasResponse, ToolSchemaEntry } from './types'
import { SessionTable } from './components/SessionTable'
import { Toggle } from './components/Toggle'
import { Toggle, ThemeToggle } from './components/Toggle'
import { Modal } from './components/Modal'
import AgentDataflow from './components/AgentDataflow'
import Stats from './components/Stats'
import Kpis from './components/Kpis'
import DateRangeSlider from './components/DateRangeSlider'
import { EnterpriseFeature } from './components/EnterpriseFeature'

// Embedding/Electron detection
const isEmbedded = (() => {
Expand Down Expand Up @@ -337,10 +338,10 @@ export function App(): React.JSX.Element {

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

const [view, setView] = useState<'sessions' | 'configs' | 'manager' | 'observability' | 'agents'>(() => {
const [view, setView] = useState<'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'users' | 'roles'>(() => {
try {
const saved = safeLocalStorage.getItem('app_view')
if (saved === 'sessions' || saved === 'configs' || saved === 'manager' || saved === 'observability' || saved === 'agents') {
if (saved === 'sessions' || saved === 'configs' || saved === 'manager' || saved === 'observability' || saved === 'agents' || saved === 'users' || saved === 'roles') {
return saved
}
} catch { /* ignore */ }
Expand All @@ -349,7 +350,7 @@ export function App(): React.JSX.Element {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)

// Handle view changes with unsaved changes warning
const handleViewChange = (newView: 'sessions' | 'configs' | 'manager' | 'observability' | 'agents') => {
const handleViewChange = (newView: 'sessions' | 'configs' | 'manager' | 'observability' | 'agents' | 'users' | 'roles') => {
if (hasUnsavedChanges && view === 'configs') {
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.')
if (!confirmed) return
Expand Down Expand Up @@ -735,23 +736,48 @@ export function App(): React.JSX.Element {
{view === 'configs' && 'Direct JSON editing for configuration and permission files.'}
{view === 'manager' && 'Manage MCP servers, tools, and permissions with a guided interface.'}
{view === 'agents' && 'Monitor agent identities, sessions, and permission overrides.'}
{view === 'users' && 'Multi-user management and access control (Enterprise feature).'}
{view === 'roles' && 'Role-based access control and permission management (Enterprise feature).'}
</p>
</div>
<div className="flex gap-2 items-center">
<div className="hidden sm:flex border border-app-border rounded overflow-hidden">
<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>
<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>
<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')}>
<span className="inline-flex items-center gap-1">
Users
<svg className="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<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" />
</svg>
</span>
</button>
<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')}>
<span className="inline-flex items-center gap-1">
Roles
<svg className="w-3 h-3 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<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" />
</svg>
</span>
</button>
<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>
<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>
<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')}>Servers</button>
<button className={`px-3 py-1 text-sm ${view === 'observability' ? 'text-app-accent bg-app-accent/10' : ''}`} onClick={() => handleViewChange('observability')}>Observability</button>
</div>
{/* Hide theme switch when embedded in Electron (exposed via window.__ELECTRON_EMBED__) */}
{!(window as any).__ELECTRON_EMBED__ && (new URLSearchParams(location.search).get('embed') !== 'electron') && (
<button className="button" onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
{theme === 'light' ? 'Dark' : 'Light'} mode
</button>
<ThemeToggle theme={theme} onChange={setTheme} />
)}
<button className="button" onClick={() => location.reload()}>Refresh</button>
<button
className="button flex items-center justify-center"
onClick={() => location.reload()}
aria-label="Refresh page"
title="Refresh page"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
</svg>
</button>
</div>
</div>

Expand Down Expand Up @@ -840,6 +866,16 @@ export function App(): React.JSX.Element {
<ConfigurationManager projectRoot={projectRoot} />
) : view === 'agents' ? (
<AgentsView sessions={uiSessions} />
) : view === 'users' ? (
<EnterpriseFeature
featureName="User Management"
description="Manage multiple users, roles, and permissions across your organization with enterprise-grade access control."
/>
) : view === 'roles' ? (
<EnterpriseFeature
featureName="Role-Based Access Control"
description="Define custom roles and granular permissions to control what users can access and manage across your MCP infrastructure."
/>
) : (
<div className="space-y-4">
<Kpis sessions={timeFiltered} prevSessions={prevTimeFiltered} />
Expand Down
110 changes: 110 additions & 0 deletions frontend/src/components/ComparisonTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
interface Feature {
name: string
openEdison: boolean
edisonWatch: boolean
}

const features: Feature[] = [
{ name: 'Single User', openEdison: true, edisonWatch: true },
{ name: 'MCP Security Controls', openEdison: true, edisonWatch: true },
{ name: 'Lethal Trifecta Detection', openEdison: true, edisonWatch: true },
{ name: 'Tool/Resource Permissions', openEdison: true, edisonWatch: true },
{ name: 'Multi-Tenancy', openEdison: false, edisonWatch: true },
{ name: 'SIEM Integration', openEdison: false, edisonWatch: true },
{ name: 'SSO (Single Sign-On)', openEdison: false, edisonWatch: true },
{ name: 'Client Software for Auto-Enforcement', openEdison: false, edisonWatch: true },
]

function CheckIcon() {
return <span className="text-green-400 text-xl">✅</span>
}

function CrossIcon() {
return <span className="text-rose-400 text-xl">❌</span>
}

export function ComparisonTable() {
return (
<div className="card max-w-5xl mx-auto">
<div className="mb-6">
<h2 className="text-2xl font-semibold mb-2">OpenEdison vs EdisonWatch</h2>
<p className="text-app-muted">
EdisonWatch adds Multi-Tenancy, SIEM, SSO, and Auto-Enforcement
</p>
</div>

<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="border-b border-app-border py-3 px-4 text-left font-semibold">Feature</th>
<th className="border-b border-app-border py-3 px-4 text-center font-semibold">
OpenEdison<br />
<span className="text-sm font-normal text-app-muted">(Open Source)</span>
</th>
<th className="border-b border-app-border py-3 px-4 text-center font-semibold">
EdisonWatch<br />
<span className="text-sm font-normal text-app-muted">(Commercial)</span>
</th>
</tr>
</thead>
<tbody>
{features.map((feature, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-app-bg/30' : ''}>
<td className="border-b border-app-border py-3 px-4">{feature.name}</td>
<td className="border-b border-app-border py-3 px-4 text-center">
{feature.openEdison ? <CheckIcon /> : <CrossIcon />}
</td>
<td className="border-b border-app-border py-3 px-4 text-center">
{feature.edisonWatch ? <CheckIcon /> : <CrossIcon />}
</td>
</tr>
))}
</tbody>
</table>
</div>

<div className="mt-6 p-4 bg-app-bg/30 rounded border border-app-border">
<h3 className="font-semibold mb-2 text-amber-400">Enterprise Features Exclusive to EdisonWatch</h3>
<ul className="list-disc list-inside text-app-muted space-y-1">
<li><strong>Multi-Tenancy</strong>: Support for multiple isolated users and organizations</li>
<li><strong>SIEM Integration</strong>: Enterprise security information and event management</li>
<li><strong>SSO (Single Sign-On)</strong>: Integration with enterprise identity providers</li>
<li><strong>Client Software for Auto-Enforcement</strong>: Automated policy enforcement at the client level</li>
</ul>
</div>

<div className="mt-6 p-6 bg-gradient-to-r from-app-accent/10 to-app-accent/5 rounded-lg border-2 border-app-accent/30">
<div className="text-center space-y-4">
<h3 className="text-xl font-semibold text-app-accent">Interested in EdisonWatch Enterprise?</h3>
<p className="text-app-muted max-w-2xl mx-auto">
Schedule a personalized demo to see how EdisonWatch can secure your organization's AI agents with enterprise-grade features.
</p>
<a
href="https://cal.com/eito80/demo"
target="_blank"
rel="noopener noreferrer"
className="inline-block px-6 py-3 font-semibold rounded-lg hover:opacity-90 transition-opacity shadow-lg"
style={{ backgroundColor: 'var(--accent)', color: 'var(--bg)' }}
>
Book a Demo Call
</a>
</div>
</div>

<div className="mt-4 text-center text-sm text-app-muted">
<p>
For more information about EdisonWatch commercial licensing, please visit{' '}
<a
href="https://edisonwatch.com"
target="_blank"
rel="noopener noreferrer"
className="text-app-accent hover:underline"
>
edisonwatch.com
</a>
</p>
</div>
</div>
)
}
2 changes: 1 addition & 1 deletion frontend/src/components/DateRangeSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ export function DateRangeSlider({
<div className="mb-3" style={{ height: 80, overflow: 'hidden' }}>
<Line ref={sparkRef as any} height={60} data={{
labels: histogram.labels,
datasets: [{ label: 'sessions', data: histogram.data, borderColor: '#8b5cf6', backgroundColor: 'rgba(139,92,246,0.2)', tension: 0.3, pointRadius: 0 }],
datasets: [{ label: 'sessions', data: histogram.data, borderColor: '#C3FFFD', backgroundColor: 'rgba(195,255,253,0.2)', tension: 0.3, pointRadius: 0 }],
}} options={{
responsive: true,
maintainAspectRatio: false,
Expand Down
Loading
Loading