Add themed admin page with user management, audit log, and system info panels; add compliance note delete functionality
This commit is contained in:
@@ -6,8 +6,8 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
- [ ] 1. Create AdminPage component with page header and tab navigation
|
- [x] 1. Create AdminPage component with page header and tab navigation
|
||||||
- [ ] 1.1 Create `frontend/src/components/pages/AdminPage.js` with the page shell
|
- [x] 1.1 Create `frontend/src/components/pages/AdminPage.js` with the page shell
|
||||||
- Import React, useState, useAuth from AuthContext, and lucide-react icons (Shield, Clock, Activity)
|
- Import React, useState, useAuth from AuthContext, and lucide-react icons (Shield, Clock, Activity)
|
||||||
- Define `API_BASE` constant matching project convention
|
- Define `API_BASE` constant matching project convention
|
||||||
- Define `TABS` array: `[{ id: 'users', label: 'User Management', icon: Shield }, { id: 'audit', label: 'Audit Log', icon: Clock }, { id: 'system', label: 'System Info', icon: Activity }]`
|
- Define `TABS` array: `[{ id: 'users', label: 'User Management', icon: Shield }, { id: 'audit', label: 'Audit Log', icon: Clock }, { id: 'system', label: 'System Info', icon: Activity }]`
|
||||||
@@ -17,15 +17,15 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Conditionally render placeholder `<div>` for each panel based on `activeTab`
|
- Conditionally render placeholder `<div>` for each panel based on `activeTab`
|
||||||
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 2.4_
|
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 2.4_
|
||||||
|
|
||||||
- [ ] 1.2 Integrate AdminPage into App.js
|
- [x] 1.2 Integrate AdminPage into App.js
|
||||||
- Import `AdminPage` from `./components/pages/AdminPage`
|
- Import `AdminPage` from `./components/pages/AdminPage`
|
||||||
- Replace the existing `{currentPage === 'admin' && isAdmin() && (<div className="space-y-6"><UserManagement onClose={() => setCurrentPage('home')} /></div>)}` block with `{currentPage === 'admin' && isAdmin() && <AdminPage />}`
|
- Replace the existing `{currentPage === 'admin' && isAdmin() && (<div className="space-y-6"><UserManagement onClose={() => setCurrentPage('home')} /></div>)}` block with `{currentPage === 'admin' && isAdmin() && <AdminPage />}`
|
||||||
- Add non-admin redirect: `{currentPage === 'admin' && !isAdmin() && setCurrentPage('home')}` (or useEffect equivalent)
|
- Add non-admin redirect: `{currentPage === 'admin' && !isAdmin() && setCurrentPage('home')}` (or useEffect equivalent)
|
||||||
- Keep existing `{showUserManagement && <UserManagement onClose={...} />}` and `{showAuditLog && <AuditLog onClose={...} />}` modal triggers unchanged
|
- Keep existing `{showUserManagement && <UserManagement onClose={...} />}` and `{showAuditLog && <AuditLog onClose={...} />}` modal triggers unchanged
|
||||||
- _Requirements: 1.2, 6.1, 6.2, 6.3, 6.4_
|
- _Requirements: 1.2, 6.1, 6.2, 6.3, 6.4_
|
||||||
|
|
||||||
- [ ] 2. Implement UserManagementPanel
|
- [-] 2. Implement UserManagementPanel
|
||||||
- [ ] 2.1 Build the themed user table and group badges
|
- [x] 2.1 Build the themed user table and group badges
|
||||||
- Define `GROUP_BADGE_THEMED` map with themed colors: Admin → danger, Standard_User → accent, Leadership → warning, Read_Only → muted
|
- Define `GROUP_BADGE_THEMED` map with themed colors: Admin → danger, Standard_User → accent, Leadership → warning, Read_Only → muted
|
||||||
- Fetch users from `GET /api/users` with `credentials: 'include'` on panel mount
|
- Fetch users from `GET /api/users` with `credentials: 'include'` on panel mount
|
||||||
- Render user table with columns: username, email, group, active status, last login
|
- Render user table with columns: username, email, group, active status, last login
|
||||||
@@ -35,7 +35,7 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Display error banner with `--intel-danger` styling on fetch failure
|
- Display error banner with `--intel-danger` styling on fetch failure
|
||||||
- _Requirements: 3.1, 3.2, 3.3, 7.1, 7.2_
|
- _Requirements: 3.1, 3.2, 3.3, 7.1, 7.2_
|
||||||
|
|
||||||
- [ ] 2.2 Implement inline add/edit form and CRUD operations
|
- [x] 2.2 Implement inline add/edit form and CRUD operations
|
||||||
- Add "Add User" button styled with `intel-button` primary variant
|
- Add "Add User" button styled with `intel-button` primary variant
|
||||||
- Show inline form with `intel-input` styled fields for username, email, password, and group dropdown
|
- Show inline form with `intel-input` styled fields for username, email, password, and group dropdown
|
||||||
- On edit action: populate form with selected user's data (username, email, group; password blank)
|
- On edit action: populate form with selected user's data (username, email, group; password blank)
|
||||||
@@ -47,7 +47,7 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Display success toast with `--intel-success` color, auto-dismiss after 2 seconds
|
- Display success toast with `--intel-success` color, auto-dismiss after 2 seconds
|
||||||
- _Requirements: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 7.3_
|
- _Requirements: 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 7.3_
|
||||||
|
|
||||||
- [ ]* 2.3 Write property test: Group badge color mapping is total and correct
|
- [ ] 2.3 Write property test: Group badge color mapping is total and correct
|
||||||
- **Property 1: Group badge color mapping is total and correct**
|
- **Property 1: Group badge color mapping is total and correct**
|
||||||
- Install `fast-check` as a dev dependency in `frontend/`
|
- Install `fast-check` as a dev dependency in `frontend/`
|
||||||
- Create test file `frontend/src/components/pages/__tests__/AdminPage.property.test.js`
|
- Create test file `frontend/src/components/pages/__tests__/AdminPage.property.test.js`
|
||||||
@@ -56,14 +56,14 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 3.3**
|
- **Validates: Requirements 3.3**
|
||||||
|
|
||||||
- [ ]* 2.4 Write property test: Edit form population preserves user data
|
- [ ] 2.4 Write property test: Edit form population preserves user data
|
||||||
- **Property 2: Edit form population preserves user data**
|
- **Property 2: Edit form population preserves user data**
|
||||||
- Generate random user objects with arbitrary username, email, and group values
|
- Generate random user objects with arbitrary username, email, and group values
|
||||||
- Verify that populating the edit form results in `formData.username === user.username`, `formData.email === user.email`, `formData.group === user.group`, and `formData.password === ''`
|
- Verify that populating the edit form results in `formData.username === user.username`, `formData.email === user.email`, `formData.group === user.group`, and `formData.password === ''`
|
||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 3.5**
|
- **Validates: Requirements 3.5**
|
||||||
|
|
||||||
- [ ]* 2.5 Write property test: Self-modification prevention
|
- [ ] 2.5 Write property test: Self-modification prevention
|
||||||
- **Property 3: Self-modification prevention**
|
- **Property 3: Self-modification prevention**
|
||||||
- Generate random user lists that include a user matching the current admin's ID
|
- Generate random user lists that include a user matching the current admin's ID
|
||||||
- Verify the admin's own row has group dropdown disabled and active toggle disabled
|
- Verify the admin's own row has group dropdown disabled and active toggle disabled
|
||||||
@@ -71,11 +71,11 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 3.8**
|
- **Validates: Requirements 3.8**
|
||||||
|
|
||||||
- [ ] 3. Checkpoint — Verify user management panel
|
- [x] 3. Checkpoint — Verify user management panel
|
||||||
- Ensure all tests pass, ask the user if questions arise.
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
- [ ] 4. Implement AuditLogPanel
|
- [-] 4. Implement AuditLogPanel
|
||||||
- [ ] 4.1 Build the themed audit log table with action badges and filters
|
- [x] 4.1 Build the themed audit log table with action badges and filters
|
||||||
- Define `ACTION_BADGE_THEMED` map with themed colors: login/success → green, delete → danger, create → accent, update → warning, default → muted
|
- Define `ACTION_BADGE_THEMED` map with themed colors: login/success → green, delete → danger, create → accent, update → warning, default → muted
|
||||||
- Fetch audit logs from `GET /api/audit-logs?page=1&limit=25` with `credentials: 'include'` on panel mount
|
- Fetch audit logs from `GET /api/audit-logs?page=1&limit=25` with `credentials: 'include'` on panel mount
|
||||||
- Fetch action types from `GET /api/audit-logs/actions` for the action filter dropdown
|
- Fetch action types from `GET /api/audit-logs/actions` for the action filter dropdown
|
||||||
@@ -87,7 +87,7 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Display "No audit log entries found" message with `--text-muted` color when results are empty
|
- Display "No audit log entries found" message with `--text-muted` color when results are empty
|
||||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.9, 4.10, 7.1, 7.2_
|
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.9, 4.10, 7.1, 7.2_
|
||||||
|
|
||||||
- [ ] 4.2 Implement filter controls and pagination
|
- [x] 4.2 Implement filter controls and pagination
|
||||||
- Render filter bar with: username text input, action type dropdown, entity type dropdown, start date picker, end date picker
|
- Render filter bar with: username text input, action type dropdown, entity type dropdown, start date picker, end date picker
|
||||||
- Style all filter controls with `intel-input` and `intel-button` components
|
- Style all filter controls with `intel-input` and `intel-button` components
|
||||||
- On filter apply: re-fetch audit logs from page 1 with selected filter parameters
|
- On filter apply: re-fetch audit logs from page 1 with selected filter parameters
|
||||||
@@ -95,22 +95,22 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- On page change: fetch the requested page
|
- On page change: fetch the requested page
|
||||||
- _Requirements: 4.5, 4.6, 4.7, 4.8_
|
- _Requirements: 4.5, 4.6, 4.7, 4.8_
|
||||||
|
|
||||||
- [ ]* 4.3 Write property test: Action badge color mapping is total and correct
|
- [ ] 4.3 Write property test: Action badge color mapping is total and correct
|
||||||
- **Property 4: Action badge color mapping is total and correct**
|
- **Property 4: Action badge color mapping is total and correct**
|
||||||
- Generate random action strings including all known actions and arbitrary unknown strings
|
- Generate random action strings including all known actions and arbitrary unknown strings
|
||||||
- Verify the badge function returns correct themed colors for known actions and default muted styling for unknown actions
|
- Verify the badge function returns correct themed colors for known actions and default muted styling for unknown actions
|
||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 4.4**
|
- **Validates: Requirements 4.4**
|
||||||
|
|
||||||
- [ ]* 4.4 Write property test: Applying filters resets pagination to page 1
|
- [ ] 4.4 Write property test: Applying filters resets pagination to page 1
|
||||||
- **Property 5: Applying filters resets pagination to page 1**
|
- **Property 5: Applying filters resets pagination to page 1**
|
||||||
- Generate random filter combinations (username text, action type, entity type, start date, end date) and random current page numbers
|
- Generate random filter combinations (username text, action type, entity type, start date, end date) and random current page numbers
|
||||||
- Verify that applying filters results in a fetch call with `page=1`
|
- Verify that applying filters results in a fetch call with `page=1`
|
||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 4.7**
|
- **Validates: Requirements 4.7**
|
||||||
|
|
||||||
- [ ] 5. Implement SystemInfoPanel
|
- [-] 5. Implement SystemInfoPanel
|
||||||
- [ ] 5.1 Build stat cards and recent activity list
|
- [x] 5.1 Build stat cards and recent activity list
|
||||||
- Fetch users from `GET /api/users` and recent audit logs from `GET /api/audit-logs?limit=10&page=1` on panel mount
|
- Fetch users from `GET /api/users` and recent audit logs from `GET /api/audit-logs?limit=10&page=1` on panel mount
|
||||||
- Compute derived stats: total users (`users.length`), active users (`users.filter(u => u.is_active)`), recent logins (users with `last_login` within last 7 days), total audit entries (from pagination.total)
|
- Compute derived stats: total users (`users.length`), active users (`users.filter(u => u.is_active)`), recent logins (users with `last_login` within last 7 days), total audit entries (from pagination.total)
|
||||||
- Render four stat cards using the `stat-card` pattern with accent-colored top bar and hover lift effect
|
- Render four stat cards using the `stat-card` pattern with accent-colored top bar and hover lift effect
|
||||||
@@ -119,25 +119,25 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Display loading spinner while fetching
|
- Display loading spinner while fetching
|
||||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 7.1_
|
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 7.1_
|
||||||
|
|
||||||
- [ ]* 5.2 Write property test: Recent login count computation
|
- [ ] 5.2 Write property test: Recent login count computation
|
||||||
- **Property 6: Recent login count computation**
|
- **Property 6: Recent login count computation**
|
||||||
- Generate random user lists with random `last_login` timestamps (including null values)
|
- Generate random user lists with random `last_login` timestamps (including null values)
|
||||||
- Verify the computed "recent logins" count equals the number of users whose `last_login` is non-null and falls within the last 7 days
|
- Verify the computed "recent logins" count equals the number of users whose `last_login` is non-null and falls within the last 7 days
|
||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 5.1**
|
- **Validates: Requirements 5.1**
|
||||||
|
|
||||||
- [ ] 6. Checkpoint — Verify all panels and integration
|
- [x] 6. Checkpoint — Verify all panels and integration
|
||||||
- Ensure all tests pass, ask the user if questions arise.
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
- [ ] 7. Access control and final wiring
|
- [-] 7. Access control and final wiring
|
||||||
- [ ] 7.1 Verify access control integration
|
- [x] 7.1 Verify access control integration
|
||||||
- Confirm `AdminPage` reads auth context via `useAuth()` and only renders content for Admin-group users
|
- Confirm `AdminPage` reads auth context via `useAuth()` and only renders content for Admin-group users
|
||||||
- Confirm `App.js` redirects non-admin users to home when `currentPage === 'admin'`
|
- Confirm `App.js` redirects non-admin users to home when `currentPage === 'admin'`
|
||||||
- Confirm `NavDrawer` continues to show "Admin Panel" only for Admin-group users (no changes needed — verify existing behavior)
|
- Confirm `NavDrawer` continues to show "Admin Panel" only for Admin-group users (no changes needed — verify existing behavior)
|
||||||
- Confirm `UserMenu` quick-access links ("Manage Users", "Audit Log") continue to open existing modal components (no changes needed — verify existing behavior)
|
- Confirm `UserMenu` quick-access links ("Manage Users", "Audit Log") continue to open existing modal components (no changes needed — verify existing behavior)
|
||||||
- _Requirements: 6.1, 6.2, 6.3, 6.4_
|
- _Requirements: 6.1, 6.2, 6.3, 6.4_
|
||||||
|
|
||||||
- [ ]* 7.2 Write property test: Admin-only access control
|
- [ ] 7.2 Write property test: Admin-only access control
|
||||||
- **Property 7: Admin-only access control**
|
- **Property 7: Admin-only access control**
|
||||||
- Generate random user objects with random group values
|
- Generate random user objects with random group values
|
||||||
- Verify admin page content renders if and only if `user.group === 'Admin'`
|
- Verify admin page content renders if and only if `user.group === 'Admin'`
|
||||||
@@ -145,7 +145,7 @@ Replace the current inline `UserManagement` modal rendering on the admin page wi
|
|||||||
- Use `fc.assert(property, { numRuns: 100 })`
|
- Use `fc.assert(property, { numRuns: 100 })`
|
||||||
- **Validates: Requirements 6.1, 6.2**
|
- **Validates: Requirements 6.1, 6.2**
|
||||||
|
|
||||||
- [ ] 8. Final checkpoint — Ensure all tests pass
|
- [x] 8. Final checkpoint — Ensure all tests pass
|
||||||
- Ensure all tests pass, ask the user if questions arise.
|
- Ensure all tests pass, ask the user if questions arise.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -891,6 +891,68 @@ function createComplianceRouter(db, upload, requireAuth, requireGroup) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// DELETE /notes/:id
|
||||||
|
// Delete a note (or all notes in the same group_id) by note ID.
|
||||||
|
// Only the note author or an Admin can delete.
|
||||||
|
//
|
||||||
|
// Params: id — note row ID
|
||||||
|
// Query: ?group=true — delete all notes sharing the same group_id
|
||||||
|
// Response: { deleted: number }
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
router.delete('/notes/:id', requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||||
|
const noteId = parseInt(req.params.id, 10);
|
||||||
|
if (isNaN(noteId)) return res.status(400).json({ error: 'Invalid note ID' });
|
||||||
|
|
||||||
|
const deleteGroup = req.query.group === 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch the note to verify ownership
|
||||||
|
const note = await dbGet(db,
|
||||||
|
`SELECT id, hostname, metric_id, note, group_id, created_by FROM compliance_notes WHERE id = ?`,
|
||||||
|
[noteId]
|
||||||
|
);
|
||||||
|
if (!note) return res.status(404).json({ error: 'Note not found' });
|
||||||
|
|
||||||
|
// Only the author or an Admin can delete
|
||||||
|
const isAuthor = req.user && String(req.user.id) === String(note.created_by);
|
||||||
|
const isAdminUser = req.user && req.user.group === 'Admin';
|
||||||
|
if (!isAuthor && !isAdminUser) {
|
||||||
|
return res.status(403).json({ error: 'You can only delete your own notes' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
if (deleteGroup && note.group_id) {
|
||||||
|
const result = await dbRun(db,
|
||||||
|
`DELETE FROM compliance_notes WHERE group_id = ?`,
|
||||||
|
[note.group_id]
|
||||||
|
);
|
||||||
|
deleted = result.changes || 0;
|
||||||
|
} else {
|
||||||
|
const result = await dbRun(db,
|
||||||
|
`DELETE FROM compliance_notes WHERE id = ?`,
|
||||||
|
[noteId]
|
||||||
|
);
|
||||||
|
deleted = result.changes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
logAudit(db, {
|
||||||
|
userId: req.user.id,
|
||||||
|
username: req.user.username,
|
||||||
|
action: 'compliance_note_delete',
|
||||||
|
entityType: 'compliance_note',
|
||||||
|
entityId: String(noteId),
|
||||||
|
details: JSON.stringify({ hostname: note.hostname, group_id: note.group_id, deleted_count: deleted }),
|
||||||
|
ipAddress: req.ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ deleted });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Compliance] DELETE /notes error:', err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to delete note' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// GET /trends
|
// GET /trends
|
||||||
// Per-upload active totals + per-team counts for time-series charts.
|
// Per-upload active totals + per-team counts for time-series charts.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import VulnerabilityTriagePage from './components/pages/ReportingPage';
|
|||||||
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
import KnowledgeBasePage from './components/pages/KnowledgeBasePage';
|
||||||
import ExportsPage from './components/pages/ExportsPage';
|
import ExportsPage from './components/pages/ExportsPage';
|
||||||
import CompliancePage from './components/pages/CompliancePage';
|
import CompliancePage from './components/pages/CompliancePage';
|
||||||
|
import AdminPage from './components/pages/AdminPage';
|
||||||
import ArchiveSummaryBar from './components/pages/ArchiveSummaryBar';
|
import ArchiveSummaryBar from './components/pages/ArchiveSummaryBar';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -1012,11 +1013,8 @@ export default function App() {
|
|||||||
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
{currentPage === 'compliance' && <CompliancePage onNavigate={setCurrentPage} />}
|
||||||
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
{currentPage === 'knowledge-base' && <KnowledgeBasePage />}
|
||||||
{currentPage === 'exports' && <ExportsPage />}
|
{currentPage === 'exports' && <ExportsPage />}
|
||||||
{currentPage === 'admin' && isAdmin() && (
|
{currentPage === 'admin' && isAdmin() && <AdminPage />}
|
||||||
<div className="space-y-6">
|
{currentPage === 'admin' && !isAdmin() && (() => { setCurrentPage('home'); return null; })()}
|
||||||
<UserManagement onClose={() => setCurrentPage('home')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* User Management Modal */}
|
{/* User Management Modal */}
|
||||||
{showUserManagement && (
|
{showUserManagement && (
|
||||||
|
|||||||
1382
frontend/src/components/pages/AdminPage.js
Normal file
1382
frontend/src/components/pages/AdminPage.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield } from 'lucide-react';
|
import { X, MessageSquare, Send, Loader, AlertCircle, Clock, Shield, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
||||||
|
|
||||||
@@ -90,6 +90,22 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteNote = async (noteId, hasGroup) => {
|
||||||
|
if (!window.confirm('Delete this note?')) return;
|
||||||
|
try {
|
||||||
|
const url = hasGroup
|
||||||
|
? `${API_BASE}/compliance/notes/${noteId}?group=true`
|
||||||
|
: `${API_BASE}/compliance/notes/${noteId}`;
|
||||||
|
const res = await fetch(url, { method: 'DELETE', credentials: 'include' });
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Failed to delete note');
|
||||||
|
await fetchDetail();
|
||||||
|
if (onNoteAdded) onNoteAdded();
|
||||||
|
} catch (err) {
|
||||||
|
setNoteError(err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || [];
|
const activeMetrics = detail?.metrics?.filter(m => m.status === 'active') || [];
|
||||||
const resolvedMetrics = detail?.metrics?.filter(m => m.status === 'resolved') || [];
|
const resolvedMetrics = detail?.metrics?.filter(m => m.status === 'resolved') || [];
|
||||||
|
|
||||||
@@ -227,9 +243,25 @@ export default function ComplianceDetailPanel({ hostname, onClose, onNoteAdded,
|
|||||||
<MetricChip key={n.id} metricId={n.metric_id} category={metricMap[n.metric_id] || ''} />
|
<MetricChip key={n.id} metricId={n.metric_id} category={metricMap[n.metric_id] || ''} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace', flexShrink: 0, marginLeft: '0.5rem', whiteSpace: 'nowrap' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', flexShrink: 0, marginLeft: '0.5rem' }}>
|
||||||
|
<span style={{ fontSize: '0.68rem', color: '#334155', fontFamily: 'monospace', whiteSpace: 'nowrap' }}>
|
||||||
{g.created_by && `${g.created_by} · `}{g.created_at?.slice(0, 10)}
|
{g.created_by && `${g.created_by} · `}{g.created_at?.slice(0, 10)}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteNote(g.notes[0].id, !!g.notes[0].group_id)}
|
||||||
|
title="Delete note"
|
||||||
|
style={{
|
||||||
|
background: 'none', border: '1px solid rgba(239,68,68,0.15)',
|
||||||
|
borderRadius: '0.25rem', padding: '0.2rem',
|
||||||
|
cursor: 'pointer', color: '#334155',
|
||||||
|
transition: 'all 0.15s', lineHeight: 1,
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.5)'; }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.color = '#334155'; e.currentTarget.style.borderColor = 'rgba(239,68,68,0.15)'; }}
|
||||||
|
>
|
||||||
|
<Trash2 style={{ width: '11px', height: '11px' }} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '0.8rem', color: '#CBD5E1', lineHeight: 1.5 }}>{g.note}</div>
|
<div style={{ fontSize: '0.8rem', color: '#CBD5E1', lineHeight: 1.5 }}>{g.note}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user