Refactor WordPress integration service to use API key for connection testing
- Updated the `IntegrationService` to perform connection tests using only the API key, removing reliance on username and app password. - Simplified health check logic and improved error messaging for better clarity. - Added functionality to revoke API keys in the `WordPressIntegrationForm` component. - Enhanced site settings page with a site selector and improved integration status display. - Cleaned up unused code and improved overall structure for better maintainability.
This commit is contained in:
@@ -16,7 +16,8 @@ import {
|
||||
AlertIcon,
|
||||
DownloadIcon,
|
||||
PlusIcon,
|
||||
CopyIcon
|
||||
CopyIcon,
|
||||
TrashBinIcon
|
||||
} from '../../icons';
|
||||
import { Globe, Key, RefreshCw } from 'lucide-react';
|
||||
|
||||
@@ -40,15 +41,6 @@ export default function WordPressIntegrationForm({
|
||||
const [generatingKey, setGeneratingKey] = useState(false);
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
const [apiKeyVisible, setApiKeyVisible] = useState(false);
|
||||
const [isActive, setIsActive] = useState(integration?.is_active ?? true);
|
||||
const [syncEnabled, setSyncEnabled] = useState(integration?.sync_enabled ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
if (integration) {
|
||||
setIsActive(integration.is_active ?? true);
|
||||
setSyncEnabled(integration.sync_enabled ?? true);
|
||||
}
|
||||
}, [integration]);
|
||||
|
||||
// Load API key from site settings on mount
|
||||
useEffect(() => {
|
||||
@@ -121,6 +113,40 @@ export default function WordPressIntegrationForm({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeApiKey = async () => {
|
||||
if (!confirm('Are you sure you want to revoke the API key? Your WordPress plugin will stop working until you generate a new key.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setGeneratingKey(true);
|
||||
// Clear API key from site settings by setting it to empty string
|
||||
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ wp_api_key: '' }),
|
||||
});
|
||||
|
||||
setApiKey('');
|
||||
setApiKeyVisible(false);
|
||||
|
||||
// Trigger integration update to reload the integration state
|
||||
if (onIntegrationUpdate && integration) {
|
||||
await loadApiKeyFromSite();
|
||||
// Reload integration to reflect changes
|
||||
const integrations = await integrationApi.getSiteIntegrations(siteId);
|
||||
const wp = integrations.find(i => i.platform === 'wordpress');
|
||||
if (wp) {
|
||||
onIntegrationUpdate(wp);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('API key revoked successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to revoke API key: ${error.message}`);
|
||||
} finally {
|
||||
setGeneratingKey(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveApiKeyToSite = async (key: string) => {
|
||||
try {
|
||||
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
|
||||
@@ -146,108 +172,103 @@ export default function WordPressIntegrationForm({
|
||||
toast.success('Plugin download started');
|
||||
};
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Update integration with active/sync settings
|
||||
if (integration) {
|
||||
await integrationApi.updateIntegration(integration.id, {
|
||||
is_active: isActive,
|
||||
sync_enabled: syncEnabled,
|
||||
} as any);
|
||||
} else {
|
||||
// Create integration if it doesn't exist
|
||||
await integrationApi.saveWordPressIntegration(siteId, {
|
||||
url: siteUrl || '',
|
||||
username: '',
|
||||
app_password: '',
|
||||
api_key: apiKey,
|
||||
is_active: isActive,
|
||||
sync_enabled: syncEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
// Reload integration
|
||||
const updated = await integrationApi.getWordPressIntegration(siteId);
|
||||
if (onIntegrationUpdate && updated) {
|
||||
onIntegrationUpdate(updated);
|
||||
}
|
||||
|
||||
toast.success('Integration settings saved successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to save settings: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!apiKey || !siteUrl) {
|
||||
toast.error('Please ensure API key and site URL are configured');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
// Test connection using API key and site URL
|
||||
const result = await fetchAPI(`/v1/integration/integrations/test-connection/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
site_id: siteId,
|
||||
api_key: apiKey,
|
||||
site_url: siteUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
// Check for fully functional connection (includes bidirectional communication)
|
||||
if (result?.fully_functional) {
|
||||
toast.success('✅ Connection is fully functional! Plugin is connected and can communicate with IGNY8.');
|
||||
} else if (result?.success) {
|
||||
// Basic connection works but not fully functional
|
||||
const issues = result?.issues || [];
|
||||
const healthChecks = result?.health_checks || {};
|
||||
|
||||
// Show specific warning based on what's missing
|
||||
if (!healthChecks.plugin_connected) {
|
||||
toast.warning('⚠️ WordPress is reachable but the plugin is not configured with an API key. Please add the API key in your WordPress plugin settings.');
|
||||
} else if (!healthChecks.bidirectional_communication) {
|
||||
toast.warning('⚠️ Plugin is configured but cannot reach IGNY8 backend. Please check your WordPress site\'s outbound connections and firewall settings.');
|
||||
} else {
|
||||
toast.warning(`⚠️ ${result?.message || 'Connection partially working. Some features may not function correctly.'}`);
|
||||
}
|
||||
} else {
|
||||
// Connection completely failed
|
||||
toast.error(`❌ Connection test failed: ${result?.message || 'Unknown error'}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Connection test failed: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const maskApiKey = (key: string) => {
|
||||
if (!key) return '';
|
||||
if (key.length <= 12) return key;
|
||||
return key.substring(0, 8) + '**********' + key.substring(key.length - 4);
|
||||
};
|
||||
|
||||
// Toggle integration enabled status
|
||||
const [integrationEnabled, setIntegrationEnabled] = useState(integration?.is_active ?? true);
|
||||
|
||||
const handleToggleIntegration = async (enabled: boolean) => {
|
||||
try {
|
||||
setIntegrationEnabled(enabled);
|
||||
|
||||
if (integration) {
|
||||
// Update existing integration
|
||||
await integrationApi.updateIntegration(integration.id, {
|
||||
is_active: enabled,
|
||||
} as any);
|
||||
toast.success(enabled ? 'Integration enabled' : 'Integration disabled');
|
||||
|
||||
// Reload integration
|
||||
const updated = await integrationApi.getWordPressIntegration(siteId);
|
||||
if (onIntegrationUpdate && updated) {
|
||||
onIntegrationUpdate(updated);
|
||||
}
|
||||
} else if (enabled && apiKey) {
|
||||
// Create integration when enabling for first time
|
||||
await integrationApi.saveWordPressIntegration(siteId, {
|
||||
url: siteUrl || '',
|
||||
username: '',
|
||||
app_password: '',
|
||||
api_key: apiKey,
|
||||
is_active: enabled,
|
||||
sync_enabled: true,
|
||||
});
|
||||
toast.success('Integration created and enabled');
|
||||
|
||||
// Reload integration
|
||||
const updated = await integrationApi.getWordPressIntegration(siteId);
|
||||
if (onIntegrationUpdate && updated) {
|
||||
onIntegrationUpdate(updated);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to update integration: ${error.message}`);
|
||||
// Revert on error
|
||||
setIntegrationEnabled(!enabled);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (integration) {
|
||||
setIntegrationEnabled(integration.is_active ?? true);
|
||||
}
|
||||
}, [integration]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
|
||||
<Globe className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
WordPress Integration
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Connect your WordPress site using the IGNY8 WP Bridge plugin
|
||||
</p>
|
||||
{/* Header with Toggle */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-3 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
|
||||
<Globe className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
WordPress Integration
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Connect your WordPress site using the IGNY8 WP Bridge plugin
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Switch */}
|
||||
{apiKey && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{integrationEnabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleIntegration(!integrationEnabled)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 ${
|
||||
integrationEnabled ? 'bg-brand-600' : 'bg-gray-300 dark:bg-gray-600'
|
||||
}`}
|
||||
role="switch"
|
||||
aria-checked={integrationEnabled}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
integrationEnabled ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* API Keys Table */}
|
||||
@@ -366,11 +387,19 @@ export default function WordPressIntegrationForm({
|
||||
<button
|
||||
onClick={handleRegenerateApiKey}
|
||||
disabled={generatingKey}
|
||||
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-500 disabled:opacity-50"
|
||||
title="Regenerate"
|
||||
className="text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-400 disabled:opacity-50 transition-colors"
|
||||
title="Regenerate API key"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRevokeApiKey}
|
||||
disabled={generatingKey}
|
||||
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-400 disabled:opacity-50 transition-colors"
|
||||
title="Revoke API key"
|
||||
>
|
||||
<TrashBinIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -407,104 +436,6 @@ export default function WordPressIntegrationForm({
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Integration Settings */}
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Integration Settings
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Checkboxes at the top */}
|
||||
<div className="space-y-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={isActive}
|
||||
onChange={(checked) => setIsActive(checked)}
|
||||
label="Enable Integration"
|
||||
/>
|
||||
<Checkbox
|
||||
id="sync_enabled"
|
||||
checked={syncEnabled}
|
||||
onChange={(checked) => setSyncEnabled(checked)}
|
||||
label="Enable Two-Way Sync"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4">
|
||||
{apiKey && siteUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||
Test Connection
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
onClick={handleSaveSettings}
|
||||
disabled={loading || !apiKey}
|
||||
>
|
||||
{loading ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Integration Status */}
|
||||
{integration && (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Integration Status
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Status</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{integration.is_active ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : (
|
||||
<AlertIcon className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{integration.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Sync Status</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{integration.sync_status === 'success' ? (
|
||||
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
||||
) : integration.sync_status === 'failed' ? (
|
||||
<AlertIcon className="w-4 h-4 text-red-500" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 text-yellow-500 animate-spin" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white capitalize">
|
||||
{integration.sync_status || 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{integration.last_sync_at && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Last Sync</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white">
|
||||
{new Date(integration.last_sync_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ import SelectDropdown from '../../components/form/SelectDropdown';
|
||||
import Checkbox from '../../components/form/input/Checkbox';
|
||||
import TextArea from '../../components/form/input/TextArea';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, runSync } from '../../services/api';
|
||||
import { fetchAPI, runSync, fetchSites, Site } from '../../services/api';
|
||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon } from '../../icons';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
|
||||
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
|
||||
|
||||
export default function SiteSettings() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
@@ -31,6 +33,12 @@ export default function SiteSettings() {
|
||||
const [wordPressIntegration, setWordPressIntegration] = useState<SiteIntegration | null>(null);
|
||||
const [integrationLoading, setIntegrationLoading] = useState(false);
|
||||
|
||||
// Site selector state
|
||||
const [sites, setSites] = useState<Site[]>([]);
|
||||
const [sitesLoading, setSitesLoading] = useState(true);
|
||||
const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false);
|
||||
const siteSelectorRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types'>(initialTab);
|
||||
@@ -39,6 +47,7 @@ export default function SiteSettings() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
site_url: '',
|
||||
site_type: 'marketing',
|
||||
hosting_type: 'igny8_sites',
|
||||
is_active: true,
|
||||
@@ -61,6 +70,12 @@ export default function SiteSettings() {
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
// Clear state when site changes
|
||||
setWordPressIntegration(null);
|
||||
setContentTypes(null);
|
||||
setSite(null);
|
||||
|
||||
// Load new site data
|
||||
loadSite();
|
||||
loadIntegrations();
|
||||
}
|
||||
@@ -80,6 +95,29 @@ export default function SiteSettings() {
|
||||
}
|
||||
}, [activeTab, wordPressIntegration]);
|
||||
|
||||
// Load sites for selector
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
}, []);
|
||||
|
||||
const loadSites = async () => {
|
||||
try {
|
||||
setSitesLoading(true);
|
||||
const response = await fetchSites();
|
||||
const activeSites = (response.results || []).filter(site => site.is_active);
|
||||
setSites(activeSites);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load sites:', error);
|
||||
} finally {
|
||||
setSitesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSiteSelect = (siteId: number) => {
|
||||
navigate(`/sites/${siteId}/settings${searchParams.get('tab') ? `?tab=${searchParams.get('tab')}` : ''}`);
|
||||
setIsSiteSelectorOpen(false);
|
||||
};
|
||||
|
||||
const loadSite = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -90,6 +128,7 @@ export default function SiteSettings() {
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
slug: data.slug || '',
|
||||
site_url: data.domain || data.url || '',
|
||||
site_type: data.site_type || 'marketing',
|
||||
hosting_type: data.hosting_type || 'igny8_sites',
|
||||
is_active: data.is_active !== false,
|
||||
@@ -109,11 +148,7 @@ export default function SiteSettings() {
|
||||
schema_logo: seoData.schema_logo || '',
|
||||
schema_same_as: Array.isArray(seoData.schema_same_as) ? seoData.schema_same_as.join(', ') : seoData.schema_same_as || '',
|
||||
});
|
||||
// If integration record missing but site has stored WP API key or hosting_type wordpress, mark as connected-active
|
||||
if (!wordPressIntegration && (data.wp_api_key || data.hosting_type === 'wordpress')) {
|
||||
setIntegrationTestStatus('connected');
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
}
|
||||
// Don't automatically mark as connected - wait for actual connection test
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load site: ${error.message}`);
|
||||
@@ -170,84 +205,51 @@ export default function SiteSettings() {
|
||||
return `${months}mo ago`;
|
||||
};
|
||||
|
||||
// Integration test status & periodic check (every 60 minutes)
|
||||
const [integrationTestStatus, setIntegrationTestStatus] = useState<'connected' | 'pending' | 'error' | 'not_configured'>('not_configured');
|
||||
const [integrationLastChecked, setIntegrationLastChecked] = useState<string | null>(null);
|
||||
const integrationCheckRef = useRef<number | null>(null);
|
||||
const integrationErrorCooldownRef = useRef<number | null>(null);
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const runIntegrationTest = async () => {
|
||||
// respect cooldown on repeated server errors
|
||||
if (integrationErrorCooldownRef.current && Date.now() < integrationErrorCooldownRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!wordPressIntegration && !site) {
|
||||
setIntegrationTestStatus('not_configured');
|
||||
return;
|
||||
}
|
||||
// Integration status with authentication check
|
||||
const [integrationStatus, setIntegrationStatus] = useState<'connected' | 'configured' | 'not_configured'>('not_configured');
|
||||
const [testingAuth, setTestingAuth] = useState(false);
|
||||
|
||||
// Check basic configuration (API key + toggle)
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
if (wordPressIntegration && wordPressIntegration.is_active && site?.wp_api_key) {
|
||||
setIntegrationStatus('configured');
|
||||
// Test authentication
|
||||
testAuthentication();
|
||||
} else {
|
||||
setIntegrationStatus('not_configured');
|
||||
}
|
||||
};
|
||||
checkStatus();
|
||||
}, [wordPressIntegration, site]);
|
||||
|
||||
// Test authentication with WordPress API
|
||||
const testAuthentication = async () => {
|
||||
if (testingAuth || !wordPressIntegration?.id) return;
|
||||
|
||||
try {
|
||||
setIntegrationTestStatus('pending');
|
||||
let resp: any = null;
|
||||
// Only run server-side test if we have an integration record to avoid triggering collection-level 500s
|
||||
if (wordPressIntegration && wordPressIntegration.id) {
|
||||
resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, { method: 'POST', body: {} });
|
||||
} else {
|
||||
// If no integration record, do not call server test here — just mark connected if site has local WP credentials.
|
||||
if (site?.wp_api_key || site?.wp_url || site?.hosting_type === 'wordpress') {
|
||||
// Assume connected (plugin shows connection) but do not invoke server test to avoid 500s.
|
||||
setIntegrationTestStatus('connected');
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
return;
|
||||
} else {
|
||||
setIntegrationTestStatus('not_configured');
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTestingAuth(true);
|
||||
const resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, {
|
||||
method: 'POST',
|
||||
body: {}
|
||||
});
|
||||
|
||||
if (resp && resp.success) {
|
||||
setIntegrationTestStatus('connected');
|
||||
// clear any error cooldown
|
||||
integrationErrorCooldownRef.current = null;
|
||||
setIntegrationStatus('connected');
|
||||
} else {
|
||||
setIntegrationTestStatus('error');
|
||||
// set cooldown to 60 minutes to avoid repeated 500s
|
||||
integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000;
|
||||
// Keep as 'configured' if auth fails
|
||||
setIntegrationStatus('configured');
|
||||
}
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
} catch (err) {
|
||||
setIntegrationTestStatus('error');
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
// Keep as 'configured' if auth test fails
|
||||
setIntegrationStatus('configured');
|
||||
} finally {
|
||||
setTestingAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// run initial test when page loads / integration/site known
|
||||
runIntegrationTest();
|
||||
|
||||
// schedule hourly checks (one per hour) — less intrusive
|
||||
if (integrationCheckRef.current) {
|
||||
window.clearInterval(integrationCheckRef.current);
|
||||
integrationCheckRef.current = null;
|
||||
}
|
||||
integrationCheckRef.current = window.setInterval(() => {
|
||||
runIntegrationTest();
|
||||
}, 60 * 60 * 1000); // 60 minutes
|
||||
|
||||
return () => {
|
||||
if (integrationCheckRef.current) {
|
||||
window.clearInterval(integrationCheckRef.current);
|
||||
integrationCheckRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [wordPressIntegration, site]);
|
||||
|
||||
// when contentTypes last_structure_fetch updates (content was synced), re-run test once
|
||||
useEffect(() => {
|
||||
if (contentTypes?.last_structure_fetch) {
|
||||
runIntegrationTest();
|
||||
}
|
||||
}, [contentTypes?.last_structure_fetch]);
|
||||
|
||||
// Sync Now handler extracted
|
||||
const [syncLoading, setSyncLoading] = useState(false);
|
||||
const handleManualSync = async () => {
|
||||
setSyncLoading(true);
|
||||
try {
|
||||
@@ -260,67 +262,10 @@ export default function SiteSettings() {
|
||||
toast.error(res?.message || 'Sync failed to start');
|
||||
}
|
||||
} else {
|
||||
// No integration record — attempt a site-level sync job instead of calling test-connection (avoids server 500 test endpoint)
|
||||
// This will trigger the site sync runner which is safer and returns structured result
|
||||
try {
|
||||
const runResult = await runSync(Number(siteId), 'from_external');
|
||||
if (runResult && runResult.sync_results) {
|
||||
toast.success('Site sync started (from external).');
|
||||
// Refresh integrations and content types after a short delay
|
||||
setTimeout(async () => {
|
||||
await loadIntegrations();
|
||||
await loadContentTypes();
|
||||
}, 2000);
|
||||
setIntegrationTestStatus('connected');
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
} else {
|
||||
toast.error('Failed to start site sync.');
|
||||
setIntegrationTestStatus('error');
|
||||
integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000;
|
||||
}
|
||||
} catch (e: any) {
|
||||
// If there are no active integrations, attempt a collection-level test (if site has WP creds),
|
||||
// otherwise prompt the user to configure the integration.
|
||||
const errResp = e?.response || {};
|
||||
const errMessage = errResp?.error || e?.message || '';
|
||||
if (errMessage && errMessage.includes('No active integrations found')) {
|
||||
if (site?.wp_api_key || site?.wp_url) {
|
||||
try {
|
||||
const body = {
|
||||
site_id: siteId ? Number(siteId) : undefined,
|
||||
api_key: site?.wp_api_key,
|
||||
site_url: site?.wp_url,
|
||||
};
|
||||
const resp = await fetchAPI(`/v1/integration/integrations/test-connection/`, { method: 'POST', body: JSON.stringify(body) });
|
||||
if (resp && resp.success) {
|
||||
toast.success('Connection verified (collection). Fetching integrations...');
|
||||
setIntegrationTestStatus('connected');
|
||||
setIntegrationLastChecked(new Date().toISOString());
|
||||
await loadIntegrations();
|
||||
await loadContentTypes();
|
||||
} else {
|
||||
toast.error(resp?.message || 'Connection test failed (collection).');
|
||||
setIntegrationTestStatus('error');
|
||||
}
|
||||
} catch (innerErr: any) {
|
||||
toast.error(innerErr?.message || 'Collection-level connection test failed.');
|
||||
setIntegrationTestStatus('error');
|
||||
}
|
||||
} else {
|
||||
toast.error('No active integrations found for this site. Please configure WordPress integration in the Integrations tab.');
|
||||
setIntegrationTestStatus('not_configured');
|
||||
}
|
||||
} else {
|
||||
toast.error(e?.message || 'Failed to run site sync.');
|
||||
setIntegrationTestStatus('error');
|
||||
integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
toast.error('No integration configured. Please configure WordPress integration first.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(`Sync failed: ${err?.message || String(err)}`);
|
||||
setIntegrationTestStatus('error');
|
||||
integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000;
|
||||
} finally {
|
||||
setSyncLoading(false);
|
||||
}
|
||||
@@ -394,29 +339,82 @@ export default function SiteSettings() {
|
||||
<div className="p-6">
|
||||
<PageMeta title="Site Settings - IGNY8" />
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<PageHeader
|
||||
title={site?.name ? `${site.name} — Site Settings` : 'Site Settings'}
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
hideSiteSector
|
||||
/>
|
||||
{/* Integration status indicator (larger) */}
|
||||
<div className="flex items-center gap-3 ml-2">
|
||||
<span
|
||||
className={`inline-block w-6 h-6 rounded-full ${
|
||||
integrationTestStatus === 'connected' ? 'bg-green-500' :
|
||||
integrationTestStatus === 'pending' ? 'bg-yellow-400' :
|
||||
integrationTestStatus === 'error' ? 'bg-red-500' : 'bg-gray-300'
|
||||
}`}
|
||||
title={`Integration status: ${integrationTestStatus}${integrationLastChecked ? ' • last checked ' + formatRelativeTime(integrationLastChecked) : ''}`}
|
||||
<div className="flex items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<PageHeader
|
||||
title={site?.name ? `${site.name} — Site Settings` : 'Site Settings'}
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
hideSiteSector
|
||||
/>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{integrationTestStatus === 'connected' && 'Connected'}
|
||||
{integrationTestStatus === 'pending' && 'Checking...'}
|
||||
{integrationTestStatus === 'error' && 'Error'}
|
||||
{integrationTestStatus === 'not_configured' && 'Not configured'}
|
||||
</span>
|
||||
{/* Integration status indicator */}
|
||||
<div className="flex items-center gap-3 ml-2">
|
||||
<span
|
||||
className={`inline-block w-6 h-6 rounded-full ${
|
||||
integrationStatus === 'connected' ? 'bg-green-500' :
|
||||
integrationStatus === 'configured' ? 'bg-brand-500' : 'bg-gray-300'
|
||||
}`}
|
||||
title={`Integration status: ${
|
||||
integrationStatus === 'connected' ? 'Connected & Authenticated' :
|
||||
integrationStatus === 'configured' ? 'Configured (testing...)' : 'Not configured'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{integrationStatus === 'connected' && 'Connected'}
|
||||
{integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')}
|
||||
{integrationStatus === 'not_configured' && 'Not configured'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site Selector - Only show if more than 1 site */}
|
||||
{!sitesLoading && sites.length > 1 && (
|
||||
<div className="relative inline-block">
|
||||
<button
|
||||
ref={siteSelectorRef}
|
||||
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-brand-200 rounded-lg hover:bg-brand-50 hover:border-brand-300 dark:bg-gray-800 dark:text-gray-300 dark:border-brand-700/50 dark:hover:bg-brand-500/10 dark:hover:border-brand-600/50 transition-colors"
|
||||
aria-label="Switch site"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<GridIcon className="w-4 h-4 text-brand-500 dark:text-brand-400" />
|
||||
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
|
||||
</span>
|
||||
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<Dropdown
|
||||
isOpen={isSiteSelectorOpen}
|
||||
onClose={() => setIsSiteSelectorOpen(false)}
|
||||
anchorRef={siteSelectorRef}
|
||||
>
|
||||
{sites.map((s) => (
|
||||
<DropdownItem
|
||||
key={s.id}
|
||||
onItemClick={() => handleSiteSelect(s.id)}
|
||||
className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${
|
||||
site?.id === s.id
|
||||
? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300"
|
||||
: "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span className="flex-1">{s.name}</span>
|
||||
{site?.id === s.id && (
|
||||
<svg
|
||||
className="w-4 h-4 text-brand-600 dark:text-brand-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
@@ -644,6 +642,17 @@ export default function SiteSettings() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Site URL</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.site_url}
|
||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Site Type</Label>
|
||||
<SelectDropdown
|
||||
|
||||
Reference in New Issue
Block a user