Full parallel implementation of typesense and opensearch

This commit is contained in:
Stefan Hardegger
2025-09-20 09:40:09 +02:00
parent 54df3c471e
commit f1773873d4
20 changed files with 2869 additions and 290 deletions

View File

@@ -1,14 +1,39 @@
'use client';
import { useState } from 'react';
import React, { useState, useEffect } from 'react';
import Button from '../ui/Button';
import { storyApi, authorApi, databaseApi, configApi } from '../../lib/api';
import { storyApi, authorApi, databaseApi, configApi, searchAdminApi } from '../../lib/api';
interface SystemSettingsProps {
// No props needed - this component manages its own state
}
export default function SystemSettings({}: SystemSettingsProps) {
const [searchEngineStatus, setSearchEngineStatus] = useState<{
currentEngine: string;
dualWrite: boolean;
typesenseAvailable: boolean;
openSearchAvailable: boolean;
loading: boolean;
message: string;
success?: boolean;
}>({
currentEngine: 'typesense',
dualWrite: false,
typesenseAvailable: false,
openSearchAvailable: false,
loading: false,
message: ''
});
const [openSearchStatus, setOpenSearchStatus] = useState<{
reindex: { loading: boolean; message: string; success?: boolean };
recreate: { loading: boolean; message: string; success?: boolean };
}>({
reindex: { loading: false, message: '' },
recreate: { loading: false, message: '' }
});
const [typesenseStatus, setTypesenseStatus] = useState<{
reindex: { loading: boolean; message: string; success?: boolean };
recreate: { loading: boolean; message: string; success?: boolean };
@@ -419,13 +444,323 @@ export default function SystemSettings({}: SystemSettingsProps) {
}, 10000);
};
// Search Engine Management Functions
const loadSearchEngineStatus = async () => {
try {
const status = await searchAdminApi.getStatus();
setSearchEngineStatus(prev => ({
...prev,
currentEngine: status.primaryEngine,
dualWrite: status.dualWrite,
typesenseAvailable: status.typesenseAvailable,
openSearchAvailable: status.openSearchAvailable,
}));
} catch (error: any) {
console.error('Failed to load search engine status:', error);
}
};
const handleSwitchEngine = async (engine: string) => {
setSearchEngineStatus(prev => ({ ...prev, loading: true, message: `Switching to ${engine}...` }));
try {
const result = engine === 'opensearch'
? await searchAdminApi.switchToOpenSearch()
: await searchAdminApi.switchToTypesense();
setSearchEngineStatus(prev => ({
...prev,
loading: false,
message: result.message,
success: true,
currentEngine: engine
}));
setTimeout(() => {
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
}, 5000);
} catch (error: any) {
setSearchEngineStatus(prev => ({
...prev,
loading: false,
message: error.message || 'Failed to switch engine',
success: false
}));
setTimeout(() => {
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
}, 5000);
}
};
const handleToggleDualWrite = async () => {
const newDualWrite = !searchEngineStatus.dualWrite;
setSearchEngineStatus(prev => ({
...prev,
loading: true,
message: `${newDualWrite ? 'Enabling' : 'Disabling'} dual-write...`
}));
try {
const result = newDualWrite
? await searchAdminApi.enableDualWrite()
: await searchAdminApi.disableDualWrite();
setSearchEngineStatus(prev => ({
...prev,
loading: false,
message: result.message,
success: true,
dualWrite: newDualWrite
}));
setTimeout(() => {
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
}, 5000);
} catch (error: any) {
setSearchEngineStatus(prev => ({
...prev,
loading: false,
message: error.message || 'Failed to toggle dual-write',
success: false
}));
setTimeout(() => {
setSearchEngineStatus(prev => ({ ...prev, message: '', success: undefined }));
}, 5000);
}
};
const handleOpenSearchReindex = async () => {
setOpenSearchStatus(prev => ({
...prev,
reindex: { loading: true, message: 'Reindexing OpenSearch...', success: undefined }
}));
try {
const result = await searchAdminApi.reindexOpenSearch();
setOpenSearchStatus(prev => ({
...prev,
reindex: {
loading: false,
message: result.success ? result.message : (result.error || 'Reindex failed'),
success: result.success
}
}));
setTimeout(() => {
setOpenSearchStatus(prev => ({
...prev,
reindex: { loading: false, message: '', success: undefined }
}));
}, 8000);
} catch (error: any) {
setOpenSearchStatus(prev => ({
...prev,
reindex: {
loading: false,
message: error.message || 'Network error occurred',
success: false
}
}));
setTimeout(() => {
setOpenSearchStatus(prev => ({
...prev,
reindex: { loading: false, message: '', success: undefined }
}));
}, 8000);
}
};
const handleOpenSearchRecreate = async () => {
setOpenSearchStatus(prev => ({
...prev,
recreate: { loading: true, message: 'Recreating OpenSearch indices...', success: undefined }
}));
try {
const result = await searchAdminApi.recreateOpenSearchIndices();
setOpenSearchStatus(prev => ({
...prev,
recreate: {
loading: false,
message: result.success ? result.message : (result.error || 'Recreation failed'),
success: result.success
}
}));
setTimeout(() => {
setOpenSearchStatus(prev => ({
...prev,
recreate: { loading: false, message: '', success: undefined }
}));
}, 8000);
} catch (error: any) {
setOpenSearchStatus(prev => ({
...prev,
recreate: {
loading: false,
message: error.message || 'Network error occurred',
success: false
}
}));
setTimeout(() => {
setOpenSearchStatus(prev => ({
...prev,
recreate: { loading: false, message: '', success: undefined }
}));
}, 8000);
}
};
// Load status on component mount
useEffect(() => {
loadSearchEngineStatus();
}, []);
return (
<div className="space-y-6">
{/* Typesense Search Management */}
{/* Search Engine Management */}
<div className="theme-card theme-shadow rounded-lg p-6">
<h2 className="text-xl font-semibold theme-header mb-4">Search Index Management</h2>
<h2 className="text-xl font-semibold theme-header mb-4">Search Engine Migration</h2>
<p className="theme-text mb-6">
Manage all Typesense search indexes (stories, authors, collections, etc.). Use these tools if search functionality isn't working properly.
Manage the transition from Typesense to OpenSearch. Switch between engines, enable dual-write mode, and perform maintenance operations.
</p>
<div className="space-y-6">
{/* Current Status */}
<div className="border theme-border rounded-lg p-4">
<h3 className="text-lg font-semibold theme-header mb-3">Current Configuration</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div className="flex justify-between">
<span>Primary Engine:</span>
<span className={`font-medium ${searchEngineStatus.currentEngine === 'opensearch' ? 'text-blue-600 dark:text-blue-400' : 'text-green-600 dark:text-green-400'}`}>
{searchEngineStatus.currentEngine.charAt(0).toUpperCase() + searchEngineStatus.currentEngine.slice(1)}
</span>
</div>
<div className="flex justify-between">
<span>Dual-Write:</span>
<span className={`font-medium ${searchEngineStatus.dualWrite ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400'}`}>
{searchEngineStatus.dualWrite ? 'Enabled' : 'Disabled'}
</span>
</div>
<div className="flex justify-between">
<span>Typesense:</span>
<span className={`font-medium ${searchEngineStatus.typesenseAvailable ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{searchEngineStatus.typesenseAvailable ? 'Available' : 'Unavailable'}
</span>
</div>
<div className="flex justify-between">
<span>OpenSearch:</span>
<span className={`font-medium ${searchEngineStatus.openSearchAvailable ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{searchEngineStatus.openSearchAvailable ? 'Available' : 'Unavailable'}
</span>
</div>
</div>
</div>
{/* Engine Switching */}
<div className="border theme-border rounded-lg p-4">
<h3 className="text-lg font-semibold theme-header mb-3">Engine Controls</h3>
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<Button
onClick={() => handleSwitchEngine('typesense')}
disabled={searchEngineStatus.loading || !searchEngineStatus.typesenseAvailable || searchEngineStatus.currentEngine === 'typesense'}
variant={searchEngineStatus.currentEngine === 'typesense' ? 'primary' : 'ghost'}
className="flex-1"
>
{searchEngineStatus.currentEngine === 'typesense' ? '✓ Typesense (Active)' : 'Switch to Typesense'}
</Button>
<Button
onClick={() => handleSwitchEngine('opensearch')}
disabled={searchEngineStatus.loading || !searchEngineStatus.openSearchAvailable || searchEngineStatus.currentEngine === 'opensearch'}
variant={searchEngineStatus.currentEngine === 'opensearch' ? 'primary' : 'ghost'}
className="flex-1"
>
{searchEngineStatus.currentEngine === 'opensearch' ? '✓ OpenSearch (Active)' : 'Switch to OpenSearch'}
</Button>
<Button
onClick={handleToggleDualWrite}
disabled={searchEngineStatus.loading}
variant={searchEngineStatus.dualWrite ? 'secondary' : 'ghost'}
className="flex-1"
>
{searchEngineStatus.dualWrite ? 'Disable Dual-Write' : 'Enable Dual-Write'}
</Button>
</div>
{searchEngineStatus.message && (
<div className={`text-sm p-3 rounded mb-3 ${
searchEngineStatus.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{searchEngineStatus.message}
</div>
)}
</div>
{/* OpenSearch Operations */}
<div className="border theme-border rounded-lg p-4">
<h3 className="text-lg font-semibold theme-header mb-3">OpenSearch Operations</h3>
<p className="text-sm theme-text mb-4">
Perform maintenance operations on OpenSearch indices. Use these if OpenSearch isn't returning expected results.
</p>
<div className="flex flex-col sm:flex-row gap-3 mb-4">
<Button
onClick={handleOpenSearchReindex}
disabled={openSearchStatus.reindex.loading || openSearchStatus.recreate.loading || !searchEngineStatus.openSearchAvailable}
loading={openSearchStatus.reindex.loading}
variant="ghost"
className="flex-1"
>
{openSearchStatus.reindex.loading ? 'Reindexing...' : '🔄 Reindex OpenSearch'}
</Button>
<Button
onClick={handleOpenSearchRecreate}
disabled={openSearchStatus.reindex.loading || openSearchStatus.recreate.loading || !searchEngineStatus.openSearchAvailable}
loading={openSearchStatus.recreate.loading}
variant="secondary"
className="flex-1"
>
{openSearchStatus.recreate.loading ? 'Recreating...' : '🏗 Recreate Indices'}
</Button>
</div>
{/* OpenSearch Status Messages */}
{openSearchStatus.reindex.message && (
<div className={`text-sm p-3 rounded mb-3 ${
openSearchStatus.reindex.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{openSearchStatus.reindex.message}
</div>
)}
{openSearchStatus.recreate.message && (
<div className={`text-sm p-3 rounded mb-3 ${
openSearchStatus.recreate.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{openSearchStatus.recreate.message}
</div>
)}
</div>
</div>
</div>
{/* Legacy Typesense Management */}
<div className="theme-card theme-shadow rounded-lg p-6">
<h2 className="text-xl font-semibold theme-header mb-4">Legacy Typesense Management</h2>
<p className="theme-text mb-6">
Manage Typesense search indexes (for backwards compatibility and during migration). These tools will be removed once migration is complete.
</p>
<div className="space-y-6">