Fixes to the import error checks
This commit is contained in:
211
plans/import-directory-status.md
Normal file
211
plans/import-directory-status.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Implementation Plan: Directory Status Check for Import.tsx
|
||||
|
||||
## Overview
|
||||
|
||||
Add functionality to `Import.tsx` that checks if the required directories (`comics` and `userdata`) exist before allowing the import process to start. If either directory is missing, display a warning banner to the user and disable the import functionality.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
- **Endpoint**: `GET /api/library/getDirectoryStatus`
|
||||
- **Response Structure**:
|
||||
```typescript
|
||||
interface DirectoryStatus {
|
||||
comics: { exists: boolean };
|
||||
userdata: { exists: boolean };
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Add Directory Status Type
|
||||
|
||||
In [`Import.tsx`](src/client/components/Import/Import.tsx:1), add a type definition for the directory status response:
|
||||
|
||||
```typescript
|
||||
interface DirectoryStatus {
|
||||
comics: { exists: boolean };
|
||||
userdata: { exists: boolean };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create useQuery Hook for Directory Status
|
||||
|
||||
Use `@tanstack/react-query` (already imported) to fetch directory status on component mount:
|
||||
|
||||
```typescript
|
||||
const { data: directoryStatus, isLoading: isCheckingDirectories, error: directoryError } = useQuery({
|
||||
queryKey: ['directoryStatus'],
|
||||
queryFn: async (): Promise<DirectoryStatus> => {
|
||||
const response = await axios.get('http://localhost:3000/api/library/getDirectoryStatus');
|
||||
return response.data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Derive Missing Directories State
|
||||
|
||||
Compute which directories are missing from the query result:
|
||||
|
||||
```typescript
|
||||
const missingDirectories = useMemo(() => {
|
||||
if (!directoryStatus) return [];
|
||||
const missing: string[] = [];
|
||||
if (!directoryStatus.comics?.exists) missing.push('comics');
|
||||
if (!directoryStatus.userdata?.exists) missing.push('userdata');
|
||||
return missing;
|
||||
}, [directoryStatus]);
|
||||
|
||||
const hasAllDirectories = missingDirectories.length === 0;
|
||||
```
|
||||
|
||||
### 4. Create Warning Banner Component
|
||||
|
||||
Add a warning banner that displays when directories are missing, positioned above the import button. This uses the same styling patterns as the existing error banner:
|
||||
|
||||
```tsx
|
||||
{/* Directory Status Warning */}
|
||||
{!isCheckingDirectories && missingDirectories.length > 0 && (
|
||||
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-amber-500 bg-amber-50 dark:bg-amber-900/20 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="w-6 h-6 text-amber-600 dark:text-amber-400 mt-0.5">
|
||||
<i className="h-6 w-6 icon-[solar--folder-error-bold]"></i>
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-amber-800 dark:text-amber-300">
|
||||
Required Directories Missing
|
||||
</p>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400 mt-1">
|
||||
The following directories do not exist and must be created before importing:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-amber-700 dark:text-amber-400 mt-2">
|
||||
{missingDirectories.map((dir) => (
|
||||
<li key={dir}>
|
||||
<code className="bg-amber-100 dark:bg-amber-900/50 px-1 rounded">{dir}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400 mt-2">
|
||||
Please ensure these directories are mounted correctly in your Docker configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### 5. Disable Import Button When Directories Missing
|
||||
|
||||
Modify the button's `disabled` prop and click handler:
|
||||
|
||||
```tsx
|
||||
<button
|
||||
className="..."
|
||||
onClick={handleForceReImport}
|
||||
disabled={isForceReImporting || hasActiveSession || !hasAllDirectories}
|
||||
title={!hasAllDirectories
|
||||
? "Cannot import: Required directories are missing"
|
||||
: "Re-import all files to fix Elasticsearch indexing issues"}
|
||||
>
|
||||
```
|
||||
|
||||
### 6. Update handleForceReImport Guard
|
||||
|
||||
Add early return in the handler for missing directories:
|
||||
|
||||
```typescript
|
||||
const handleForceReImport = async () => {
|
||||
setImportError(null);
|
||||
|
||||
// Check for missing directories
|
||||
if (!hasAllDirectories) {
|
||||
setImportError(
|
||||
`Cannot start import: Required directories are missing (${missingDirectories.join(', ')}). Please check your Docker volume configuration.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ... existing logic
|
||||
};
|
||||
```
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| [`src/client/components/Import/Import.tsx`](src/client/components/Import/Import.tsx) | Add useQuery for directory status, warning banner UI, disable button logic |
|
||||
| [`src/client/components/Import/Import.test.tsx`](src/client/components/Import/Import.test.tsx) | Add tests for directory status scenarios |
|
||||
|
||||
## Test Cases to Add
|
||||
|
||||
### Import.test.tsx Updates
|
||||
|
||||
1. **Should show warning banner when comics directory is missing**
|
||||
2. **Should show warning banner when userdata directory is missing**
|
||||
3. **Should show warning banner when both directories are missing**
|
||||
4. **Should disable import button when directories are missing**
|
||||
5. **Should enable import button when all directories exist**
|
||||
6. **Should handle directory status API error gracefully**
|
||||
|
||||
Example test structure:
|
||||
|
||||
```typescript
|
||||
describe('Import Component - Directory Status', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Mock successful directory status by default
|
||||
(axios.get as jest.Mock) = jest.fn().mockResolvedValue({
|
||||
data: { comics: { exists: true }, userdata: { exists: true } }
|
||||
});
|
||||
});
|
||||
|
||||
test('should show warning when comics directory is missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: false }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Required Directories Missing')).toBeInTheDocument();
|
||||
expect(screen.getByText('comics')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should disable import button when directories are missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: false }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
const button = screen.getByRole('button', { name: /Force Re-Import/i });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Import Component Mounts] --> B[Fetch Directory Status]
|
||||
B --> C{API Success?}
|
||||
C -->|Yes| D{All Directories Exist?}
|
||||
C -->|No| E[Show Error Banner]
|
||||
D -->|Yes| F[Enable Import Button]
|
||||
D -->|No| G[Show Warning Banner]
|
||||
G --> H[Disable Import Button]
|
||||
F --> I[User Clicks Import]
|
||||
I --> J[Proceed with Import]
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The directory status is fetched once on mount with a 30-second stale time
|
||||
- The warning uses amber/yellow colors to differentiate from error messages (red)
|
||||
- The existing `importError` state and UI can remain unchanged
|
||||
- No changes needed to the backend - the endpoint already exists
|
||||
@@ -490,4 +490,188 @@ describe('Import Component - Real-time Updates', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import Component - Directory Status', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(axios as any).mockResolvedValue({ data: [] });
|
||||
(axios.request as jest.Mock) = jest.fn().mockResolvedValue({ data: {} });
|
||||
// Mock successful directory status by default
|
||||
(axios.get as jest.Mock) = jest.fn().mockResolvedValue({
|
||||
data: { comics: { exists: true }, userdata: { exists: true } }
|
||||
});
|
||||
});
|
||||
|
||||
test('should show warning banner when comics directory is missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: false }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Required Directories Missing')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('comics')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show warning banner when userdata directory is missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: true }, userdata: { exists: false } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Required Directories Missing')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('userdata')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show warning banner when both directories are missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: false }, userdata: { exists: false } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Required Directories Missing')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('comics')).toBeInTheDocument();
|
||||
expect(screen.getByText('userdata')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should disable import button when directories are missing', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: false }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
const button = screen.getByRole('button', { name: /Force Re-Import/i });
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should enable import button when all directories exist', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: true }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => {
|
||||
const button = screen.getByRole('button', { name: /Force Re-Import/i });
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should not show warning banner when all directories exist', async () => {
|
||||
(axios.get as jest.Mock).mockResolvedValue({
|
||||
data: { comics: { exists: true }, userdata: { exists: true } }
|
||||
});
|
||||
|
||||
const { useStore } = require('../../store');
|
||||
useStore.mockImplementation((selector: any) =>
|
||||
selector({
|
||||
importJobQueue: {
|
||||
status: 'drained',
|
||||
successfulJobCount: 0,
|
||||
failedJobCount: 0,
|
||||
mostRecentImport: '',
|
||||
setStatus: mockSetStatus,
|
||||
},
|
||||
getSocket: mockGetSocket,
|
||||
disconnectSocket: mockDisconnectSocket,
|
||||
})
|
||||
);
|
||||
|
||||
render(<Import />, { wrapper: createWrapper() });
|
||||
|
||||
// Wait for the component to finish loading
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /Force Re-Import/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The warning banner should not be present
|
||||
expect(screen.queryByText('Required Directories Missing')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { isEmpty } from "lodash";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useStore } from "../../store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import axios from "axios";
|
||||
import { useGetJobResultStatisticsQuery } from "../../graphql/generated";
|
||||
import { RealTimeImportStats } from "./RealTimeImportStats";
|
||||
import { useImportSessionStatus } from "../../hooks/useImportSessionStatus";
|
||||
import { SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
|
||||
|
||||
interface DirectoryIssue {
|
||||
directory: string;
|
||||
issue: string;
|
||||
}
|
||||
|
||||
interface DirectoryStatus {
|
||||
isValid: boolean;
|
||||
issues: DirectoryIssue[];
|
||||
}
|
||||
|
||||
export const Import = (): ReactElement => {
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
@@ -20,6 +31,24 @@ export const Import = (): ReactElement => {
|
||||
})),
|
||||
);
|
||||
|
||||
// Check if required directories exist
|
||||
const { data: directoryStatus, isLoading: isCheckingDirectories, isError: isDirectoryCheckError, error: directoryError } = useQuery({
|
||||
queryKey: ["directoryStatus"],
|
||||
queryFn: async (): Promise<DirectoryStatus> => {
|
||||
const response = await axios.get(`${SETTINGS_SERVICE_BASE_URI}/getDirectoryStatus`);
|
||||
return response.data;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 30000, // Cache for 30 seconds
|
||||
retry: false, // Don't retry on failure - show error immediately
|
||||
});
|
||||
|
||||
// Use isValid for quick check, issues array for detailed display
|
||||
// If there's an error fetching directory status, assume directories are invalid
|
||||
const directoryCheckFailed = isDirectoryCheckError;
|
||||
const hasAllDirectories = directoryCheckFailed ? false : (directoryStatus?.isValid ?? true);
|
||||
const directoryIssues = directoryStatus?.issues ?? [];
|
||||
|
||||
// Force re-import mutation - re-imports all files regardless of import status
|
||||
const { mutate: forceReImport, isPending: isForceReImporting } = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -99,6 +128,21 @@ export const Import = (): ReactElement => {
|
||||
const handleForceReImport = async () => {
|
||||
setImportError(null);
|
||||
|
||||
// Check for missing directories before starting
|
||||
if (!hasAllDirectories) {
|
||||
if (directoryCheckFailed) {
|
||||
setImportError(
|
||||
"Cannot start import: Failed to verify directory status. Please check that the backend service is running."
|
||||
);
|
||||
} else {
|
||||
const issueDetails = directoryIssues.map(i => `${i.directory}: ${i.issue}`).join(", ");
|
||||
setImportError(
|
||||
`Cannot start import: ${issueDetails || "Required directories are missing"}. Please check your Docker volume configuration.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for active session before starting using definitive status
|
||||
if (hasActiveSession) {
|
||||
setImportError(
|
||||
@@ -197,6 +241,58 @@ export const Import = (): ReactElement => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Directory Check Error - shown when API call fails */}
|
||||
{!isCheckingDirectories && directoryCheckFailed && (
|
||||
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-red-500 bg-red-50 dark:bg-red-900/20 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="w-6 h-6 text-red-600 dark:text-red-400 mt-0.5">
|
||||
<i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i>
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-red-800 dark:text-red-300">
|
||||
Failed to Check Directory Status
|
||||
</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-1">
|
||||
Unable to verify if required directories exist. Import functionality has been disabled.
|
||||
</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-2">
|
||||
Error: {(directoryError as Error)?.message || "Unknown error"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Directory Status Warning - shown when directories have issues */}
|
||||
{!isCheckingDirectories && !directoryCheckFailed && directoryIssues.length > 0 && (
|
||||
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-amber-500 bg-amber-50 dark:bg-amber-900/20 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="w-6 h-6 text-amber-600 dark:text-amber-400 mt-0.5">
|
||||
<i className="h-6 w-6 icon-[solar--folder-error-bold]"></i>
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-amber-800 dark:text-amber-300">
|
||||
Directory Configuration Issues
|
||||
</p>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400 mt-1">
|
||||
The following issues were detected with your directory configuration:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm text-amber-700 dark:text-amber-400 mt-2">
|
||||
{directoryIssues.map((item) => (
|
||||
<li key={item.directory}>
|
||||
<code className="bg-amber-100 dark:bg-amber-900/50 px-1 rounded">{item.directory}</code>
|
||||
<span className="ml-1">— {item.issue}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400 mt-2">
|
||||
Please ensure these directories are mounted correctly in your Docker configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Force Re-Import Button - always shown when no import is running */}
|
||||
{!hasActiveSession &&
|
||||
(importJobQueue.status === "drained" || importJobQueue.status === undefined) && (
|
||||
@@ -204,8 +300,10 @@ export const Import = (): ReactElement => {
|
||||
<button
|
||||
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-orange-400 dark:border-orange-200 bg-orange-200 px-5 py-3 text-gray-700 hover:bg-transparent hover:text-orange-600 focus:outline-none focus:ring active:text-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={handleForceReImport}
|
||||
disabled={isForceReImporting || hasActiveSession}
|
||||
title="Re-import all files to fix Elasticsearch indexing issues"
|
||||
disabled={isForceReImporting || hasActiveSession || !hasAllDirectories}
|
||||
title={!hasAllDirectories
|
||||
? "Cannot import: Required directories are missing"
|
||||
: "Re-import all files to fix Elasticsearch indexing issues"}
|
||||
>
|
||||
<span className="text-md font-medium">
|
||||
{isForceReImporting ? "Starting Re-Import..." : "Force Re-Import All Files"}
|
||||
|
||||
@@ -11,19 +11,57 @@ import { useShallow } from "zustand/react/shallow";
|
||||
import { useImportSessionStatus } from "../../hooks/useImportSessionStatus";
|
||||
|
||||
/**
|
||||
* Import statistics with card-based layout and progress bar.
|
||||
* Three states: pre-import (idle), importing (active), and post-import (complete).
|
||||
* Also surfaces missing files detected by the file watcher.
|
||||
* RealTimeImportStats component displays import statistics with a card-based layout and progress bar.
|
||||
*
|
||||
* This component manages three distinct states:
|
||||
* - **Pre-import (idle)**: Shows current file counts and "Start Import" button when new files exist
|
||||
* - **Importing (active)**: Displays real-time progress bar with completed/total counts
|
||||
* - **Post-import (complete)**: Shows final statistics including failed imports
|
||||
*
|
||||
* Additionally, it surfaces missing files detected by the file watcher, allowing users
|
||||
* to see which previously-imported files are no longer found on disk.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RealTimeImportStats />
|
||||
* ```
|
||||
*
|
||||
* @returns {ReactElement} The rendered import statistics component
|
||||
*
|
||||
* @remarks
|
||||
* The component subscribes to multiple socket events for real-time updates:
|
||||
* - `LS_LIBRARY_STATS` / `LS_FILES_MISSING`: Triggers statistics refresh
|
||||
* - `LS_FILE_DETECTED`: Shows toast notification for newly detected files
|
||||
* - `LS_INCREMENTAL_IMPORT_STARTED`: Initializes progress tracking
|
||||
* - `LS_COVER_EXTRACTED` / `LS_COVER_EXTRACTION_FAILED`: Updates progress counts
|
||||
* - `LS_IMPORT_QUEUE_DRAINED`: Marks import as complete
|
||||
*
|
||||
* @see {@link useImportSessionStatus} for import session state management
|
||||
* @see {@link useGetImportStatisticsQuery} for fetching import statistics
|
||||
*/
|
||||
export const RealTimeImportStats = (): ReactElement => {
|
||||
/** Current import error message to display, or null if no error */
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
|
||||
/** Name of recently detected file for toast notification, auto-clears after 5 seconds */
|
||||
const [detectedFile, setDetectedFile] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Real-time import progress state tracked via socket events.
|
||||
* Separate from GraphQL query data to provide immediate UI updates.
|
||||
*/
|
||||
const [socketImport, setSocketImport] = useState<{
|
||||
/** Whether import is currently in progress */
|
||||
active: boolean;
|
||||
/** Number of successfully completed import jobs */
|
||||
completed: number;
|
||||
/** Total number of jobs in the import queue */
|
||||
total: number;
|
||||
/** Number of failed import jobs */
|
||||
failed: number;
|
||||
} | null>(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { getSocket, disconnectSocket, importJobQueue } = useStore(
|
||||
@@ -34,7 +72,7 @@ export const RealTimeImportStats = (): ReactElement => {
|
||||
})),
|
||||
);
|
||||
|
||||
const { data: importStats, isLoading } = useGetImportStatisticsQuery(
|
||||
const { data: importStats, isLoading, isError: isStatsError, error: statsError } = useGetImportStatisticsQuery(
|
||||
{},
|
||||
{ refetchOnWindowFocus: false, refetchInterval: false },
|
||||
);
|
||||
@@ -182,10 +220,35 @@ export const RealTimeImportStats = (): ReactElement => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading || !stats) {
|
||||
if (isLoading) {
|
||||
return <div className="text-gray-500 dark:text-gray-400">Loading...</div>;
|
||||
}
|
||||
|
||||
if (isStatsError || !stats) {
|
||||
return (
|
||||
<div className="rounded-lg border-l-4 border-red-500 bg-red-50 dark:bg-red-900/20 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="w-6 h-6 text-red-600 dark:text-red-400 mt-0.5">
|
||||
<i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i>
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-red-800 dark:text-red-300">
|
||||
Failed to Load Import Statistics
|
||||
</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-1">
|
||||
Unable to retrieve import statistics from the server. Please check that the backend service is running.
|
||||
</p>
|
||||
{isStatsError && (
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-2">
|
||||
Error: {statsError instanceof Error ? statsError.message : "Unknown error"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isFirstImport = stats.alreadyImported === 0;
|
||||
const buttonText = isFirstImport
|
||||
? `Start Import (${stats.newFiles} files)`
|
||||
|
||||
Reference in New Issue
Block a user