feat: Implement initial Process Scheduling Simulator application with core logic, algorithms, UI components, and project setup.

This commit is contained in:
Lucas Guerra committed 2026-03-19 14:12:21 -03:00
commit 083fc0027e
54 files changed
+8815

No files matched your search

@@ -0,0 +1,73 @@
import { Pencil, Trash2 } from 'lucide-react';
import type { Process } from '@core/types';
import IconButton from '../shared/IconButton';
import Badge from '../shared/Badge';
import { classifyBound } from '../shared/processUtils';
interface ProcessCardProps {
process: Process;
onEdit: (process: Process) => void;
onRemove: (id: string) => void;
}
const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
const bound = classifyBound(process.bursts);
const totalCpu = process.bursts.filter(b => b.type === 'cpu').reduce((a, b) => a + b.duration, 0);
const totalIo = process.bursts.filter(b => b.type === 'io').reduce((a, b) => a + b.duration, 0);
const boundVariant = bound === 'CPU Bound' ? 'cpu' : bound === 'I/O Bound' ? 'io' : 'balanced';
return (
<div
className="group relative flex items-start gap-3 rounded-lg bg-slate-800 border border-slate-700 p-3 hover:border-slate-500 transition-colors"
data-testid={`process-card-${process.id}`}
>
{/* Color indicator */}
<div
className="mt-0.5 h-8 w-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: process.color }}
aria-hidden="true"
/>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-sm text-slate-100 truncate">{process.name}</span>
<Badge variant={boundVariant}>{bound}</Badge>
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-slate-400">
<span>Arrival: <span className="text-slate-300 font-medium">{process.arrivalTime}</span></span>
<span>Priority: <span className="text-slate-300 font-medium">{process.priority}</span></span>
<span>CPU: <span className="text-emerald-400 font-medium">{totalCpu}</span></span>
{totalIo > 0 && (
<span>I/O: <span className="text-cyan-400 font-medium">{totalIo}</span></span>
)}
<span>Bursts: <span className="text-slate-300 font-medium">{process.bursts.length}</span></span>
</div>
</div>
{/* Actions */}
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
<IconButton
size="sm"
variant="ghost"
label={`Editar ${process.name}`}
onClick={() => onEdit(process)}
>
<Pencil size={13} />
</IconButton>
<IconButton
size="sm"
variant="ghost"
label={`Remover ${process.name}`}
onClick={() => onRemove(process.id)}
className="hover:text-red-400 hover:bg-red-900/20"
>
<Trash2 size={13} />
</IconButton>
</div>
</div>
);
};
export default ProcessCard;
+256
View File
@@ -0,0 +1,256 @@
import { useState, useEffect } from 'react';
import { Plus, Minus, Check, X } from 'lucide-react';
import type { Process, BurstSegment } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
interface ProcessFormProps {
/** If provided, the form edits this process; otherwise creates a new one. */
editingProcess?: Process | null;
onClose: () => void;
}
const DEFAULT_BURSTS: BurstSegment[] = [{ type: 'cpu', duration: 4 }];
const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const addProcess = useProcessStore(s => s.addProcess);
const updateProcess = useProcessStore(s => s.updateProcess);
const isEditing = !!editingProcess;
const [name, setName] = useState('');
const [arrivalTime, setArrivalTime] = useState(0);
const [priority, setPriority] = useState(5);
const [color, setColor] = useState(nextColor);
const [bursts, setBursts] = useState<BurstSegment[]>(DEFAULT_BURSTS);
const [errors, setErrors] = useState<string[]>([]);
useEffect(() => {
if (editingProcess) {
setName(editingProcess.name);
setArrivalTime(editingProcess.arrivalTime);
setPriority(editingProcess.priority);
setColor(editingProcess.color);
setBursts([...editingProcess.bursts.map(b => ({ ...b }))]);
} else {
setName('');
setArrivalTime(0);
setPriority(5);
setColor(nextColor());
setBursts([{ type: 'cpu', duration: 4 }]);
}
setErrors([]);
}, [editingProcess]);
// ---- Burst helpers ----
const updateBurstDuration = (index: number, value: number) => {
setBursts(prev => prev.map((b, i) => i === index ? { ...b, duration: Math.max(1, value) } : b));
};
const addBurst = () => {
// enforce alternating cpu/io pattern
const lastType = bursts[bursts.length - 1]?.type ?? 'io';
const nextType: BurstSegment['type'] = lastType === 'cpu' ? 'io' : 'cpu';
setBursts(prev => [...prev, { type: nextType, duration: 2 }]);
};
const removeBurst = (index: number) => {
if (bursts.length <= 1) return;
setBursts(prev => {
const next = prev.filter((_, i) => i !== index);
// Safe-guard: ensure it still starts (and ends) with cpu
return next;
});
};
// ---- Validation ----
const validate = (): boolean => {
const errs: string[] = [];
if (!name.trim()) errs.push('Nome é obrigatório');
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres');
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10');
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU');
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
if (i > 0 && bursts[i - 1].type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
});
setErrors(errs);
return errs.length === 0;
};
const handleSubmit = () => {
if (!validate()) return;
if (isEditing && editingProcess) {
updateProcess(editingProcess.id, { name: name.trim(), arrivalTime, priority, color, bursts });
} else {
const p: Process = {
id: generateId(),
name: name.trim(),
arrivalTime,
priority,
color,
bursts,
};
addProcess(p);
}
onClose();
};
const inputCls = 'w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500';
const labelCls = 'block text-xs text-slate-400 mb-1';
return (
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-200">
{isEditing ? 'Editar Processo' : 'Novo Processo'}
</h3>
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}>
<X size={14} />
</IconButton>
</div>
{/* Basic fields */}
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2 flex gap-3">
{/* Name */}
<div className="flex-1">
<label className={labelCls} htmlFor="pf-name">Nome</label>
<input
id="pf-name"
type="text"
maxLength={8}
value={name}
onChange={e => setName(e.target.value)}
placeholder="P1"
className={inputCls}
aria-label="Nome do processo"
/>
</div>
{/* Color */}
<div>
<label className={labelCls} htmlFor="pf-color">Cor</label>
<input
id="pf-color"
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="h-[34px] w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
aria-label="Cor do processo"
/>
</div>
</div>
{/* Arrival */}
<div>
<label className={labelCls} htmlFor="pf-arrival">Arrival Time</label>
<input
id="pf-arrival"
type="number"
min={0}
value={arrivalTime}
onChange={e => setArrivalTime(Number(e.target.value))}
className={inputCls}
aria-label="Tempo de chegada"
/>
</div>
{/* Priority */}
<div>
<label className={labelCls} htmlFor="pf-priority">Priority (1–10)</label>
<input
id="pf-priority"
type="number"
min={1}
max={10}
value={priority}
onChange={e => setPriority(Number(e.target.value))}
className={inputCls}
aria-label="Prioridade do processo"
/>
</div>
</div>
{/* Bursts editor */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
<IconButton size="sm" variant="ghost" label="Adicionar burst" onClick={addBurst}>
<Plus size={13} />
</IconButton>
</div>
<div className="space-y-1.5" data-testid="burst-list">
{bursts.map((burst, idx) => (
<div
key={idx}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md border ${
burst.type === 'cpu'
? 'border-emerald-700/50 bg-emerald-900/20'
: 'border-cyan-700/50 bg-cyan-900/20'
}`}
>
<span className={`text-[10px] font-bold uppercase w-7 tracking-wider ${burst.type === 'cpu' ? 'text-emerald-400' : 'text-cyan-400'}`}>
{burst.type}
</span>
<input
type="number"
min={1}
value={burst.duration}
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Duração do burst ${idx + 1}`}
/>
<span className="text-[10px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label={`Remover burst ${idx + 1}`}
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="hover:text-red-400"
>
<Minus size={12} />
</IconButton>
</div>
))}
</div>
</div>
{/* Validation errors */}
{errors.length > 0 && (
<ul className="space-y-0.5" role="alert" aria-live="polite">
{errors.map((e, i) => (
<li key={i} className="text-[11px] text-red-400">{e}</li>
))}
</ul>
)}
{/* Actions */}
<div className="flex gap-2 pt-1">
<button
onClick={handleSubmit}
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'}
>
<Check size={15} />
{isEditing ? 'Salvar' : 'Criar'}
</button>
<button
onClick={onClose}
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
aria-label="Cancelar"
>
Cancelar
</button>
</div>
</div>
);
};
export default ProcessForm;
+109
View File
@@ -0,0 +1,109 @@
import { Plus, Shuffle, Trash2 } from 'lucide-react';
import type { Process } from '@core/types';
import { useProcessStore } from '../../store/processStore';
import ProcessCard from './ProcessCard';
import IconButton from '../shared/IconButton';
import { generateId, nextColor } from '../shared/processUtils';
interface ProcessListProps {
onEditProcess: (process: Process) => void;
onAddNew: () => void;
}
const MAX_PROCESSES = 16;
/** Generates a random process for quick testing */
function makeRandomProcess(index: number): Process {
const cpuDur = Math.floor(Math.random() * 6) + 1;
const ioDur = Math.floor(Math.random() * 5) + 1;
const hasBothBursts = Math.random() > 0.4;
return {
id: generateId(),
name: `P${index}`,
arrivalTime: Math.floor(Math.random() * 8),
priority: Math.floor(Math.random() * 10) + 1,
color: nextColor(),
bursts: hasBothBursts
? [{ type: 'cpu', duration: cpuDur }, { type: 'io', duration: ioDur }, { type: 'cpu', duration: Math.floor(Math.random() * 4) + 1 }]
: [{ type: 'cpu', duration: cpuDur }],
};
}
const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
const processes = useProcessStore(s => s.processes);
const removeProcess = useProcessStore(s => s.removeProcess);
const clearProcesses = useProcessStore(s => s.clearProcesses);
const addProcess = useProcessStore(s => s.addProcess);
const canAdd = processes.length < MAX_PROCESSES;
const handleRandomize = () => {
const p = makeRandomProcess(processes.length + 1);
addProcess(p);
};
return (
<div data-testid="process-list">
{/* List header */}
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">
{processes.length}/{MAX_PROCESSES} processos
</span>
<div className="flex gap-1">
<IconButton
size="sm"
variant="ghost"
label="Gerar processo aleatório"
onClick={handleRandomize}
disabled={!canAdd}
>
<Shuffle size={13} />
</IconButton>
<IconButton
size="sm"
variant="ghost"
label="Limpar todos os processos"
onClick={clearProcesses}
disabled={processes.length === 0}
className="hover:text-red-400 hover:bg-red-900/20"
>
<Trash2 size={13} />
</IconButton>
</div>
</div>
{/* Cards */}
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
{processes.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
<span>Nenhum processo adicionado</span>
<span className="text-xs">Clique em + para criar</span>
</div>
) : (
processes.map(p => (
<ProcessCard
key={p.id}
process={p}
onEdit={onEditProcess}
onRemove={removeProcess}
/>
))
)}
</div>
{/* Add button */}
{canAdd && (
<button
onClick={onAddNew}
aria-label="Adicionar novo processo"
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
>
<Plus size={14} />
Adicionar Processo
</button>
)}
</div>
);
};
export default ProcessList;
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { ListTree } from 'lucide-react';
import type { Process } from '@core/types';
import ProcessList from './ProcessList';
import ProcessForm from './ProcessForm';
const ProcessPanel = () => {
const [editingProcess, setEditingProcess] = useState<Process | null>(null);
const [showForm, setShowForm] = useState(false);
const handleEditProcess = (process: Process) => {
setEditingProcess(process);
setShowForm(true);
};
const handleAddNew = () => {
setEditingProcess(null);
setShowForm(true);
};
const handleClose = () => {
setShowForm(false);
setEditingProcess(null);
};
return (
<section aria-label="Painel de Processos">
<div className="flex items-center gap-2 mb-3 text-slate-300">
<ListTree size={18} className="text-blue-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2>
</div>
{showForm ? (
<ProcessForm editingProcess={editingProcess} onClose={handleClose} />
) : (
<ProcessList onEditProcess={handleEditProcess} onAddNew={handleAddNew} />
)}
</section>
);
};
export default ProcessPanel;