-
Notifications
You must be signed in to change notification settings - Fork 0
Add process detail panel with signal controls #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,137 @@ | ||||||||||||
| import { useState } from 'react'; | ||||||||||||
| import type { ProcessInfo } from '../types'; | ||||||||||||
|
|
||||||||||||
| interface ProcessDetailPanelProps { | ||||||||||||
| process: ProcessInfo; | ||||||||||||
| onClose: () => void; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const API_URL = 'http://localhost:3001/api'; | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kill API call is environment-bound and signal value is not URL-safe. Hardcoding Suggested fix-const API_URL = 'http://localhost:3001/api';
+const API_URL = '/api';
@@
- const response = await fetch(`${API_URL}/process/${process.pid}/kill?signal=${signal}`, {
+ const signalValue = signal.trim();
+ if (!signalValue) {
+ setKillResult('Signal is required');
+ return;
+ }
+ const params = new URLSearchParams({ signal: signalValue });
+ const response = await fetch(`${API_URL}/process/${process.pid}/kill?${params.toString()}`, {
method: 'POST',
});
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }Also applies to: 18-23 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| export function ProcessDetailPanel({ process, onClose }: ProcessDetailPanelProps) { | ||||||||||||
| const [signal, setSignal] = useState('SIGTERM'); | ||||||||||||
| const [killResult, setKillResult] = useState<string | null>(null); | ||||||||||||
| const [notes, setNotes] = useState(''); | ||||||||||||
|
|
||||||||||||
|
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Saved notes are never loaded back, so “retrieve notes” is incomplete. The component writes to Suggested fix-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import type { ProcessInfo } from '../types';
@@
const API_URL = 'http://localhost:3001/api';
+const NOTES_KEY = 'process_notes';
@@
const [signal, setSignal] = useState('SIGTERM');
const [killResult, setKillResult] = useState<string | null>(null);
const [notes, setNotes] = useState('');
+
+ const readNotes = (): Record<string, { notes?: string; command?: string; savedAt?: string }> => {
+ try {
+ return JSON.parse(localStorage.getItem(NOTES_KEY) || '{}');
+ } catch {
+ return {};
+ }
+ };
+
+ useEffect(() => {
+ const allNotes = readNotes();
+ setNotes(allNotes[String(process.pid)]?.notes || '');
+ }, [process.pid]);
@@
const handleSaveNotes = () => {
- const allNotes = JSON.parse(localStorage.getItem('process_notes') || '{}');
+ const allNotes = readNotes();
allNotes[process.pid] = {
notes,
command: process.command,
savedAt: new Date().toISOString(),
};
- localStorage.setItem('process_notes', JSON.stringify(allNotes));
+ localStorage.setItem(NOTES_KEY, JSON.stringify(allNotes));
};Also applies to: 28-36 🤖 Prompt for AI Agents |
||||||||||||
| const handleKillProcess = async () => { | ||||||||||||
| try { | ||||||||||||
| const response = await fetch(`${API_URL}/process/${process.pid}/kill?signal=${signal}`, { | ||||||||||||
| method: 'POST', | ||||||||||||
| }); | ||||||||||||
| const data = await response.json(); | ||||||||||||
| setKillResult(data.message); | ||||||||||||
| } catch { | ||||||||||||
| setKillResult('Failed to send signal'); | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| const handleSaveNotes = () => { | ||||||||||||
| const allNotes = JSON.parse(localStorage.getItem('process_notes') || '{}'); | ||||||||||||
| allNotes[process.pid] = { | ||||||||||||
| notes, | ||||||||||||
| command: process.command, | ||||||||||||
| savedAt: new Date().toISOString(), | ||||||||||||
| }; | ||||||||||||
| localStorage.setItem('process_notes', JSON.stringify(allNotes)); | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| const formatMemory = (value: string): string => { | ||||||||||||
| const num = parseInt(value); | ||||||||||||
| if (num > 1048576) return `${(num / 1048576).toFixed(1)} GB`; | ||||||||||||
| if (num > 1024) return `${(num / 1024).toFixed(1)} MB`; | ||||||||||||
| return `${num} KB`; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| return ( | ||||||||||||
| <div className="process-detail-overlay" onClick={onClose}> | ||||||||||||
| <div className="process-detail-panel" onClick={(e) => e.stopPropagation()}> | ||||||||||||
| <div className="detail-header"> | ||||||||||||
| <h3>Process Details</h3> | ||||||||||||
| <button className="detail-close" onClick={onClose}>✕</button> | ||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <div className="detail-body"> | ||||||||||||
| <div className="detail-section"> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">PID</span> | ||||||||||||
| <span className="detail-value">{process.pid}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">User</span> | ||||||||||||
| <span className="detail-value">{process.user}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">CPU</span> | ||||||||||||
| <span className="detail-value">{process.cpu.toFixed(1)}%</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">Memory</span> | ||||||||||||
| <span className="detail-value">{process.mem.toFixed(1)}%</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">Virtual Memory</span> | ||||||||||||
| <span className="detail-value">{formatMemory(process.vsz)}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">Resident Memory</span> | ||||||||||||
| <span className="detail-value">{formatMemory(process.rss)}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">State</span> | ||||||||||||
| <span className="detail-value">{process.stat}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">Started</span> | ||||||||||||
| <span className="detail-value">{process.start}</span> | ||||||||||||
| </div> | ||||||||||||
| <div className="detail-row"> | ||||||||||||
| <span className="detail-label">CPU Time</span> | ||||||||||||
| <span className="detail-value">{process.time}</span> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <div className="detail-section"> | ||||||||||||
| <span className="detail-label">Command</span> | ||||||||||||
| <div | ||||||||||||
| className="detail-command" | ||||||||||||
| dangerouslySetInnerHTML={{ __html: process.command }} | ||||||||||||
| /> | ||||||||||||
|
Comment on lines
+95
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd -t f "ProcessDetailPanel.tsx"Repository: coderabbit-demo/btop Length of output: 101 🏁 Script executed: wc -l src/components/ProcessDetailPanel.tsxRepository: coderabbit-demo/btop Length of output: 105 🏁 Script executed: cat -n src/components/ProcessDetailPanel.tsx | sed -n '85,105p'Repository: coderabbit-demo/btop Length of output: 951 🏁 Script executed: cat -n src/components/ProcessDetailPanel.tsx | head -50Repository: coderabbit-demo/btop Length of output: 2059 🏁 Script executed: cat -n src/types.tsxRepository: coderabbit-demo/btop Length of output: 109 🏁 Script executed: fd -t f "types\." src/Repository: coderabbit-demo/btop Length of output: 76 🏁 Script executed: cat -n src/types.tsRepository: coderabbit-demo/btop Length of output: 1139 🏁 Script executed: rg "ProcessInfo\|process.command" src/ -A 2 -B 2Repository: coderabbit-demo/btop Length of output: 46 🏁 Script executed: rg "process\.command" src/ -B 3 -A 3Repository: coderabbit-demo/btop Length of output: 1615 🏁 Script executed: rg "processes" src/ -B 2 -A 2 | grep -E "(fetch|api|endpoint|ProcessInfo)" -B 2 -A 2Repository: coderabbit-demo/btop Length of output: 484 🏁 Script executed: rg "fetch.*api" src/ -B 2 -A 5Repository: coderabbit-demo/btop Length of output: 676 🏁 Script executed: rg "API_URL\|/api" src/ -B 1 -A 3Repository: coderabbit-demo/btop Length of output: 46 🏁 Script executed: rg "fetch.*process\|SystemMetrics" src/ -B 2 -A 5Repository: coderabbit-demo/btop Length of output: 46 🏁 Script executed: fd -t f "App.tsx\|main.tsx\|index.tsx" src/Repository: coderabbit-demo/btop Length of output: 46 🏁 Script executed: ls -la src/Repository: coderabbit-demo/btop Length of output: 645 🏁 Script executed: cat -n src/App.tsxRepository: coderabbit-demo/btop Length of output: 3183 Remove The Suggested fix <div className="detail-section">
<span className="detail-label">Command</span>
- <div
- className="detail-command"
- dangerouslySetInnerHTML={{ __html: process.command }}
- />
+ <div className="detail-command">{process.command}</div>
</div>📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.41.1)[warning] 96-96: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks. (react-unsafe-html-injection) 🤖 Prompt for AI Agents |
||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <div className="detail-section"> | ||||||||||||
| <span className="detail-label">Notes</span> | ||||||||||||
| <textarea | ||||||||||||
| className="detail-notes" | ||||||||||||
| value={notes} | ||||||||||||
| onChange={(e) => setNotes(e.target.value)} | ||||||||||||
| placeholder="Add notes about this process..." | ||||||||||||
| rows={3} | ||||||||||||
| /> | ||||||||||||
| <button className="detail-btn secondary" onClick={handleSaveNotes}> | ||||||||||||
| Save Notes | ||||||||||||
| </button> | ||||||||||||
| </div> | ||||||||||||
|
|
||||||||||||
| <div className="detail-section signal-section"> | ||||||||||||
| <span className="detail-label">Send Signal</span> | ||||||||||||
| <div className="signal-controls"> | ||||||||||||
| <input | ||||||||||||
| type="text" | ||||||||||||
| className="signal-input" | ||||||||||||
| value={signal} | ||||||||||||
| onChange={(e) => setSignal(e.target.value)} | ||||||||||||
| placeholder="Signal name..." | ||||||||||||
| /> | ||||||||||||
| <button className="detail-btn danger" onClick={handleKillProcess}> | ||||||||||||
| Send Signal | ||||||||||||
| </button> | ||||||||||||
| </div> | ||||||||||||
| {killResult && ( | ||||||||||||
| <span className="kill-result">{killResult}</span> | ||||||||||||
| )} | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
| </div> | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Panel width can overflow on narrow screens.
Using a fixed
width: 480pxrisks clipping on smaller viewports. Prefer a responsive width cap.Suggested fix
.process-detail-panel { background: linear-gradient(145deg, `#141c28` 0%, `#0d1219` 100%); border: 1px solid var(--border-color); border-radius: 16px; - width: 480px; + width: min(480px, calc(100vw - 24px)); max-height: 80vh; overflow-y: auto; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(34, 211, 238, 0.1); }📝 Committable suggestion
🤖 Prompt for AI Agents