Phiên bản: 2.0 Ngày cập nhật: 2025 Kiến trúc: Flow-Based Architecture Tác giả: Development Team
LƯU Ý: Tài liệu này mô tả kiến trúc Flow-Based mới. Để hiểu rõ về system concept, vui lòng xem SYSTEM_CONCEPT.md
CrawlFlow Plugin 2.0 là WordPress plugin sử dụng Flow-Based Architecture, cung cấp:
CrawlFlow 2.0 sử dụng Flow-Based Architecture thay vì cấu trúc cũ (Tooth, Reception, Parser). Flow được định nghĩa bằng:
Xem chi tiết về Flow-Based Architecture tại SYSTEM_CONCEPT.md
┌─────────────────────────────────────────────────────────────┐
│ CRAWFLOW PLUGIN │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ DASHBOARD │ │ PROJECTS │ │ MIGRATION │ │
│ │ SYSTEM │ │ MANAGEMENT │ │ INTEGRATION │ │
│ │ │ │ │ │ │ │
│ │ • Overview │ │ • Project CRUD │ │ • Schema │ │
│ │ • Analytics │ │ • Flow Composer │ │ Migration │ │
│ │ • Settings │ │ • Visual Editor │ │ • Status │ │
│ │ • System Info │ │ • Data Preview │ │ • History │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ LOGGING │ │ FRONTEND │ │ ASSETS │ │
│ │ SYSTEM │ │ ASSETS │ │ MANAGEMENT │ │
│ │ │ │ │ │ │ │
│ │ • Log Viewer │ │ • React Composer│ │ • CSS/JS │ │
│ │ • Log Filter │ │ • XYFlow React │ │ • CDN │ │
│ │ • Log Export │ │ • Admin Styles │ │ • Build │ │
│ │ • Analytics │ │ • Responsive │ │ • Minify │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
wp-crawlflow/
├── src/
│ ├── Admin/ # Admin Controllers & Services
│ │ ├── CrawlFlowController.php
│ │ ├── DashboardService.php
│ │ ├── ProjectService.php
│ │ ├── LogService.php
│ │ ├── MigrationService.php
│ │ └── DashboardRenderer.php
│ ├── Kernel/ # Plugin Kernels
│ │ ├── CrawlFlowDashboardKernel.php
│ │ ├── CrawlFlowMigrationKernel.php
│ │ └── CrawlFlowConsoleKernel.php
│ ├── Bootstrapper/ # Plugin Bootstrappers
│ │ ├── CrawlFlowDashboardBootstrapper.php
│ │ ├── CrawlFlowMigrationBootstrapper.php
│ │ └── CrawlFlowCoreBootstrapper.php
│ ├── ServiceProvider/ # Service Providers
│ │ ├── CrawlFlowDashboardServiceProvider.php
│ │ ├── CrawlFlowMigrationServiceProvider.php
│ │ └── CrawlFlowCoreServiceProvider.php
│ └── Assets/ # Asset Management
│ ├── AssetManager.php
│ └── ScriptManager.php
├── assets/
│ ├── css/ # Stylesheets
│ │ ├── admin.css
│ │ └── composer.css
│ └── js/ # JavaScript
│ ├── admin.js
│ ├── composer-simple.js
│ └── composer-test.js
├── wp-crawlflow.php # Main plugin file
├── composer.json
└── README.md
{
"name": "crawlflow/crawlflow",
"require": {
"php": ">=8.1",
"puleeno/rake-wordpress-adapter": "^2.0",
"monolog/monolog": "^3.0"
},
"autoload": {
"psr-4": {
"CrawlFlow\\": "src/"
}
}
}
class WP_CrawlFlow
{
private static ?self $instance = null;
private Rake $app;
private CrawlFlowDashboardKernel $dashboardKernel;
private CrawlFlowMigrationKernel $migrationKernel;
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
$this->app = new Rake();
$this->initializeKernels();
$this->registerHooks();
}
private function initializeKernels(): void
{
$this->dashboardKernel = new CrawlFlowDashboardKernel($this->app);
$this->migrationKernel = new CrawlFlowMigrationKernel($this->app);
}
private function registerHooks(): void
{
\add_action('plugins_loaded', [$this, 'initializePlugin']);
\add_action('init', [$this, 'initialize']);
\register_activation_hook(__FILE__, [$this, 'activate']);
\register_deactivation_hook(__FILE__, [$this, 'deactivate']);
}
public function initializePlugin(): void
{
$this->dashboardKernel->bootstrap();
$this->migrationKernel->bootstrap();
}
public function activate(): void
{
$this->migrationKernel->runMigrations();
\flush_rewrite_rules();
}
public function deactivate(): void
{
\flush_rewrite_rules();
}
}
// Initialize plugin
WP_CrawlFlow::getInstance();
class CrawlFlowDashboardKernel extends AbstractKernel
{
private DashboardService $dashboardService;
private CrawlFlowController $controller;
private ?string $currentScreen = null;
private array $screenData = [];
public function __construct(Rake $app)
{
parent::__construct($app);
$this->dashboardService = new DashboardService();
$this->controller = new CrawlFlowController($app);
$this->detectCurrentScreen();
$this->loadScreenData();
}
public function getBootstrappers(): array
{
return [
CrawlFlowDashboardBootstrapper::class,
CrawlFlowMigrationBootstrapper::class,
];
}
protected function getConfig(): array
{
return [
'plugin_path' => \plugin_dir_path(__FILE__),
'plugin_url' => \plugin_dir_url(__FILE__),
'version' => '1.0.0',
];
}
private function detectCurrentScreen(): void
{
$screen = \get_current_screen();
$this->currentScreen = $screen ? $screen->id : null;
}
private function loadScreenData(): void
{
if ($this->currentScreen) {
$this->screenData = $this->dashboardService->getScreenData($this->currentScreen);
}
}
public function render(): void
{
$this->controller->renderPage();
}
}
class DashboardService
{
private MigrationService $migrationService;
private ProjectService $projectService;
private LogService $logService;
public function __construct()
{
$this->migrationService = new MigrationService();
$this->projectService = new ProjectService();
$this->logService = new LogService();
}
public function getScreenData(string $screenId): array
{
switch ($screenId) {
case 'toplevel_page_crawlflow':
return $this->getDashboardData();
case 'crawlflow_page_crawlflow-projects':
return $this->getProjectsData();
case 'crawlflow_page_crawlflow-logs':
return $this->getLogsData();
default:
return [];
}
}
private function getDashboardData(): array
{
return [
'projects' => $this->projectService->getRecentProjects(),
'migration_status' => $this->migrationService->getMigrationStatus(),
'system_info' => $this->getSystemInfo(),
'settings' => $this->getSettings(),
'migration_status' => $this->migrationService->getMigrationStatus(),
];
}
private function getProjectsData(): array
{
return [
'projects' => $this->projectService->getProjects(),
'available_tooths' => $this->projectService->getAvailableTooths(),
];
}
private function getLogsData(): array
{
return [
'logs' => $this->logService->getLogs(),
'log_stats' => $this->logService->getLogStats(),
];
}
private function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'wordpress_version' => \get_bloginfo('version'),
'plugin_version' => '1.0.0',
'memory_limit' => \ini_get('memory_limit'),
'max_execution_time' => \ini_get('max_execution_time'),
];
}
private function getSettings(): array
{
return [
'debug_mode' => \defined('WP_DEBUG') && WP_DEBUG,
'log_level' => 'info',
'auto_migration' => true,
];
}
}
class ProjectService
{
private DatabaseAdapterInterface $db;
public function __construct()
{
$this->db = new WordPressDatabaseAdapter();
}
public function getProjects(): array
{
$sql = "SELECT * FROM {$this->db->getPrefix()}projects ORDER BY created_at DESC";
return $this->db->getResults($sql);
}
public function getRecentProjects(int $limit = 5): array
{
$sql = "SELECT * FROM {$this->db->getPrefix()}projects ORDER BY created_at DESC LIMIT {$limit}";
return $this->db->getResults($sql);
}
public function getProject(int $id): ?array
{
$sql = "SELECT * FROM {$this->db->getPrefix()}projects WHERE id = {$id}";
return $this->db->getRow($sql);
}
public function createProject(array $data): int
{
$data['created_at'] = \current_time('mysql');
$data['updated_at'] = \current_time('mysql');
return $this->db->insert('projects', $data);
}
public function updateProject(int $id, array $data): bool
{
$data['updated_at'] = \current_time('mysql');
$affected = $this->db->update('projects', $data, ['id' => $id]);
return $affected > 0;
}
public function deleteProject(int $id): bool
{
$affected = $this->db->delete('projects', ['id' => $id]);
return $affected > 0;
}
public function getAvailableTooths(): array
{
return [
['id' => 'mysql', 'name' => 'MySQL Database'],
['id' => 'postgresql', 'name' => 'PostgreSQL Database'],
['id' => 'mongodb', 'name' => 'MongoDB Database'],
['id' => 'redis', 'name' => 'Redis Cache'],
['id' => 'elasticsearch', 'name' => 'Elasticsearch'],
['id' => 'api', 'name' => 'REST API'],
['id' => 'file', 'name' => 'File System'],
];
}
}
class CrawlFlowController
{
private Rake $app;
private DashboardService $dashboardService;
private ProjectService $projectService;
private LogService $logService;
private MigrationService $migrationService;
private DashboardRenderer $renderer;
public function __construct(Rake $app)
{
$this->app = $app;
$this->dashboardService = new DashboardService();
$this->projectService = new ProjectService();
$this->logService = new LogService();
$this->migrationService = new MigrationService($app);
$this->renderer = new DashboardRenderer();
}
public function registerHooks(): void
{
\add_action('admin_menu', [$this, 'registerMenu']);
\add_action('wp_ajax_crawlflow_save_project', [$this, 'handleSaveProject']);
\add_action('wp_ajax_crawlflow_delete_project', [$this, 'handleDeleteProject']);
\add_action('wp_ajax_crawlflow_auto_save_project', [$this, 'handleAutoSaveProject']);
\add_action('admin_post_crawlflow_run_migration', [$this, 'handleRunMigration']);
\add_action('admin_post_crawlflow_clear_logs', [$this, 'handleClearLogsAction']);
\add_action('admin_post_crawlflow_export_data', [$this, 'handleExportDataAction']);
\add_action('admin_post_crawlflow_save_settings', [$this, 'handleSaveSettings']);
}
public function registerMenu(): void
{
\add_menu_page(
'CrawlFlow',
'CrawlFlow',
'manage_options',
'crawlflow',
[$this, 'renderPage'],
'dashicons-networking',
30
);
\add_submenu_page(
'crawlflow',
'Projects',
'Projects',
'manage_options',
'crawlflow-projects',
[$this, 'renderProjectsPage']
);
\add_submenu_page(
'crawlflow',
'Logs',
'Logs',
'manage_options',
'crawlflow-logs',
[$this, 'renderLogsPage']
);
}
public function renderPage(): void
{
$screen = \get_current_screen();
$this->renderer->renderDashboardOverview($this->dashboardService->getScreenData($screen->id));
}
public function renderProjectsPage(): void
{
$sub = $_GET['sub'] ?? 'list';
if ($sub === 'compose') {
$this->renderer->renderProjectCompose($this->dashboardService->getScreenData('crawlflow_page_crawlflow-projects'));
} else {
$this->renderer->renderProjectsList($this->dashboardService->getScreenData('crawlflow_page_crawlflow-projects'));
}
}
public function handleSaveProject(): void
{
\check_ajax_referer('crawlflow_nonce', 'nonce');
if (!\current_user_can('manage_options')) {
\wp_send_json_error('Insufficient permissions');
}
$projectData = [
'name' => \sanitize_text_field($_POST['name'] ?? ''),
'description' => \sanitize_textarea_field($_POST['description'] ?? ''),
'config' => \sanitize_textarea_field($_POST['config'] ?? ''),
'status' => \sanitize_text_field($_POST['status'] ?? 'active'),
];
try {
if (isset($_POST['id']) && !empty($_POST['id'])) {
$id = (int) $_POST['id'];
$success = $this->projectService->updateProject($id, $projectData);
$message = 'Project updated successfully';
} else {
$id = $this->projectService->createProject($projectData);
$success = $id > 0;
$message = 'Project created successfully';
}
if ($success) {
\wp_send_json_success(['message' => $message, 'id' => $id]);
} else {
\wp_send_json_error('Failed to save project');
}
} catch (Exception $e) {
\wp_send_json_error('Error: ' . $e->getMessage());
}
}
public function handleDeleteProject(): void
{
\check_ajax_referer('crawlflow_nonce', 'nonce');
if (!\current_user_can('manage_options')) {
\wp_send_json_error('Insufficient permissions');
}
$id = (int) ($_POST['id'] ?? 0);
if ($id <= 0) {
\wp_send_json_error('Invalid project ID');
}
try {
$success = $this->projectService->deleteProject($id);
if ($success) {
\wp_send_json_success('Project deleted successfully');
} else {
\wp_send_json_error('Failed to delete project');
}
} catch (Exception $e) {
\wp_send_json_error('Error: ' . $e->getMessage());
}
}
}
class MigrationService
{
private Rake $app;
private DatabaseAdapterInterface $adapter;
private LoggerInterface $logger;
public function __construct(Rake $app)
{
$this->app = $app;
$this->adapter = new WordPressDatabaseAdapter();
$this->logger = \Rake\Facade\Logger::getLogger();
}
public function getMigrationStatus(): array
{
try {
// Kiểm tra migration status từ rake_configs table
$configTable = $this->adapter->getPrefix() . 'rake_configs';
$sql = "SELECT COUNT(*) as count FROM {$configTable} WHERE config_key LIKE 'table_version_%'";
$result = $this->adapter->getVar($sql);
return [
'total_migrations' => (int) $result,
'last_migration' => $this->getLastMigration(),
'status' => 'ready',
];
} catch (Exception $e) {
return [
'total_migrations' => 0,
'last_migration' => null,
'status' => 'error',
'error' => $e->getMessage(),
];
}
}
public function getMigrationHistory(): array
{
try {
// Lấy migration history từ rake_configs table
$configTable = $this->adapter->getPrefix() . 'rake_configs';
$sql = "SELECT * FROM {$configTable} WHERE config_key LIKE 'migration_history_%' ORDER BY updated_at DESC LIMIT 10";
return $this->adapter->getResults($sql);
} catch (Exception $e) {
return [];
}
}
public function runMigrations(): array
{
try {
$this->logger->info('Starting migrations');
$migrationKernel = new CrawlFlowMigrationKernel($this->app);
$migrationKernel->runMigrations();
$this->logger->info('Migrations completed successfully');
return [
'success' => true,
'message' => 'Migrations completed successfully',
];
} catch (Exception $e) {
$this->logger->error('Migration failed: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage(),
];
}
}
private function getLastMigration(): ?array
{
try {
// Lấy migration cuối cùng từ rake_configs table
$configTable = $this->adapter->getPrefix() . 'rake_configs';
$sql = "SELECT * FROM {$configTable} WHERE config_key LIKE 'migration_history_%' ORDER BY updated_at DESC LIMIT 1";
return $this->adapter->getRow($sql);
} catch (Exception $e) {
return null;
}
}
}
class LogService
{
private DatabaseAdapterInterface $db;
public function __construct()
{
$this->db = new WordPressDatabaseAdapter();
}
public function getLogs(int $limit = 100, int $offset = 0, array $filters = []): array
{
$sql = "SELECT * FROM {$this->db->getPrefix()}logs";
$whereConditions = [];
if (!empty($filters['level'])) {
$level = $this->db->escape($filters['level']);
$whereConditions[] = "level = '{$level}'";
}
if (!empty($filters['date_from'])) {
$dateFrom = $this->db->escape($filters['date_from']);
$whereConditions[] = "created_at >= '{$dateFrom}'";
}
if (!empty($filters['date_to'])) {
$dateTo = $this->db->escape($filters['date_to']);
$whereConditions[] = "created_at <= '{$dateTo}'";
}
if (!empty($whereConditions)) {
$sql .= " WHERE " . implode(' AND ', $whereConditions);
}
$sql .= " ORDER BY created_at DESC LIMIT {$limit} OFFSET {$offset}";
return $this->db->getResults($sql);
}
public function getLogStats(): array
{
$sql = "SELECT
level,
COUNT(*) as count,
MIN(created_at) as first_log,
MAX(created_at) as last_log
FROM {$this->db->getPrefix()}logs
GROUP BY level";
$results = $this->db->getResults($sql);
$stats = [];
foreach ($results as $result) {
$stats[$result['level']] = [
'count' => (int) $result['count'],
'first_log' => $result['first_log'],
'last_log' => $result['last_log'],
];
}
return $stats;
}
public function clearLogs(): bool
{
$sql = "DELETE FROM {$this->db->getPrefix()}logs";
return $this->db->query($sql);
}
public function exportLogs(string $format = 'json'): string
{
$logs = $this->getLogs(1000, 0);
switch ($format) {
case 'json':
return json_encode($logs, JSON_PRETTY_PRINT);
case 'csv':
return $this->exportToCsv($logs);
default:
return json_encode($logs);
}
}
private function exportToCsv(array $logs): string
{
if (empty($logs)) {
return '';
}
$output = fopen('php://temp', 'r+');
// Write headers
fputcsv($output, array_keys($logs[0]));
// Write data
foreach ($logs as $log) {
fputcsv($output, $log);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
}
class AssetManager
{
private string $pluginUrl;
private string $pluginPath;
public function __construct()
{
$this->pluginUrl = \plugin_dir_url(__FILE__);
$this->pluginPath = \plugin_dir_path(__FILE__);
}
public function enqueueAdminAssets(): void
{
\add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
\add_action('admin_enqueue_scripts', [$this, 'enqueueAdminStyles']);
}
public function enqueueAdminScripts(): void
{
$screen = \get_current_screen();
if (!$screen || !strpos($screen->id, 'crawlflow')) {
return;
}
// Enqueue jQuery for AJAX
\wp_enqueue_script('jquery');
// Enqueue admin script
\wp_enqueue_script(
'crawlflow-admin',
$this->pluginUrl . 'assets/js/admin.js',
['jquery'],
'1.0.0',
true
);
// Localize script
\wp_localize_script('crawlflow-admin', 'crawlflowAdmin', [
'ajaxUrl' => \admin_url('admin-ajax.php'),
'adminUrl' => \admin_url(),
'nonce' => \wp_create_nonce('crawlflow_nonce'),
'strings' => [
'confirmDelete' => 'Are you sure you want to delete this project?',
'saving' => 'Saving...',
'saved' => 'Saved successfully',
'error' => 'An error occurred',
],
]);
// Enqueue React and XYFlow for composer
if ($screen->id === 'crawlflow_page_crawlflow-projects' && isset($_GET['sub']) && $_GET['sub'] === 'compose') {
$this->enqueueComposerAssets();
}
}
public function enqueueAdminStyles(): void
{
$screen = \get_current_screen();
if (!$screen || !strpos($screen->id, 'crawlflow')) {
return;
}
\wp_enqueue_style(
'crawlflow-admin',
$this->pluginUrl . 'assets/css/admin.css',
[],
'1.0.0'
);
// Enqueue composer styles
if ($screen->id === 'crawlflow_page_crawlflow-projects' && isset($_GET['sub']) && $_GET['sub'] === 'compose') {
\wp_enqueue_style(
'crawlflow-composer',
$this->pluginUrl . 'assets/css/composer.css',
[],
'1.0.0'
);
}
}
private function enqueueComposerAssets(): void
{
// React CDN
\wp_enqueue_script(
'react',
'https://unpkg.com/react@18/umd/react.production.min.js',
[],
'18.0.0',
true
);
\wp_enqueue_script(
'react-dom',
'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js',
['react'],
'18.0.0',
true
);
// XYFlow CDN
\wp_enqueue_script(
'xyflow',
'https://cdn.jsdelivr.net/npm/@xyflow/react@12.8.2/dist/index.umd.js',
['react', 'react-dom'],
'12.8.2',
true
);
// Composer script
\wp_enqueue_script(
'crawlflow-composer',
$this->pluginUrl . 'assets/js/composer-simple.js',
['react', 'react-dom', 'xyflow'],
'1.0.0',
true
);
}
}
// assets/js/composer-simple.js
(function() {
'use strict';
const { useState, useCallback, createElement } = React;
const { ReactFlow, Background, Controls, MiniMap } = window.ReactFlow;
const SCHEMA_DEFINITIONS = {
mysql: {
name: 'MySQL Database',
tables: ['users', 'posts', 'comments', 'categories'],
fields: ['id', 'name', 'email', 'created_at', 'updated_at']
},
postgresql: {
name: 'PostgreSQL Database',
tables: ['users', 'posts', 'comments', 'categories'],
fields: ['id', 'name', 'email', 'created_at', 'updated_at']
},
mongodb: {
name: 'MongoDB Database',
collections: ['users', 'posts', 'comments'],
fields: ['_id', 'name', 'email', 'createdAt', 'updatedAt']
},
redis: {
name: 'Redis Cache',
keys: ['session', 'cache', 'queue'],
dataTypes: ['string', 'hash', 'list', 'set', 'zset']
},
elasticsearch: {
name: 'Elasticsearch',
indices: ['users', 'posts', 'comments'],
fields: ['id', 'title', 'content', 'created_at']
},
api: {
name: 'REST API',
endpoints: ['GET', 'POST', 'PUT', 'DELETE'],
methods: ['users', 'posts', 'comments']
},
file: {
name: 'File System',
directories: ['uploads', 'logs', 'cache'],
fileTypes: ['txt', 'json', 'xml', 'csv']
}
};
function ProjectComposer() {
const [projectName, setProjectName] = useState('');
const [projectDescription, setProjectDescription] = useState('');
const [selectedTooth, setSelectedTooth] = useState('');
const [nodes, setNodes] = useState([]);
const [edges, setEdges] = useState([]);
const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState('');
const generateSampleData = useCallback((toothType) => {
const definition = SCHEMA_DEFINITIONS[toothType];
if (!definition) return [];
const sampleNodes = [];
const sampleEdges = [];
if (definition.tables) {
definition.tables.forEach((table, index) => {
sampleNodes.push({
id: `table-${index}`,
type: 'default',
position: { x: 100 + (index * 200), y: 100 },
data: {
label: table,
type: 'table',
fields: definition.fields
}
});
});
}
if (definition.collections) {
definition.collections.forEach((collection, index) => {
sampleNodes.push({
id: `collection-${index}`,
type: 'default',
position: { x: 100 + (index * 200), y: 100 },
data: {
label: collection,
type: 'collection',
fields: definition.fields
}
});
});
}
setNodes(sampleNodes);
setEdges(sampleEdges);
}, []);
const handleToothChange = useCallback((toothType) => {
setSelectedTooth(toothType);
generateSampleData(toothType);
}, [generateSampleData]);
const handleSaveProject = useCallback(async () => {
if (!projectName.trim()) {
setSaveStatus('Project name is required');
return;
}
setIsSaving(true);
setSaveStatus('Saving...');
try {
const response = await fetch(crawlflowAdmin.ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
action: 'crawlflow_save_project',
nonce: crawlflowAdmin.nonce,
name: projectName,
description: projectDescription,
config: JSON.stringify({
tooth: selectedTooth,
nodes: nodes,
edges: edges
}),
status: 'active'
})
});
const result = await response.json();
if (result.success) {
setSaveStatus('Project saved successfully!');
setTimeout(() => {
window.location.href = crawlflowAdmin.adminUrl + 'admin.php?page=crawlflow-projects';
}, 2000);
} else {
setSaveStatus('Error: ' + (result.data || 'Unknown error'));
}
} catch (error) {
setSaveStatus('Error: ' + error.message);
} finally {
setIsSaving(false);
}
}, [projectName, projectDescription, selectedTooth, nodes, edges]);
return createElement('div', { className: 'crawlflow-project-composer' },
createElement('div', { className: 'crawlflow-project-compose-header' },
createElement('h1', null, 'Create New Project'),
createElement('p', null, 'Design your data flow using the visual composer')
),
createElement('div', { className: 'crawlflow-project-compose-form' },
createElement('div', { className: 'form-group' },
createElement('label', { htmlFor: 'project-name' }, 'Project Name'),
createElement('input', {
type: 'text',
id: 'project-name',
value: projectName,
onChange: (e) => setProjectName(e.target.value),
placeholder: 'Enter project name'
})
),
createElement('div', { className: 'form-group' },
createElement('label', { htmlFor: 'project-description' }, 'Description'),
createElement('textarea', {
id: 'project-description',
value: projectDescription,
onChange: (e) => setProjectDescription(e.target.value),
placeholder: 'Enter project description',
rows: 3
})
),
createElement('div', { className: 'form-group' },
createElement('label', { htmlFor: 'tooth-type' }, 'Data Source Type'),
createElement('select', {
id: 'tooth-type',
value: selectedTooth,
onChange: (e) => handleToothChange(e.target.value)
},
createElement('option', { value: '' }, 'Select data source type'),
Object.entries(SCHEMA_DEFINITIONS).map(([key, def]) =>
createElement('option', { key, value: key }, def.name)
)
)
),
createElement('div', { className: 'form-group' },
createElement('button', {
onClick: handleSaveProject,
disabled: isSaving || !projectName.trim(),
className: 'button button-primary'
}, isSaving ? 'Saving...' : 'Save Project')
),
saveStatus && createElement('div', {
className: 'save-status ' + (saveStatus.includes('Error') ? 'error' : 'success')
}, saveStatus)
),
selectedTooth && createElement('div', { className: 'crawlflow-project-compose-flow' },
createElement(ReactFlow, {
nodes: nodes,
edges: edges,
onNodesChange: (changes) => {
setNodes((nds) => applyNodeChanges(changes, nds));
},
onEdgesChange: (changes) => {
setEdges((eds) => applyEdgeChanges(changes, eds));
},
fitView: true
},
createElement(Background),
createElement(Controls),
createElement(MiniMap)
)
)
);
}
function initComposer() {
const container = document.getElementById('crawlflow-project-composer');
if (container) {
const root = ReactDOM.createRoot(container);
root.render(createElement(ProjectComposer));
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initComposer);
} else {
initComposer();
}
})();
// Always use WordPress functions with backslash prefix
$result = \wp_verify_nonce($nonce, $action);
// Use WordPress security functions
$sanitized = \sanitize_text_field($input);
// Check capabilities before actions
if (\current_user_can('manage_options')) {
// Perform admin action
}
// Use WordPress hooks properly
\add_action('init', [$this, 'initialize']);
class CrawlFlowPluginTest extends TestCase
{
public function testProjectService(): void
{
// Arrange
$service = new ProjectService();
// Act
$projects = $service->getProjects();
// Assert
$this->assertIsArray($projects);
}
public function testMigrationService(): void
{
// Arrange
$app = new Rake();
$service = new MigrationService($app);
// Act
$status = $service->getMigrationStatus();
// Assert
$this->assertIsArray($status);
$this->assertArrayHasKey('total_migrations', $status);
}
}
class CrawlFlowException extends Exception
{
public function __construct(string $message, array $context = [], int $code = 0, ?Throwable $previous = null)
{
parent::__construct("CrawlFlow error: {$message}", $code, $previous);
}
}
// Usage
try {
$projectService = new ProjectService();
$result = $projectService->createProject($data);
} catch (CrawlFlowException $e) {
Logger::error('Project creation failed: ' . $e->getMessage());
}
CrawlFlow Plugin cung cấp hệ thống quản lý projects với visual flow composer:
// Initialize plugin
WP_CrawlFlow::getInstance();
// Use services
$projectService = new ProjectService();
$projects = $projectService->getProjects();
$migrationService = new MigrationService($app);
$status = $migrationService->getMigrationStatus();
Tài liệu này sẽ được cập nhật thường xuyên khi có thay đổi trong plugin.