docv/components/4nk/ProcessesViewer.tsx

357 lines
15 KiB
TypeScript

import { useState, memo } from 'react';
import { isFileBlob, type FileBlob } from '@/lib/4nk/models/Data';
import { iframeUrl } from "@/app/page";
import MessageBus from '@/lib/4nk/MessageBus';
interface BlockState {
commited_in: string;
state_id: string;
pcd_commitment: Record<string, string>;
public_data: Record<string, any>;
}
interface Block {
states: BlockState[];
}
interface Processes {
[key: string]: Block;
}
interface ProcessesViewerProps {
processes: Processes | null;
myProcesses: string[];
onProcessesUpdate?: (processes: Processes) => void;
}
const compareStates = (
currentState: BlockState,
index: number,
previousState?: BlockState,
currentPrivateData?: Record<string, any>,
previousPrivateData?: Record<string, any>
) => {
const result: Record<string, {
value: any,
status: 'unchanged' | 'modified',
hash?: string,
isPrivate: boolean,
stateId: string
}> = {};
Object.keys(currentState.public_data).forEach(key => {
const currentValue = currentState.public_data[key];
const previousValue = previousState?.public_data[key];
const isModified = index > 0 && previousValue !== undefined && JSON.stringify(currentValue) !== JSON.stringify(previousValue);
result[key] = {
value: currentValue,
status: isModified ? 'modified' : 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: false,
stateId: currentState.state_id
};
});
if (index === 0 && currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
} else if (previousPrivateData) {
Object.entries(previousPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'unchanged',
hash: previousState?.pcd_commitment[key],
isPrivate: true,
stateId: previousState!.state_id
};
});
if (currentPrivateData) {
Object.entries(currentPrivateData).forEach(([key, value]) => {
result[key] = {
value,
status: 'modified',
hash: currentState.pcd_commitment[key],
isPrivate: true,
stateId: currentState.state_id
};
});
}
}
return result;
};
function ProcessesViewer({ processes, myProcesses, onProcessesUpdate }: ProcessesViewerProps) {
const [expandedBlocks, setExpandedBlocks] = useState<string[]>([]);
const [isFiltered, setIsFiltered] = useState<boolean>(false);
const [privateData, setPrivateData] = useState<Record<string, Record<string, any>>>({});
const [editingField, setEditingField] = useState<{ processId: string; stateId: string; key: string; value: any; } | null>(null);
const [tempValue, setTempValue] = useState<any>(null);
const toggleBlock = (blockId: string) => {
setExpandedBlocks(prev => prev.includes(blockId) ? prev.filter(id => id !== blockId) : [...prev, blockId]);
};
const handleFilterClick = () => setIsFiltered(prev => !prev);
if (!processes || Object.keys(processes).length === 0) {
return (
<div className="flex flex-col items-center justify-center py-12 text-gray-500">
<h3 className="text-lg font-medium mb-2">Aucun processus disponible</h3>
<p>Connectez-vous pour voir vos processus</p>
</div>
);
}
const fetchPrivateData = async (processId: string, stateId: string) => {
if (!expandedBlocks.includes(processId) || !myProcesses.includes(processId)) return;
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const data = await messageBus.getData(processId, stateId);
setPrivateData(prev => ({ ...prev, [stateId]: data }));
} catch (err) {
console.error(err);
}
};
const handleDownload = (name: string | undefined, fileBlob: FileBlob) => {
const blob = new Blob([fileBlob.data], { type: fileBlob.type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name || 'download';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const formatValue = (key: string, value: string | number[] | FileBlob) => {
if (isFileBlob(value)) {
return (
<button className="text-blue-600 hover:underline" onClick={() => handleDownload(key, value)}>
📥 Télécharger
</button>
);
}
return <span>{JSON.stringify(value || '')}</span>;
};
const getDataIcon = (value: any) => {
if (isFileBlob(value)) return '📄';
if (typeof value === 'string') return '📝';
if (typeof value === 'number') return '🔢';
if (Array.isArray(value)) return '📋';
if (typeof value === 'boolean') return '✅';
return '📦';
};
const handleFieldUpdate = async (processId: string, stateId: string, key: string, value: any) => {
try {
const messageBus = MessageBus.getInstance(iframeUrl);
await messageBus.isReady();
const updatedProcess = await messageBus.updateProcess(processId, stateId, { [key]: value }, [], null);
if (!updatedProcess) throw new Error('No updated process found');
const newStateId = updatedProcess.diffs[0]?.state_id;
if (!newStateId) throw new Error('No new state id found');
await messageBus.notifyProcessUpdate(processId, newStateId);
await messageBus.validateState(processId, newStateId);
const updatedProcesses = await messageBus.getProcesses();
onProcessesUpdate?.(updatedProcesses);
} catch (err) {
console.error(err);
}
};
const renderEditForm = (key: string, value: any, onSave: (v: any) => void, onCancel: () => void) => {
if (tempValue === null) setTempValue(value);
const handleFormClick = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); };
if (isFileBlob(value)) {
return (
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
<input className="dark:bg-gray-800 dark:text-gray-100" type="file" onChange={(e) => {
e.stopPropagation();
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target?.result) {
setTempValue({ type: file.type, data: new Uint8Array(event.target.result as ArrayBuffer) });
}
};
reader.readAsArrayBuffer(file);
}
}} />
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => { onSave(tempValue); setTempValue(null); }}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 dark:bg-gray-700 dark:text-gray-100 rounded" onClick={() => { onCancel(); setTempValue(null); }}>Annuler</button>
</div>
</div>
);
}
if (typeof value === 'boolean') {
return (
<div className="flex items-center space-x-2" onClick={handleFormClick}>
<select className="border rounded px-2 py-1 dark:bg-gray-800 dark:text-gray-100" value={tempValue.toString()} onChange={(e) => setTempValue(e.target.value === 'true')}>
<option value="true">Vrai</option>
<option value="false">Faux</option>
</select>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 dark:bg-gray-700 dark:text-gray-100 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
}
if (Array.isArray(value)) {
return (
<div className="flex flex-col space-y-2" onClick={handleFormClick}>
<textarea
className="border rounded p-2 dark:bg-gray-800 dark:text-gray-100"
rows={4}
value={JSON.stringify(tempValue, null, 2)}
onChange={(e) => {
try { const parsed = JSON.parse(e.target.value); if (Array.isArray(parsed)) setTempValue(parsed); } catch { }
}}
/>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 dark:bg-gray-700 dark:text-gray-100 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
}
return (
<div className="flex space-x-2 items-center" onClick={handleFormClick}>
<input
className="border rounded px-2 py-1 dark:bg-gray-800 dark:text-gray-100"
type={typeof value === 'number' ? 'number' : 'text'}
value={tempValue}
onChange={(e) => setTempValue(typeof value === 'number' ? parseFloat(e.target.value) : e.target.value)}
autoFocus
/>
<div className="flex space-x-2">
<button className="px-2 py-1 bg-blue-500 text-white rounded" onClick={() => onSave(tempValue)}>Sauvegarder</button>
<button className="px-2 py-1 bg-gray-300 dark:bg-gray-700 dark:text-gray-100 rounded" onClick={() => onCancel()}>Annuler</button>
</div>
</div>
);
};
const renderDataField = (key: string, value: any, hash: string | undefined, isPrivate: boolean, processId: string, stateId: string, status: 'unchanged' | 'modified' = 'unchanged', originStateId?: string) => {
const isEditing = editingField?.key === key && editingField?.processId === processId && editingField?.stateId === stateId;
return (
<div
key={key}
className={`border rounded p-2 mb-2 transition-colors
${status === 'modified' ? 'bg-green-100 dark:bg-green-900' : 'bg-white dark:bg-gray-800'}`}
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center space-x-1">
<span title={isPrivate ? 'Donnée privée' : 'Donnée publique'}>{isPrivate ? '🔒' : '🌐'}</span>
<span>{getDataIcon(value)}</span>
<span className="font-medium">{key}</span>
{originStateId && originStateId !== stateId && <span title={`Propagé depuis l'état ${originStateId}`}></span>}
</div>
<button
className="text-sm text-gray-500 dark:text-gray-300 hover:text-gray-700 dark:hover:text-gray-100"
onClick={(e) => { e.stopPropagation(); setEditingField({ processId, stateId, key, value }); }}
>
{isEditing ? '✕' : '🔄'}
</button>
</div>
<div>
{isEditing ? renderEditForm(key, value, async (v) => { await handleFieldUpdate(processId, stateId, key, v); setEditingField(null); setTempValue(null); }, () => { setEditingField(null); setTempValue(null); }) : (
<div className="flex items-center space-x-1">
<span>{formatValue(key, value)}</span>
{hash && <span title={`Hash: ${hash}`}>🔑</span>}
</div>
)}
</div>
</div>
);
};
return (
<div className="w-full h-full overflow-auto p-2 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Processus</h2>
<button className="px-2 py-1 border rounded text-sm dark:border-gray-700 dark:text-gray-200" onClick={handleFilterClick}>
{isFiltered ? 'Show All' : 'Filter'}
</button>
</div>
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
{isFiltered ? Object.keys(processes).filter(p => myProcesses.includes(p)).length : Object.keys(processes).length} processus disponible(s)
</p>
<div className="space-y-4">
{Object.entries(processes).map(([processId, process]) => {
if (isFiltered && !myProcesses.includes(processId)) return null;
const isExpanded = expandedBlocks.includes(processId);
const stateCount = process.states.length - 1;
return (
<div key={processId} className="border rounded shadow-sm border-gray-200 dark:border-gray-700">
<div
className="flex justify-between items-center p-2 cursor-pointer bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={() => toggleBlock(processId)}
>
<div className="font-mono">{processId.slice(0, 8)}...{processId.slice(-4)}</div>
<div>{stateCount} état(s)</div>
<div>{isExpanded ? '▼' : '▶'}</div>
</div>
{isExpanded && (
<div className="p-2 space-y-2 bg-white dark:bg-gray-900">
<div><strong>Process ID:</strong> {processId}</div>
{process.states.map((state, index) => {
if (index === stateCount) return null;
if (myProcesses.includes(processId) && !privateData[state.state_id]) setTimeout(() => fetchPrivateData(processId, state.state_id), 0);
const statePrivateData = privateData[state.state_id] || {};
const stateData = compareStates(state, index, index > 0 ? process.states[index - 1] : undefined, statePrivateData, index > 0 ? privateData[process.states[index - 1].state_id] : undefined);
return (
<div key={`${processId}-state-${index}`} className="border-t border-gray-200 dark:border-gray-700 pt-2">
<h4 className="font-medium mb-1">État {index + 1}</h4>
<div className="text-sm mb-1"><strong>TransactionId:</strong> {state.commited_in}</div>
<div className="text-sm mb-2"><strong>Empreinte totale de l'état:</strong> {state.state_id}</div>
<div className="space-y-1">
{Object.entries(stateData).map(([key, { value, status, hash, isPrivate, stateId }]) =>
renderDataField(key, value, hash, isPrivate, processId, stateId, status, state.state_id)
)}
{myProcesses.includes(processId) && Object.keys(statePrivateData).length === 0 && (
<div className="text-gray-400 dark:text-gray-500 text-sm">Chargement des données privées...</div>
)}
{!myProcesses.includes(processId) && (
<div className="text-gray-400 dark:text-gray-500 text-sm">🔒 Vous n'avez pas accès aux données privées de ce processus</div>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
ProcessesViewer.displayName = 'ProcessesViewer';
export default memo(ProcessesViewer);