chore: sync submodule state (parent ref update)
Made-with: Cursor
This commit is contained in:
42
frontend/solacenet-console/README.md
Normal file
42
frontend/solacenet-console/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# SolaceNet Operations Console
|
||||
|
||||
React-based admin UI for managing SolaceNet capabilities, entitlements, and policies.
|
||||
|
||||
## Features
|
||||
|
||||
- Capability management and toggling
|
||||
- Entitlement configuration
|
||||
- Policy rule management
|
||||
- Audit log viewing
|
||||
- Kill switch controls
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd frontend/solacenet-console
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```
|
||||
REACT_APP_API_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Login with admin credentials
|
||||
2. View all capabilities in the main table
|
||||
3. Click "Manage" to toggle capability states
|
||||
4. Use "Kill Switch" for emergency capability disabling
|
||||
5. View audit logs for all changes
|
||||
|
||||
## Development
|
||||
|
||||
The console connects to the SolaceNet API endpoints:
|
||||
- `/api/v1/solacenet/capabilities`
|
||||
- `/api/v1/solacenet/policy/kill-switch/:id`
|
||||
- `/api/v1/solacenet/audit/toggles`
|
||||
33
frontend/solacenet-console/package.json
Normal file
33
frontend/solacenet-console/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "solacenet-console",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
53
frontend/solacenet-console/src/App.css
Normal file
53
frontend/solacenet-console/src/App.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #333;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tabs button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: #e9ecef;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tabs button:hover {
|
||||
background-color: #dee2e6;
|
||||
}
|
||||
|
||||
.tabs button.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
min-height: 600px;
|
||||
}
|
||||
40
frontend/solacenet-console/src/App.tsx
Normal file
40
frontend/solacenet-console/src/App.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// SolaceNet Operations Console
|
||||
// React/TypeScript admin UI for capability management
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { CapabilityManager } from './components/CapabilityManager';
|
||||
import { AuditLogViewer } from './components/AuditLogViewer';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState<'capabilities' | 'audit'>('capabilities');
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<header>
|
||||
<h1>SolaceNet Operations Console</h1>
|
||||
<nav className="tabs">
|
||||
<button
|
||||
className={activeTab === 'capabilities' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('capabilities')}
|
||||
>
|
||||
Capabilities
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === 'audit' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('audit')}
|
||||
>
|
||||
Audit Logs
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="content">
|
||||
{activeTab === 'capabilities' && <CapabilityManager />}
|
||||
{activeTab === 'audit' && <AuditLogViewer />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
67
frontend/solacenet-console/src/components/AuditLogViewer.css
Normal file
67
frontend/solacenet-console/src/components/AuditLogViewer.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.audit-log-viewer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filters input,
|
||||
.filters select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.logs-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.logs-table th,
|
||||
.logs-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.action-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.action-enabled {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.action-disabled {
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-suspended {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.action-kill_switch {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
121
frontend/solacenet-console/src/components/AuditLogViewer.tsx
Normal file
121
frontend/solacenet-console/src/components/AuditLogViewer.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './AuditLogViewer.css';
|
||||
|
||||
interface AuditLog {
|
||||
id: string;
|
||||
actor: string;
|
||||
action: string;
|
||||
capabilityId: string;
|
||||
beforeState?: string;
|
||||
afterState: string;
|
||||
timestamp: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:3000';
|
||||
|
||||
export const AuditLogViewer: React.FC = () => {
|
||||
const [logs, setLogs] = useState<AuditLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filters, setFilters] = useState({
|
||||
capabilityId: '',
|
||||
actor: '',
|
||||
action: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [filters]);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.capabilityId) params.append('capabilityId', filters.capabilityId);
|
||||
if (filters.actor) params.append('actor', filters.actor);
|
||||
if (filters.action) params.append('action', filters.action);
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/v1/solacenet/audit/toggles?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
setLogs(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch audit logs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading audit logs...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="audit-log-viewer">
|
||||
<h2>Audit Logs</h2>
|
||||
|
||||
<div className="filters">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Capability ID"
|
||||
value={filters.capabilityId}
|
||||
onChange={(e) => setFilters({ ...filters, capabilityId: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Actor"
|
||||
value={filters.actor}
|
||||
onChange={(e) => setFilters({ ...filters, actor: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
value={filters.action}
|
||||
onChange={(e) => setFilters({ ...filters, action: e.target.value })}
|
||||
>
|
||||
<option value="">All Actions</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="kill_switch">Kill Switch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="logs-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Actor</th>
|
||||
<th>Action</th>
|
||||
<th>Capability</th>
|
||||
<th>Before</th>
|
||||
<th>After</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id}>
|
||||
<td>{new Date(log.timestamp).toLocaleString()}</td>
|
||||
<td>{log.actor}</td>
|
||||
<td>
|
||||
<span className={`action-badge action-${log.action}`}>
|
||||
{log.action}
|
||||
</span>
|
||||
</td>
|
||||
<td>{log.capabilityId}</td>
|
||||
<td>{log.beforeState || '-'}</td>
|
||||
<td>{log.afterState}</td>
|
||||
<td>{log.reason || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
112
frontend/solacenet-console/src/components/CapabilityManager.css
Normal file
112
frontend/solacenet-console/src/components/CapabilityManager.css
Normal file
@@ -0,0 +1,112 @@
|
||||
.capability-manager {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tenant-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tenant-selector input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.capabilities-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.capability-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.capability-card h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.capability-id {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.state-indicator {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.state-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.state-disabled {
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.state-pilot {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.state-enabled {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.state-suspended {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.state-drain {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.actions select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actions select:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
165
frontend/solacenet-console/src/components/CapabilityManager.tsx
Normal file
165
frontend/solacenet-console/src/components/CapabilityManager.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import './CapabilityManager.css';
|
||||
|
||||
interface Capability {
|
||||
id: string;
|
||||
capabilityId: string;
|
||||
name: string;
|
||||
version: string;
|
||||
defaultState: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Entitlement {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
capabilityId: string;
|
||||
stateOverride?: string;
|
||||
}
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:3000';
|
||||
|
||||
export const CapabilityManager: React.FC = () => {
|
||||
const [capabilities, setCapabilities] = useState<Capability[]>([]);
|
||||
const [entitlements, setEntitlements] = useState<Entitlement[]>([]);
|
||||
const [selectedTenant, setSelectedTenant] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCapabilities();
|
||||
if (selectedTenant) {
|
||||
fetchEntitlements(selectedTenant);
|
||||
}
|
||||
}, [selectedTenant]);
|
||||
|
||||
const fetchCapabilities = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/v1/solacenet/capabilities`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
setCapabilities(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch capabilities:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEntitlements = async (tenantId: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/v1/solacenet/tenants/${tenantId}/programs/entitlements`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
setEntitlements(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch entitlements:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCapability = async (capabilityId: string, newState: string) => {
|
||||
try {
|
||||
// Create or update entitlement
|
||||
const existing = entitlements.find(e => e.capabilityId === capabilityId);
|
||||
|
||||
if (existing) {
|
||||
// Update existing entitlement
|
||||
await fetch(`${API_BASE}/api/v1/solacenet/entitlements/${existing.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
stateOverride: newState,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// Create new entitlement
|
||||
await fetch(`${API_BASE}/api/v1/solacenet/entitlements`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: selectedTenant,
|
||||
capabilityId,
|
||||
stateOverride: newState,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await fetchEntitlements(selectedTenant);
|
||||
alert(`Capability ${capabilityId} set to ${newState}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle capability:', error);
|
||||
alert('Failed to toggle capability');
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentState = (capabilityId: string): string => {
|
||||
const entitlement = entitlements.find(e => e.capabilityId === capabilityId);
|
||||
return entitlement?.stateOverride || 'disabled';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading capabilities...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="capability-manager">
|
||||
<div className="header">
|
||||
<h2>Capability Management</h2>
|
||||
<div className="tenant-selector">
|
||||
<label>Tenant ID:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedTenant}
|
||||
onChange={(e) => setSelectedTenant(e.target.value)}
|
||||
placeholder="Enter tenant ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="capabilities-grid">
|
||||
{capabilities.map((cap) => {
|
||||
const currentState = getCurrentState(cap.capabilityId);
|
||||
return (
|
||||
<div key={cap.id} className="capability-card">
|
||||
<h3>{cap.name}</h3>
|
||||
<p className="capability-id">{cap.capabilityId}</p>
|
||||
<p className="version">v{cap.version}</p>
|
||||
<div className="state-indicator">
|
||||
<span className={`state-badge state-${currentState}`}>
|
||||
{currentState}
|
||||
</span>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<select
|
||||
value={currentState}
|
||||
onChange={(e) => toggleCapability(cap.capabilityId, e.target.value)}
|
||||
disabled={!selectedTenant}
|
||||
>
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="pilot">Pilot</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="suspended">Suspended</option>
|
||||
<option value="drain">Drain</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user