Qubit_EPM/src/components/ProcessForm.jsx

1193 lines
43 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { FaTable, FaArrowRight, FaTimes, FaPlus, FaFilter, FaCalculator, FaExclamationTriangle } from 'react-icons/fa';
import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon, getTableIcon, CustomProcessIcon } from './CustomIcons';
const ProcessForm = ({ isOpen, onClose, onSave, tables, existingProcess = null }) => {
const [processName, setProcessName] = useState('');
const [processType, setProcessType] = useState('ETL');
const [description, setDescription] = useState('');
const [sourceTables, setSourceTables] = useState([]);
const [destinationTables, setDestinationTables] = useState([]);
const [mappings, setMappings] = useState([]);
const [filters, setFilters] = useState([]);
const [aggregations, setAggregations] = useState([]);
const [processStatus, setProcessStatus] = useState('inactive');
const [activeTab, setActiveTab] = useState('basic');
const [validationError, setValidationError] = useState('');
// Initialize form with existing process data if editing
useEffect(() => {
if (existingProcess) {
setProcessName(existingProcess.name || '');
setProcessType(existingProcess.type || 'ETL');
setDescription(existingProcess.description || '');
setSourceTables(existingProcess.source_table || []);
setDestinationTables(existingProcess.destination_table || []);
setProcessStatus(existingProcess.status || 'inactive');
// Initialize mappings, filters, and aggregations if they exist
if (existingProcess.mappings) setMappings(existingProcess.mappings);
if (existingProcess.filters) setFilters(existingProcess.filters);
if (existingProcess.aggregations) setAggregations(existingProcess.aggregations);
} else {
// Reset form for new process
setProcessName('');
setProcessType('ETL');
setDescription('');
setSourceTables([]);
setDestinationTables([]);
setMappings([]);
setFilters([]);
setAggregations([]);
setProcessStatus('inactive');
}
setValidationError('');
}, [existingProcess, isOpen]);
const handleSave = () => {
// Validate form
if (!processName.trim()) {
setValidationError('Please enter a process name');
return;
}
if (sourceTables.length === 0) {
setValidationError('Please select at least one source table');
return;
}
if (destinationTables.length === 0) {
setValidationError('Please select at least one destination table');
return;
}
// Create process object
const processData = {
name: processName,
type: processType,
description,
source_table: sourceTables,
destination_table: destinationTables,
mappings,
filters,
aggregations,
slug: existingProcess ? existingProcess.slug : `process_${Date.now()}`,
status: processStatus // Use the selected status
};
onSave(processData);
onClose();
};
const handleAddMapping = () => {
setMappings([...mappings, { source: '', target: '', type: 'direct' }]);
};
const handleUpdateMapping = (index, field, value) => {
const updatedMappings = [...mappings];
updatedMappings[index][field] = value;
setMappings(updatedMappings);
};
const handleRemoveMapping = (index) => {
setMappings(mappings.filter((_, i) => i !== index));
};
const handleAddFilter = () => {
setFilters([...filters, { column: '', operator: '=', value: '' }]);
};
const handleUpdateFilter = (index, field, value) => {
const updatedFilters = [...filters];
updatedFilters[index][field] = value;
setFilters(updatedFilters);
};
const handleRemoveFilter = (index) => {
setFilters(filters.filter((_, i) => i !== index));
};
const handleAddAggregation = () => {
setAggregations([...aggregations, { function: 'SUM', column: '', alias: '' }]);
};
const handleUpdateAggregation = (index, field, value) => {
const updatedAggregations = [...aggregations];
updatedAggregations[index][field] = value;
setAggregations(updatedAggregations);
};
const handleRemoveAggregation = (index) => {
setAggregations(aggregations.filter((_, i) => i !== index));
};
// Function to check if a table is a stage table
const isStageTable = (tableId) => {
const table = tables.find(t => t.slug === tableId);
return (table && table.type && table.type.toLowerCase() === 'stage') ||
(table && table.name && table.name.toLowerCase().includes('_stage'));
};
// Function to check if a table is a dimension table
const isDimensionTable = (tableId) => {
const table = tables.find(t => t.slug === tableId);
return (table && table.type && table.type.toLowerCase() === 'dimension') ||
(table && table.name && table.name.toLowerCase().includes('_dim') &&
!table.name.toLowerCase().includes('_stage') &&
!table.name.toLowerCase().includes('_fact'));
};
// Function to check if a table is a fact table
const isFactTable = (tableId) => {
const table = tables.find(t => t.slug === tableId);
return (table && table.type && table.type.toLowerCase() === 'fact') ||
(table && table.name && table.name.toLowerCase().includes('_fact'));
};
// Function to check if a connection is valid based on table types
const isValidConnection = (sourceId, destinationId) => {
// Stage tables can connect to either Dimension or Fact tables
if (isStageTable(sourceId)) {
return isDimensionTable(destinationId) || isFactTable(destinationId);
}
// Fact tables can connect to either Dimension or Fact tables
if (isFactTable(sourceId)) {
return isDimensionTable(destinationId) || isFactTable(destinationId);
}
// Dimension to Fact table connections are not allowed
if (isDimensionTable(sourceId) && isFactTable(destinationId)) {
return false;
}
// All other connections are allowed (including Dimension to Dimension)
return true;
};
// Check if a destination table can be selected based on current source tables
const canSelectDestination = (destinationId) => {
// If no source tables are selected, any destination can be selected
if (sourceTables.length === 0) {
return true;
}
// Check if any of the selected source tables can connect to this destination
return sourceTables.some(sourceId => isValidConnection(sourceId, destinationId));
};
// Check if a source table can be selected based on current destination tables
const canSelectSource = (sourceId) => {
// If no destination tables are selected, any source can be selected
if (destinationTables.length === 0) {
return true;
}
// Check if this source can connect to any of the selected destinations
return destinationTables.some(destinationId => isValidConnection(sourceId, destinationId));
};
const handleTableSelection = (tableId, type) => {
setValidationError('');
if (type === 'source') {
if (sourceTables.includes(tableId)) {
// Removing a source table
setSourceTables(sourceTables.filter(id => id !== tableId));
} else {
// Adding a source table - check if it's compatible with current destinations
if (canSelectSource(tableId)) {
setSourceTables([...sourceTables, tableId]);
} else {
setValidationError('This source table type cannot connect to the selected destination table(s). Dimension tables cannot connect to Fact tables.');
}
}
} else {
if (destinationTables.includes(tableId)) {
// Removing a destination table
setDestinationTables(destinationTables.filter(id => id !== tableId));
} else {
// Adding a destination table - check if it's compatible with current sources
if (canSelectDestination(tableId)) {
setDestinationTables([...destinationTables, tableId]);
} else {
setValidationError('This destination table type cannot be connected from the selected source table(s). Dimension tables cannot connect to Fact tables.');
}
}
}
};
// Get available columns for selected tables
const getAvailableColumns = (tableIds) => {
let columns = [];
tableIds.forEach(tableId => {
const table = tables.find(t => t.slug === tableId);
if (table && table.columns) {
// Ensure table type is properly set based on naming convention
if (table.name && table.name.toLowerCase().includes('_stage')) {
table.type = 'stage';
} else if (table.name && table.name.toLowerCase().includes('_fact')) {
table.type = 'fact';
} else if (table.name && table.name.toLowerCase().includes('_dim')) {
table.type = 'dimension';
} else if (table.type) {
// Normalize table type to lowercase for consistency
table.type = table.type.toLowerCase();
}
columns = [...columns, ...table.columns.map(col => ({
id: `${tableId}.${col}`,
name: `${table.name}.${col}`,
column: col,
table: tableId
}))];
}
});
return columns;
};
const sourceColumns = getAvailableColumns(sourceTables);
const destinationColumns = getAvailableColumns(destinationTables);
// Function to get table type label
const getTableTypeLabel = (tableType) => {
if (!tableType) return '';
switch (tableType.toLowerCase()) {
case 'dimension':
return 'DIM';
case 'fact':
return 'FACT';
case 'stage':
return 'STAGE';
default:
return tableType.toUpperCase();
}
};
// Function to get table type color
const getTableTypeColor = (tableType) => {
if (!tableType) return '#666666';
switch (tableType.toLowerCase()) {
case 'dimension':
return '#52c41a'; // Green
case 'fact':
return '#1890ff'; // Blue
case 'stage':
return '#fa8c16'; // Orange
default:
return '#666666';
}
};
// Function to get the appropriate icon based on table type
const getProcessTableIcon = (tableType) => {
if (!tableType) return <FaTable />;
const type = tableType && typeof tableType === 'string' ? tableType.toLowerCase() : '';
console.log('Table type:', type);
switch (type) {
case 'stage':
return <CustomDatabaseIcon width="16" height="16" />;
case 'fact':
return <CustomDocumentIcon width="16" height="16" />;
case 'dimension':
return <CustomDimensionIcon width="16" height="16" />;
default:
return <FaTable />;
}
};
// Simple component to render the appropriate icon
const TableIcon = ({ type }) => {
if (!type) return <FaTable />;
const tableType = type.toLowerCase();
if (tableType === 'stage') return <CustomDatabaseIcon width="16" height="16" />;
if (tableType === 'fact') return <CustomDocumentIcon width="16" height="16" />;
if (tableType === 'dimension') return <CustomDimensionIcon width="16" height="16" />;
return <FaTable />;
};
if (!isOpen) return null;
return (
<div className="process-form-overlay" style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div className="process-form-container" style={{
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
width: '80%',
maxWidth: '900px',
maxHeight: '90vh',
overflow: 'auto',
padding: '20px',
position: 'relative'
}}>
<button
onClick={onClose}
style={{
position: 'absolute',
top: '15px',
right: '15px',
background: 'transparent',
border: 'none',
fontSize: '20px',
cursor: 'pointer',
color: '#666'
}}
>
<FaTimes />
</button>
<h2 style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
color: '#fa8c16',
marginBottom: '20px'
}}>
<CustomProcessIcon width="24" height="24" /> {existingProcess ? 'Edit Process' : 'Create New Process'}
</h2>
{/* Validation Error Message */}
{validationError && (
<div style={{
padding: '10px 15px',
background: 'rgba(255, 77, 79, 0.1)',
border: '1px solid rgba(255, 77, 79, 0.3)',
borderRadius: '4px',
color: '#ff4d4f',
marginBottom: '15px',
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<FaExclamationTriangle />
{validationError}
</div>
)}
{/* Tabs */}
<div style={{
display: 'flex',
borderBottom: '1px solid #eee',
marginBottom: '20px'
}}>
<button
onClick={() => setActiveTab('basic')}
style={{
padding: '10px 15px',
background: activeTab === 'basic' ? '#fa8c16' : 'transparent',
color: activeTab === 'basic' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'basic' ? '2px solid #fa8c16' : 'none',
cursor: 'pointer',
fontWeight: activeTab === 'basic' ? 'bold' : 'normal'
}}
>
Basic Info
</button>
<button
onClick={() => setActiveTab('mappings')}
style={{
padding: '10px 15px',
background: activeTab === 'mappings' ? '#fa8c16' : 'transparent',
color: activeTab === 'mappings' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'mappings' ? '2px solid #fa8c16' : 'none',
cursor: 'pointer',
fontWeight: activeTab === 'mappings' ? 'bold' : 'normal'
}}
>
Column Mappings
</button>
<button
onClick={() => setActiveTab('filters')}
style={{
padding: '10px 15px',
background: activeTab === 'filters' ? '#fa8c16' : 'transparent',
color: activeTab === 'filters' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'filters' ? '2px solid #fa8c16' : 'none',
cursor: 'pointer',
fontWeight: activeTab === 'filters' ? 'bold' : 'normal'
}}
>
Filters
</button>
<button
onClick={() => setActiveTab('aggregations')}
style={{
padding: '10px 15px',
background: activeTab === 'aggregations' ? '#fa8c16' : 'transparent',
color: activeTab === 'aggregations' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'aggregations' ? '2px solid #fa8c16' : 'none',
cursor: 'pointer',
fontWeight: activeTab === 'aggregations' ? 'bold' : 'normal'
}}
>
Aggregations
</button>
</div>
{/* Basic Info Tab */}
{activeTab === 'basic' && (
<div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Process Name:
</label>
<input
type="text"
value={processName}
onChange={(e) => setProcessName(e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
placeholder="Enter process name"
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Process Type:
</label>
<select
value={processType}
onChange={(e) => setProcessType(e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="Extract">Extract</option>
<option value="Transform">Transform</option>
<option value="Load">Load</option>
<option value="Validation">Validation</option>
{/* <option value="Aggregation">Aggregation</option> */}
</select>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Process Status:
</label>
<select
value={processStatus}
onChange={(e) => setProcessStatus(e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
background: processStatus === 'active' ? 'rgba(82, 196, 26, 0.1)' : 'rgba(255, 77, 79, 0.1)',
color: processStatus === 'active' ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold'
}}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<small style={{
display: 'block',
marginTop: '5px',
color: '#666',
fontSize: '12px'
}}>
{processStatus === 'active'
? 'Active processes will show animated connections in the flow diagram.'
: 'Inactive processes will show gray, non-animated connections in the flow diagram.'}
</small>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Description:
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px',
minHeight: '80px'
}}
placeholder="Describe what this process does"
/>
</div>
<div style={{ marginBottom: '15px' }}>
<div style={{
padding: '10px 15px',
background: 'rgba(24, 144, 255, 0.1)',
border: '1px solid rgba(24, 144, 255, 0.3)',
borderRadius: '4px',
color: '#1890ff',
marginBottom: '15px',
fontSize: '13px'
}}>
<strong>Connection Rules:</strong>
<ul style={{ margin: '5px 0 0 0', paddingLeft: '20px' }}>
<li>Stage tables can connect to either Dimension or Fact tables</li>
<li>Fact tables can connect to either Dimension or Fact tables</li>
<li>Dimension to Dimension table connections are allowed</li>
<li>Dimension to Fact table connections are not allowed</li>
</ul>
<div style={{
marginTop: '10px',
padding: '8px',
background: 'rgba(250, 140, 22, 0.1)',
border: '1px solid rgba(250, 140, 22, 0.3)',
borderRadius: '4px'
}}>
<strong style={{ color: '#fa8c16' }}>Stage Tables:</strong>
<p style={{ margin: '5px 0 0 0', color: '#666' }}>
Stage tables (<span style={{
background: '#fa8c16',
color: 'white',
padding: '1px 4px',
borderRadius: '3px',
fontSize: '10px'
}}>STAGE</span>) serve as the master tables for loading metadata and data.
They include error-handling functionality and support subset operations.
</p>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' , color: 'black'}}>
Source Tables:
</label>
<div
style={{
border: '1px solid #ddd',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto',
padding: '10px',
}}
>
{tables.map((table) => {
const isDisabled = destinationTables.length > 0 && !canSelectSource(table.slug);
// Debug: Ensure table type is properly set and normalized
if (table.name && table.name.toLowerCase().includes('_stage')) {
table.type = 'stage';
} else if (table.name && table.name.toLowerCase().includes('_fact')) {
table.type = 'fact';
} else if (table.name && table.name.toLowerCase().includes('_dim')) {
table.type = 'dimension';
}
return (
<div
key={table.slug}
style={{
display: 'flex',
alignItems: 'center',
padding: '5px',
borderBottom: '1px solid #f0f0f0',
opacity: isDisabled ? 0.5 : 1,
}}
>
<input
type="checkbox"
id={`source-${table.slug}`}
checked={sourceTables.includes(table.slug)}
onChange={() => handleTableSelection(table.slug, 'source')}
style={{ marginRight: '8px' }}
disabled={isDisabled}
/>
<label
htmlFor={`source-${table.slug}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
cursor: isDisabled ? 'not-allowed' : 'pointer',
}}
>
<TableIcon type={table.type} />
{table.name}
<span
style={{
fontSize: '10px',
background: getTableTypeColor(table.type),
color: 'white',
padding: '1px 4px',
borderRadius: '3px',
marginLeft: '5px',
}}
>
{getTableTypeLabel(table.type)}
</span>
</label>
</div>
);
})}
</div>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: 'black'}}>
Destination Tables:
</label>
<div
style={{
border: '1px solid #ddd',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto',
padding: '10px',
}}
>
{tables.map((table) => {
const isDisabled = sourceTables.length > 0 && !canSelectDestination(table.slug);
// Debug: Ensure table type is properly set and normalized
if (table.name && table.name.toLowerCase().includes('_stage')) {
table.type = 'stage';
} else if (table.name && table.name.toLowerCase().includes('_fact')) {
table.type = 'fact';
} else if (table.name && table.name.toLowerCase().includes('_dim')) {
table.type = 'dimension';
}
return (
<div
key={table.slug}
style={{
display: 'flex',
alignItems: 'center',
padding: '5px',
borderBottom: '1px solid #f0f0f0',
opacity: isDisabled ? 0.5 : 1,
}}
>
<input
type="checkbox"
id={`dest-${table.slug}`}
checked={destinationTables.includes(table.slug)}
onChange={() => handleTableSelection(table.slug, 'destination')}
style={{ marginRight: '8px' }}
disabled={isDisabled}
/>
<label
htmlFor={`dest-${table.slug}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
cursor: isDisabled ? 'not-allowed' : 'pointer',
}}
>
<TableIcon type={table.type} />
{table.name}
<span
style={{
fontSize: '10px',
background: getTableTypeColor(table.type),
color: 'white',
padding: '1px 4px',
borderRadius: '3px',
marginLeft: '5px',
}}
>
{getTableTypeLabel(table.type)}
</span>
</label>
</div>
);
})}
</div>
</div>
</div>
</div>
)}
{/* Column Mappings Tab */}
{activeTab === 'mappings' && (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '15px'
}}>
<h3 style={{ margin: 0 }}>Column Mappings</h3>
<button
onClick={handleAddMapping}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
padding: '5px 10px',
background: '#fa8c16',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
<FaPlus size={12} /> Add Mapping
</button>
</div>
{mappings.length === 0 ? (
<div style={{
padding: '20px',
textAlign: 'center',
background: '#f9f9f9',
borderRadius: '4px',
color: '#666'
}}>
No column mappings defined. Click "Add Mapping" to create one.
</div>
) : (
<div>
{mappings.map((mapping, index) => (
<div key={index} style={{
display: 'flex',
gap: '10px',
alignItems: 'center',
marginBottom: '10px',
padding: '10px',
background: '#f9f9f9',
borderRadius: '4px'
}}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Source Column
</label>
<select
value={mapping.source}
onChange={(e) => handleUpdateMapping(index, 'source', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="">Select Source Column</option>
{sourceColumns.map(col => (
<option key={col.id} value={col.id}>{col.name}</option>
))}
</select>
</div>
<div style={{ display: 'flex', alignItems: 'center', padding: '0 10px' }}>
<FaArrowRight color="#fa8c16" />
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Target Column
</label>
<select
value={mapping.target}
onChange={(e) => handleUpdateMapping(index, 'target', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="">Select Target Column</option>
{destinationColumns.map(col => (
<option key={col.id} value={col.id}>{col.name}</option>
))}
</select>
</div>
<div style={{ flex: 0.5 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Mapping Type
</label>
<select
value={mapping.type}
onChange={(e) => handleUpdateMapping(index, 'type', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="direct">Direct</option>
<option value="transform">Transform</option>
<option value="lookup">Lookup</option>
</select>
</div>
<button
onClick={() => handleRemoveMapping(index)}
style={{
background: 'transparent',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
marginTop: '20px'
}}
>
<FaTimes />
</button>
</div>
))}
</div>
)}
</div>
)}
{/* Filters Tab */}
{activeTab === 'filters' && (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '15px'
}}>
<h3 style={{ margin: 0 }}>Filters</h3>
<button
onClick={handleAddFilter}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
padding: '5px 10px',
background: '#fa8c16',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
<FaPlus size={12} /> Add Filter
</button>
</div>
{filters.length === 0 ? (
<div style={{
padding: '20px',
textAlign: 'center',
background: '#f9f9f9',
borderRadius: '4px',
color: '#666'
}}>
No filters defined. Click "Add Filter" to create one.
</div>
) : (
<div>
{filters.map((filter, index) => (
<div key={index} style={{
display: 'flex',
gap: '10px',
alignItems: 'center',
marginBottom: '10px',
padding: '10px',
background: '#f9f9f9',
borderRadius: '4px'
}}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Column
</label>
<select
value={filter.column}
onChange={(e) => handleUpdateFilter(index, 'column', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="">Select Column</option>
{sourceColumns.map(col => (
<option key={col.id} value={col.id}>{col.name}</option>
))}
</select>
</div>
<div style={{ width: '120px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Operator
</label>
<select
value={filter.operator}
onChange={(e) => handleUpdateFilter(index, 'operator', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="=">=</option>
<option value="!=">!=</option>
<option value=">">&gt;</option>
<option value="<">&lt;</option>
<option value=">=">&gt;=</option>
<option value="<=">&lt;=</option>
<option value="LIKE">LIKE</option>
<option value="IN">IN</option>
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Value
</label>
<input
type="text"
value={filter.value}
onChange={(e) => handleUpdateFilter(index, 'value', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
placeholder="Enter filter value"
/>
</div>
<button
onClick={() => handleRemoveFilter(index)}
style={{
background: 'transparent',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
marginTop: '20px'
}}
>
<FaTimes />
</button>
</div>
))}
</div>
)}
</div>
)}
{/* Aggregations Tab */}
{activeTab === 'aggregations' && (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '15px'
}}>
<h3 style={{ margin: 0 }}>Aggregations</h3>
<button
onClick={handleAddAggregation}
style={{
display: 'flex',
alignItems: 'center',
gap: '5px',
padding: '5px 10px',
background: '#fa8c16',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
<FaPlus size={12} /> Add Aggregation
</button>
</div>
{aggregations.length === 0 ? (
<div style={{
padding: '20px',
textAlign: 'center',
background: '#f9f9f9',
borderRadius: '4px',
color: '#666'
}}>
No aggregations defined. Click "Add Aggregation" to create one.
</div>
) : (
<div>
{aggregations.map((agg, index) => (
<div key={index} style={{
display: 'flex',
gap: '10px',
alignItems: 'center',
marginBottom: '10px',
padding: '10px',
background: '#f9f9f9',
borderRadius: '4px'
}}>
<div style={{ width: '120px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Function
</label>
<select
value={agg.function}
onChange={(e) => handleUpdateAggregation(index, 'function', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="SUM">SUM</option>
<option value="AVG">AVG</option>
<option value="MIN">MIN</option>
<option value="MAX">MAX</option>
<option value="COUNT">COUNT</option>
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Column
</label>
<select
value={agg.column}
onChange={(e) => handleUpdateAggregation(index, 'column', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
>
<option value="">Select Column</option>
{sourceColumns.map(col => (
<option key={col.id} value={col.id}>{col.name}</option>
))}
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', fontSize: '12px' }}>
Alias
</label>
<input
type="text"
value={agg.alias}
onChange={(e) => handleUpdateAggregation(index, 'alias', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: '1px solid #ddd',
borderRadius: '4px'
}}
placeholder="Result column name"
/>
</div>
<button
onClick={() => handleRemoveAggregation(index)}
style={{
background: 'transparent',
border: 'none',
color: '#ff4d4f',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '30px',
height: '30px',
marginTop: '20px'
}}
>
<FaTimes />
</button>
</div>
))}
</div>
)}
</div>
)}
<div style={{
marginTop: '20px',
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
borderTop: '1px solid #eee',
paddingTop: '20px'
}}>
<button
onClick={onClose}
style={{
padding: '8px 16px',
background: '#f0f0f0',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
onClick={handleSave}
style={{
padding: '8px 16px',
background: '#fa8c16',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '5px'
}}
>
<CustomProcessIcon width="16" height="16" /> {existingProcess ? 'Update Process' : 'Create Process'}
</button>
</div>
</div>
</div>
);
};
export default ProcessForm;