new features
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
AlertCircle,
|
||||
Server,
|
||||
RefreshCw,
|
||||
Loader2
|
||||
Loader2,
|
||||
Download
|
||||
} from 'lucide-react';
|
||||
import { WorkPackage } from '@/types/traceability';
|
||||
import { parseCSVContent, ParseResult } from '@/lib/csvParser';
|
||||
@@ -22,6 +23,9 @@ import { parseCSVContent, ParseResult } from '@/lib/csvParser';
|
||||
const STORAGE_KEY = 'traceability_data';
|
||||
const SERVER_URL_KEY = 'traceability_server_url';
|
||||
|
||||
// API URL for production deployment
|
||||
const API_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
interface DataUpdateDialogProps {
|
||||
onDataLoaded: (workPackages: WorkPackage[]) => void;
|
||||
onClose?: () => void;
|
||||
@@ -38,7 +42,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
|
||||
// Server endpoint config - persisted
|
||||
const [serverUrl, setServerUrl] = useState(() =>
|
||||
localStorage.getItem(SERVER_URL_KEY) || '/api/traceability'
|
||||
localStorage.getItem(SERVER_URL_KEY) || (API_URL ? `${API_URL}/api/traceability` : '/api/traceability')
|
||||
);
|
||||
|
||||
// Drag and drop state
|
||||
@@ -91,13 +95,153 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
// Reload from static CSV file
|
||||
const handleReloadFromCSV = async () => {
|
||||
setIsLoading(true);
|
||||
setLogs([]);
|
||||
setErrors([]);
|
||||
setParseResult(null);
|
||||
|
||||
try {
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
addLog(`🔍 Fetching /data/traceability_export.csv${cacheBuster}`);
|
||||
|
||||
const response = await fetch(`/data/traceability_export.csv${cacheBuster}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load CSV: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const csvText = await response.text();
|
||||
addLog(`📄 Received ${csvText.length} bytes`);
|
||||
|
||||
const result = parseCSVContent(csvText);
|
||||
setParseResult(result);
|
||||
setLogs(prev => [...prev, ...result.logs]);
|
||||
setErrors(result.errors);
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
setErrors([errorMsg]);
|
||||
addLog(`❌ Error: ${errorMsg}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger sync from OpenProject (runs Python script on server)
|
||||
const handleSyncFromServer = async () => {
|
||||
if (!serverUrl) {
|
||||
setErrors(['Please enter a server endpoint URL']);
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(SERVER_URL_KEY, serverUrl);
|
||||
|
||||
setIsLoading(true);
|
||||
setLogs([]);
|
||||
setErrors([]);
|
||||
setParseResult(null);
|
||||
|
||||
try {
|
||||
// First, trigger the sync (runs Python script)
|
||||
const syncUrl = serverUrl.replace(/\/api\/traceability\/?$/, '/api/traceability/sync');
|
||||
addLog(`🔄 Triggering sync at: ${syncUrl}`);
|
||||
|
||||
const syncResponse = await fetch(syncUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!syncResponse.ok) {
|
||||
const errorData = await syncResponse.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `Sync failed: ${syncResponse.status}`);
|
||||
}
|
||||
|
||||
const syncResult = await syncResponse.json();
|
||||
addLog(`✅ Sync complete: ${syncResult.message}`);
|
||||
if (syncResult.stdout) {
|
||||
syncResult.stdout.split('\n').filter(Boolean).forEach((line: string) => {
|
||||
addLog(`📋 ${line}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Now fetch the updated data
|
||||
addLog(`🔍 Fetching updated data from: ${serverUrl}`);
|
||||
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json, text/csv' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
addLog(`📄 Response content-type: ${contentType}`);
|
||||
|
||||
if (contentType.includes('text/csv')) {
|
||||
addLog(`📋 Parsing CSV response...`);
|
||||
const csvText = await response.text();
|
||||
const result = parseCSVContent(csvText);
|
||||
|
||||
setLogs(prev => [...prev, ...result.logs]);
|
||||
setErrors(result.errors);
|
||||
setParseResult(result);
|
||||
|
||||
} else if (contentType.includes('application/json')) {
|
||||
addLog(`📋 Parsing JSON response...`);
|
||||
const data = await response.json();
|
||||
|
||||
let workPackages: WorkPackage[];
|
||||
if (Array.isArray(data)) {
|
||||
workPackages = data;
|
||||
} else if (data.workPackages) {
|
||||
workPackages = data.workPackages;
|
||||
} else {
|
||||
throw new Error('Invalid JSON format: expected array or {workPackages: [...]}');
|
||||
}
|
||||
|
||||
addLog(`✅ Received ${workPackages.length} work packages`);
|
||||
|
||||
const typeCounts = workPackages.reduce((acc, wp) => {
|
||||
acc[wp.type] = (acc[wp.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
setParseResult({
|
||||
success: true,
|
||||
workPackages,
|
||||
logs: [],
|
||||
errors: [],
|
||||
typeCounts
|
||||
});
|
||||
} else {
|
||||
addLog(`⚠️ Unknown content-type, attempting CSV parse...`);
|
||||
const text = await response.text();
|
||||
const result = parseCSVContent(text);
|
||||
setLogs(prev => [...prev, ...result.logs]);
|
||||
setErrors(result.errors);
|
||||
setParseResult(result);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
setErrors([errorMsg]);
|
||||
addLog(`❌ Error: ${errorMsg}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Just fetch existing data (no sync)
|
||||
const handleFetchFromServer = async () => {
|
||||
if (!serverUrl) {
|
||||
setErrors(['Please enter a server endpoint URL']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save URL for next time
|
||||
localStorage.setItem(SERVER_URL_KEY, serverUrl);
|
||||
|
||||
setIsLoading(true);
|
||||
@@ -159,7 +303,6 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
typeCounts
|
||||
});
|
||||
} else {
|
||||
// Try to parse as CSV anyway
|
||||
addLog(`⚠️ Unknown content-type, attempting CSV parse...`);
|
||||
const text = await response.text();
|
||||
const result = parseCSVContent(text);
|
||||
@@ -179,7 +322,6 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
|
||||
const handleApply = () => {
|
||||
if (parseResult?.success && parseResult.workPackages.length > 0) {
|
||||
// Persist to localStorage for other users/sessions
|
||||
persistData(parseResult.workPackages);
|
||||
onDataLoaded(parseResult.workPackages);
|
||||
onClose?.();
|
||||
@@ -204,7 +346,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
Update Traceability Data
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Upload a CSV file or fetch from your server
|
||||
Upload a CSV file, reload from static file, or sync from OpenProject
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -212,15 +354,15 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="upload" className="flex items-center gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload CSV
|
||||
Upload / Reload
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="server" className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4" />
|
||||
Server Fetch
|
||||
Server Sync
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab 1: Manual CSV Upload */}
|
||||
{/* Tab 1: Manual CSV Upload / Reload */}
|
||||
<TabsContent value="upload" className="space-y-4">
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
@@ -247,45 +389,77 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReloadFromCSV}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Loading...</>
|
||||
) : (
|
||||
<><Download className="h-4 w-4 mr-2" /> Reload from Static CSV</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> to generate the CSV file
|
||||
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> to generate the CSV file, then reload.
|
||||
</p>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab 2: Server Fetch */}
|
||||
{/* Tab 2: Server Sync */}
|
||||
<TabsContent value="server" className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="serverUrl">Server Endpoint</Label>
|
||||
<Label htmlFor="serverUrl">API Endpoint</Label>
|
||||
<Input
|
||||
id="serverUrl"
|
||||
placeholder="/api/traceability or https://your-server.com/api/data"
|
||||
placeholder="/api/traceability or https://your-api.com/api/traceability"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Endpoint that runs your Python script and returns CSV or JSON
|
||||
Uses the data service API to sync with OpenProject
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFetchFromServer}
|
||||
disabled={isLoading || !serverUrl}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
|
||||
) : (
|
||||
<><Server className="h-4 w-4 mr-2" /> Fetch from Server</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
onClick={handleSyncFromServer}
|
||||
disabled={isLoading || !serverUrl}
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Syncing...</>
|
||||
) : (
|
||||
<><RefreshCw className="h-4 w-4 mr-2" /> Sync from OpenProject</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleFetchFromServer}
|
||||
disabled={isLoading || !serverUrl}
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
|
||||
) : (
|
||||
<><Server className="h-4 w-4 mr-2" /> Fetch Existing Data</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-3 text-xs space-y-2">
|
||||
<p className="font-medium">Server Setup:</p>
|
||||
<ol className="list-decimal ml-4 space-y-1 text-muted-foreground">
|
||||
<li>Create an endpoint that runs <code>get_traceability.py</code></li>
|
||||
<li>Return the CSV file or JSON with work packages</li>
|
||||
<li>Example: <code>GET /api/traceability</code> → returns CSV</li>
|
||||
</ol>
|
||||
<p className="font-medium">How it works:</p>
|
||||
<ul className="list-disc ml-4 space-y-1 text-muted-foreground">
|
||||
<li><strong>Sync from OpenProject</strong>: Runs the Python script on the server to fetch latest data from OpenProject API</li>
|
||||
<li><strong>Fetch Existing Data</strong>: Gets the last synced CSV from the server (no OpenProject call)</li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Note: Server sync only works in deployed environment with the data-service running.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -337,7 +511,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
|
||||
{/* Logs */}
|
||||
{logs.length > 0 && (
|
||||
<details className="text-xs">
|
||||
<details className="text-xs" open>
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
|
||||
View logs ({logs.length} entries)
|
||||
</summary>
|
||||
|
||||
Reference in New Issue
Block a user