/** * Orphaned Tables Panel * * Lists third-party database tables whose owning plugin is deactivated, * uninstalled, or unidentified. Detection is report-only; removal is offered * only for tables whose owner is confirmed uninstalled and requires typing * DELETE to confirm. */ import { useState, useEffect, useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { Spinner } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; import ProrankButton from '../../../components/ProrankButton'; import { useNotification } from '../../../contexts/NotificationContext'; import * as React from 'react'; interface OrphanTable { table: string; status: 'orphaned' | 'inactive' | 'unknown'; owner: string; size_kb: number; rows: number; } const STATUS_META: Record = { orphaned: { label: __('Plugin uninstalled', 'prorank-seo'), color: '#b91c1c', bg: '#fef2f2' }, inactive: { label: __('Plugin deactivated', 'prorank-seo'), color: '#92400e', bg: '#fffbeb' }, unknown: { label: __('Unidentified', 'prorank-seo'), color: '#475569', bg: '#f1f5f9' }, }; const OrphanedTablesPanel: React.FC = () => { const [tables, setTables] = useState([]); const [loading, setLoading] = useState(true); const [dropping, setDropping] = useState(''); const { showNotification } = useNotification(); const loadTables = useCallback(async () => { setLoading(true); try { const response = await apiFetch<{ tables: OrphanTable[] }>({ path: '/prorank-seo/v1/database-optimization/orphaned-tables', }); setTables(Array.isArray(response?.tables) ? response.tables : []); } catch (error) { setTables([]); } finally { setLoading(false); } }, []); useEffect(() => { loadTables(); }, [loadTables]); const handleDrop = async (table: OrphanTable) => { const confirmation = window.prompt( sprintf( /* translators: %s: database table name */ __('This permanently deletes the table %s and all its data. Make sure you have a backup. Type DELETE to confirm.', 'prorank-seo'), table.table ) ); if (confirmation !== 'DELETE') { return; } setDropping(table.table); try { const response = await apiFetch<{ success: boolean; message: string }>({ path: '/prorank-seo/v1/database-optimization/orphaned-tables/drop', method: 'POST', data: { table: table.table, confirm: 'DELETE' }, }); showNotification(response?.message || __('Table removed.', 'prorank-seo'), 'success'); await loadTables(); } catch (error: any) { showNotification(error?.message || __('Failed to remove table.', 'prorank-seo'), 'error'); } finally { setDropping(''); } }; return (

{__('Leftover Plugin Tables', 'prorank-seo')}

{__('Tables created by plugins that are no longer active. Removal is only offered when the owning plugin is uninstalled — unidentified tables are listed for information only.', 'prorank-seo')}

{loading ? (
) : tables.length === 0 ? (
{__('No leftover tables found — every third-party table belongs to an active plugin.', 'prorank-seo')}
) : ( {tables.map((table) => { const meta = STATUS_META[table.status] || STATUS_META.unknown; return ( ); })}
{__('Table', 'prorank-seo')} {__('Owner', 'prorank-seo')} {__('Status', 'prorank-seo')} {__('Size', 'prorank-seo')}
{table.table} {table.owner || '—'} {meta.label} {table.size_kb >= 1024 ? `${(table.size_kb / 1024).toFixed(1)} MB` : `${table.size_kb.toFixed(1)} KB`} {table.status === 'orphaned' && ( handleDrop(table)} > {dropping === table.table ? __('Removing…', 'prorank-seo') : __('Remove', 'prorank-seo')} )}
)}
); }; export default OrphanedTablesPanel;