1193 lines
43 KiB
JavaScript
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=">">></option>
|
|
<option value="<"><</option>
|
|
<option value=">=">>=</option>
|
|
<option value="<="><=</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; |