update page
This commit is contained in:
@@ -12,19 +12,15 @@ import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Cloud,
|
||||
Server,
|
||||
RefreshCw,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { WorkPackage } from '@/types/traceability';
|
||||
import { parseCSVContent, ParseResult } from '@/lib/csvParser';
|
||||
import {
|
||||
fetchFromOpenProject,
|
||||
fetchFromBackendProxy,
|
||||
OpenProjectConfig,
|
||||
DEFAULT_CONFIG
|
||||
} from '@/services/openProjectService';
|
||||
|
||||
const STORAGE_KEY = 'traceability_data';
|
||||
const SERVER_URL_KEY = 'traceability_server_url';
|
||||
|
||||
interface DataUpdateDialogProps {
|
||||
onDataLoaded: (workPackages: WorkPackage[]) => void;
|
||||
@@ -40,11 +36,10 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Backend proxy config
|
||||
const [proxyUrl, setProxyUrl] = useState('');
|
||||
|
||||
// Direct OpenProject config
|
||||
const [opConfig, setOpConfig] = useState<OpenProjectConfig>(DEFAULT_CONFIG);
|
||||
// Server endpoint config - persisted
|
||||
const [serverUrl, setServerUrl] = useState(() =>
|
||||
localStorage.getItem(SERVER_URL_KEY) || '/api/traceability'
|
||||
);
|
||||
|
||||
// Drag and drop state
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -53,6 +48,15 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
setLogs(prev => [...prev, log]);
|
||||
};
|
||||
|
||||
const persistData = (workPackages: WorkPackage[]) => {
|
||||
const data = {
|
||||
lastUpdated: new Date().toISOString(),
|
||||
workPackages
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
addLog(`💾 Data persisted to storage (${workPackages.length} items)`);
|
||||
};
|
||||
|
||||
const handleFile = async (file: File) => {
|
||||
setFileName(file.name);
|
||||
setIsLoading(true);
|
||||
@@ -60,10 +64,13 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
setErrors([]);
|
||||
|
||||
try {
|
||||
addLog(`📂 Reading file: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`);
|
||||
const text = await file.text();
|
||||
addLog(`📝 File loaded, ${text.length} characters`);
|
||||
|
||||
const result = parseCSVContent(text);
|
||||
setParseResult(result);
|
||||
setLogs(result.logs);
|
||||
setLogs(prev => [...prev, ...result.logs]);
|
||||
setErrors(result.errors);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -84,66 +91,87 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
const handleFetchFromProxy = async () => {
|
||||
if (!proxyUrl) {
|
||||
setErrors(['Please enter a backend proxy URL']);
|
||||
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);
|
||||
setLogs([]);
|
||||
setErrors([]);
|
||||
setParseResult(null);
|
||||
|
||||
try {
|
||||
const result = await fetchFromBackendProxy(proxyUrl, addLog);
|
||||
setLogs(result.logs);
|
||||
setErrors(result.errors);
|
||||
addLog(`🔍 Connecting to server: ${serverUrl}`);
|
||||
|
||||
if (result.success) {
|
||||
const typeCounts = result.workPackages.reduce((acc, wp) => {
|
||||
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: result.workPackages,
|
||||
logs: result.logs,
|
||||
errors: result.errors,
|
||||
workPackages,
|
||||
logs: [],
|
||||
errors: [],
|
||||
typeCounts
|
||||
});
|
||||
} else {
|
||||
// Try to parse as CSV anyway
|
||||
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);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchFromOpenProject = async () => {
|
||||
setIsLoading(true);
|
||||
setLogs([]);
|
||||
setErrors([]);
|
||||
setParseResult(null);
|
||||
|
||||
try {
|
||||
const result = await fetchFromOpenProject(opConfig, undefined, addLog);
|
||||
setLogs(result.logs);
|
||||
setErrors(result.errors);
|
||||
|
||||
if (result.success) {
|
||||
const typeCounts = result.workPackages.reduce((acc, wp) => {
|
||||
acc[wp.type] = (acc[wp.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
setParseResult({
|
||||
success: true,
|
||||
workPackages: result.workPackages,
|
||||
logs: result.logs,
|
||||
errors: result.errors,
|
||||
typeCounts
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
setErrors([errorMsg]);
|
||||
addLog(`❌ Error: ${errorMsg}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -151,6 +179,8 @@ 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?.();
|
||||
}
|
||||
@@ -174,23 +204,19 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
Update Traceability Data
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose how to update your data from OpenProject
|
||||
Upload a CSV file or fetch from your server
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<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
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="proxy" className="flex items-center gap-2">
|
||||
<TabsTrigger value="server" className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4" />
|
||||
Backend API
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="direct" className="flex items-center gap-2">
|
||||
<Cloud className="h-4 w-4" />
|
||||
Direct Fetch
|
||||
Server Fetch
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -222,103 +248,46 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> locally to generate the CSV
|
||||
Run <code className="bg-muted px-1 rounded">python get_traceability.py</code> to generate the CSV file
|
||||
</p>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab 2: Backend Proxy */}
|
||||
<TabsContent value="proxy" className="space-y-4">
|
||||
{/* Tab 2: Server Fetch */}
|
||||
<TabsContent value="server" className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="proxyUrl">Backend API Endpoint</Label>
|
||||
<Label htmlFor="serverUrl">Server Endpoint</Label>
|
||||
<Input
|
||||
id="proxyUrl"
|
||||
placeholder="https://your-server.com/api/traceability"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
id="serverUrl"
|
||||
placeholder="/api/traceability or https://your-server.com/api/data"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Your backend should return JSON or CSV with work package data
|
||||
Endpoint that runs your Python script and returns CSV or JSON
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFetchFromProxy}
|
||||
disabled={isLoading || !proxyUrl}
|
||||
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 Backend</>
|
||||
<><Server className="h-4 w-4 mr-2" /> Fetch from Server</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded-lg p-3 text-xs space-y-2">
|
||||
<p className="font-medium">Backend Setup Guide:</p>
|
||||
<p className="font-medium">Server Setup:</p>
|
||||
<ol className="list-decimal ml-4 space-y-1 text-muted-foreground">
|
||||
<li>Deploy the Python script on your server</li>
|
||||
<li>Create an API endpoint that runs the script</li>
|
||||
<li>Return JSON or serve the generated CSV</li>
|
||||
<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>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab 3: Direct OpenProject Fetch */}
|
||||
<TabsContent value="direct" className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="baseUrl">OpenProject URL</Label>
|
||||
<Input
|
||||
id="baseUrl"
|
||||
placeholder="https://openproject.example.com"
|
||||
value={opConfig.baseUrl}
|
||||
onChange={(e) => setOpConfig(prev => ({ ...prev, baseUrl: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="projectId">Project Identifier</Label>
|
||||
<Input
|
||||
id="projectId"
|
||||
placeholder="project-id"
|
||||
value={opConfig.projectIdentifier}
|
||||
onChange={(e) => setOpConfig(prev => ({ ...prev, projectIdentifier: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="apiKey">API Key</Label>
|
||||
<Input
|
||||
id="apiKey"
|
||||
type="password"
|
||||
placeholder="Your OpenProject API Key"
|
||||
value={opConfig.apiKey}
|
||||
onChange={(e) => setOpConfig(prev => ({ ...prev, apiKey: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleFetchFromOpenProject}
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
{isLoading ? (
|
||||
<><Loader2 className="h-4 w-4 mr-2 animate-spin" /> Fetching...</>
|
||||
) : (
|
||||
<><Cloud className="h-4 w-4 mr-2" /> Fetch from OpenProject</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 text-xs">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-amber-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-700">CORS Notice</p>
|
||||
<p className="text-amber-600 mt-1">
|
||||
Direct browser fetch may fail due to CORS. Configure your OpenProject server to allow
|
||||
cross-origin requests, or use the Backend API option.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Results Section */}
|
||||
@@ -387,7 +356,7 @@ export function DataUpdateDialog({ onDataLoaded, onClose }: DataUpdateDialogProp
|
||||
{parseResult?.success && (
|
||||
<Button onClick={handleApply}>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Apply Data
|
||||
Apply & Save
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
|
||||
Reference in New Issue
Block a user