diff --git a/frontend/public/schemas/20251121/linkml/manifest.json b/frontend/public/schemas/20251121/linkml/manifest.json index 317083d99e..9d462e068d 100644 --- a/frontend/public/schemas/20251121/linkml/manifest.json +++ b/frontend/public/schemas/20251121/linkml/manifest.json @@ -1,5 +1,5 @@ { - "generated": "2025-12-07T16:47:16.823Z", + "generated": "2025-12-07T18:19:27.338Z", "version": "1.0.0", "categories": [ { diff --git a/frontend/src/components/database/DuckLakePanel.tsx b/frontend/src/components/database/DuckLakePanel.tsx index 38bd706879..66090fbd46 100644 --- a/frontend/src/components/database/DuckLakePanel.tsx +++ b/frontend/src/components/database/DuckLakePanel.tsx @@ -335,6 +335,11 @@ const TEXT = { backToTables: { nl: '← Terug naar tabellen', en: '← Back to tables' }, schema: { nl: 'Schema', en: 'Schema' }, data: { nl: 'Data', en: 'Data' }, + // Search + search: { nl: 'Zoeken', en: 'Search' }, + searchPlaceholder: { nl: 'Zoek in alle kolommen...', en: 'Search across all columns...' }, + resultsFiltered: { nl: 'resultaten gefilterd', en: 'results filtered' }, + clearSearch: { nl: 'Wissen', en: 'Clear' }, // Disconnected state messages backendRequired: { nl: 'Server-backend Vereist', @@ -418,11 +423,12 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { const [rowHeights, setRowHeights] = useState>({}); const [isFullWidth, setIsFullWidth] = useState(false); // Full-width mode for row detail view const [showWebArchive, setShowWebArchive] = useState(false); // Web archive viewer mode + const [searchQuery, setSearchQuery] = useState(''); // Search filter for table data const [webArchiveData, setWebArchiveData] = useState<{ ghcid: string; url: string; pages: { title: string; path: string; archived_file: string }[]; - claims: { claim_id: string; claim_type: string; text_content: string; xpath: string; hypernym: string }[]; + claims: { claim_id: string; claim_type: string; text_content: string; xpath: string; hypernym: string; source_page: string }[]; } | null>(null); const [selectedWebPage, setSelectedWebPage] = useState(null); const PAGE_SIZE = 50; @@ -592,6 +598,7 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { setIsLoadingData(true); setSelectedTable(tableName); setDataPage(0); + setSearchQuery(''); // Reset search when loading new table try { const offset = 0; // Tables are in the 'heritage' schema within DuckLake @@ -707,6 +714,21 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { return 'Record'; }; + // Filter rows based on search query (case-insensitive across all columns) + const filterRowsBySearch = useCallback((rows: unknown[][], _columns: string[], query: string): unknown[][] => { + if (!query.trim()) return rows; + const lowerQuery = query.toLowerCase(); + return rows.filter(row => { + return row.some(cell => { + if (cell === null || cell === undefined) return false; + const cellStr = typeof cell === 'object' + ? JSON.stringify(cell).toLowerCase() + : String(cell).toLowerCase(); + return cellStr.includes(lowerQuery); + }); + }); + }, []); + // Get GHCID from row data (try common GHCID fields) const getRowGhcid = (rowObj: Record): string | null => { const ghcidFields = ['ghcid', 'ghcid_current', 'file_name']; @@ -746,9 +768,9 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { selectedSnapshot ); - // Get claims + // Get claims (including source_page for filtering) const claimsResult = await executeQuery( - `SELECT claim_id, claim_type, text_content, xpath, hypernym FROM heritage.web_claims WHERE ghcid = '${ghcid}' LIMIT 100`, + `SELECT claim_id, claim_type, text_content, xpath, hypernym, source_page FROM heritage.web_claims WHERE ghcid = '${ghcid}' LIMIT 200`, selectedSnapshot ); @@ -765,7 +787,8 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { claim_type: r[1] as string, text_content: r[2] as string, xpath: r[3] as string, - hypernym: r[4] as string + hypernym: r[4] as string, + source_page: r[5] as string })) }); setShowWebArchive(true); @@ -1174,6 +1197,36 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { + {/* Search Bar */} + {explorerView === 'data' && !expandedRow && ( +
+
+ 🔍 + setSearchQuery(e.target.value)} + placeholder={t('searchPlaceholder')} + className="search-input" + /> + {searchQuery && ( + + )} +
+ {searchQuery && tableData && ( + + {filterRowsBySearch(tableData.rows, tableData.columns, searchQuery).length} / {tableData.rows.length} {t('resultsFiltered')} + + )} +
+ )} + {explorerView === 'schema' ? ( /* Schema View */
@@ -1265,18 +1318,22 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { )}
- {webArchiveData.pages.map((page, idx) => ( -
setSelectedWebPage( - selectedWebPage === page.archived_file ? null : page.archived_file - )} - > - {page.title || 'Untitled'} - {page.path} -
- ))} + {webArchiveData.pages.map((page, idx) => { + // Extract filename from archived_file (e.g., "pages/index.html" -> "index.html") + const pageFile = page.archived_file?.split('/').pop() || ''; + return ( +
setSelectedWebPage( + selectedWebPage === pageFile ? null : pageFile + )} + > + {page.title || 'Untitled'} + {page.path} +
+ ); + })} {webArchiveData.pages.length === 0 && (
No archived pages found
)} @@ -1285,41 +1342,60 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { {/* Extracted Claims */}
-
-
- 🏷️ Extracted Claims ({webArchiveData.claims.length}) - {selectedWebPage && ( - — from main page - )} -
-
-
- Claims are extracted from the main page (index.html). - Per-page extraction coming soon. -
-
- {webArchiveData.claims.map((claim, idx) => ( -
-
- - {claim.claim_type} - - {claim.hypernym && ( - {claim.hypernym} + {(() => { + // Filter claims by selected page + const filteredClaims = selectedWebPage + ? webArchiveData.claims.filter(c => c.source_page === selectedWebPage) + : webArchiveData.claims; + + return ( + <> +
+
+ 🏷️ Extracted Claims ({filteredClaims.length} + {selectedWebPage && ` / ${webArchiveData.claims.length} total`}) + {selectedWebPage && ( + — filtered by page + )} +
+
+
+ {selectedWebPage + ? `Showing claims from: ${selectedWebPage}. Click "Clear" above to see all.` + : 'Click a page to filter claims from that page only.' + } +
+
+ {filteredClaims.map((claim, idx) => ( +
+
+ + {claim.claim_type} + + {claim.hypernym && ( + {claim.hypernym} + )} +
+
{claim.text_content}
+ {claim.xpath && ( +
+ 📍 {claim.xpath.substring(0, 60)}... +
+ )} +
+ ))} + {filteredClaims.length === 0 && ( +
+ {selectedWebPage + ? 'No claims extracted from this page' + : 'No claims extracted' + } +
)}
-
{claim.text_content}
- {claim.xpath && ( -
- 📍 {claim.xpath.substring(0, 60)}... -
- )} -
- ))} - {webArchiveData.claims.length === 0 && ( -
No claims extracted
- )} -
+ + ); + })()}
@@ -1365,41 +1441,46 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { - {tableData.rows.map((row, rowIdx) => { - const rowObj = rowToObject(row, tableData.columns); - const rowHeight = rowHeights[rowIdx]; - return ( - setExpandedRow({ - rowIndex: dataPage * PAGE_SIZE + rowIdx + 1, - data: rowObj - })} - > - - -
handleRowResizeStart(e, rowIdx, rowHeight || 40)} - onClick={(e) => e.stopPropagation()} - /> - - {row.map((cell, cellIdx) => ( - -
- {formatCellValue(cell)} -
+ {(() => { + const filteredRows = filterRowsBySearch(tableData.rows, tableData.columns, searchQuery); + return filteredRows.map((row, rowIdx) => { + const rowObj = rowToObject(row, tableData.columns); + const rowHeight = rowHeights[rowIdx]; + // Find original index for accurate row numbering + const originalIdx = tableData.rows.indexOf(row); + return ( + setExpandedRow({ + rowIndex: dataPage * PAGE_SIZE + originalIdx + 1, + data: rowObj + })} + > + + +
handleRowResizeStart(e, rowIdx, rowHeight || 40)} + onClick={(e) => e.stopPropagation()} + /> - ))} - - ); - })} + {row.map((cell, cellIdx) => ( + +
+ {formatCellValue(cell)} +
+ + ))} + + ); + }); + })()}
@@ -1412,7 +1493,10 @@ export function DuckLakePanel({ compact = false }: DuckLakePanelProps) { ← {t('previous')} - {t('showingRows')} {dataPage * PAGE_SIZE + 1} - {dataPage * PAGE_SIZE + tableData.rows.length} + {searchQuery + ? `${filterRowsBySearch(tableData.rows, tableData.columns, searchQuery).length} ${t('resultsFiltered')}` + : `${t('showingRows')} ${dataPage * PAGE_SIZE + 1} - ${dataPage * PAGE_SIZE + tableData.rows.length}` + }