import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Panel,
useReactFlow,
ReactFlowProvider,
Handle,
Position,
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
MarkerType
} from 'reactflow';
import 'reactflow/dist/style.css';
import axios from 'axios';
// Import icons from react-icons
import {
FaDatabase,
FaTable,
FaPlus,
FaTimes,
FaKey,
FaLink,
FaLayerGroup,
FaColumns,
FaProjectDiagram,
FaChevronDown,
FaChevronRight,
FaServer,
FaCloud,
FaHdd
} from 'react-icons/fa';
import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon } from './CustomIcons';
import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import AddTableModal from './AddTableModal';
// Custom styles for ER Diagram
const generateERStyles = () => {
return `
.react-flow__node {
z-index: 1;
margin: 20px;
}
.er-database-wrapper {
z-index: -2;
pointer-events: all;
border-radius: 25px;
cursor: grab;
user-select: none;
transition: all 0.2s ease;
padding: 100px;
border: 3px solid rgba(0, 169, 157, 0.4);
background: rgba(0, 169, 157, 0.03);
box-shadow: 0 0 30px rgba(0, 169, 157, 0.1);
}
.er-database-wrapper:hover {
border-color: rgba(0, 169, 157, 0.6);
background: rgba(0, 169, 157, 0.05);
box-shadow: 0 0 40px rgba(0, 169, 157, 0.15);
}
.er-database-wrapper:active {
cursor: grabbing;
}
.er-database-label {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 169, 157, 0.95);
color: white;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0, 169, 157, 0.3);
display: flex;
align-items: center;
gap: 10px;
z-index: 15;
}
.er-schema-group {
z-index: -1;
pointer-events: all;
border-radius: 20px;
cursor: grab;
user-select: none;
transition: all 0.2s ease;
padding: 80px;
border: 2px dashed rgba(138, 43, 226, 0.3);
background: rgba(138, 43, 226, 0.05);
position: relative;
overflow: visible;
}
.er-schema-group:hover {
border-color: rgba(138, 43, 226, 0.5);
background: rgba(138, 43, 226, 0.08);
box-shadow: 0 0 20px rgba(138, 43, 226, 0.1);
}
.er-schema-group:active {
cursor: grabbing;
}
.er-schema-label {
position: absolute;
top: 15px;
left: 15px;
background: rgba(138, 43, 226, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(138, 43, 226, 0.3);
display: flex;
align-items: center;
gap: 8px;
z-index: 10;
}
.er-table-node {
background: white;
border: 2px solid #e1e5e9;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 260px;
min-height: 200px;
transition: all 0.2s ease;
}
.er-table-node:hover {
border-color: #8a2be2;
box-shadow: 0 4px 16px rgba(138, 43, 226, 0.2);
}
.er-table-header {
background: linear-gradient(135deg, #8a2be2, #9932cc);
color: white;
padding: 14px 16px;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
gap: 10px;
font-weight: bold;
font-size: 15px;
}
.er-table-header.stage {
background: linear-gradient(135deg, #00a99d, #52c41a);
}
.er-table-header.fact {
background: linear-gradient(135deg, #fa8c16, #faad14);
}
.er-table-header.dimension {
background: linear-gradient(135deg, #52c41a, #73d13d);
}
.er-column-list {
padding: 0;
margin: 0;
list-style: none;
max-height: 300px;
overflow-y: auto;
}
.er-column-item {
padding: 10px 16px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
transition: background-color 0.2s ease;
}
.er-column-item:hover {
background-color: #f8f9fa;
}
.er-column-item:last-child {
border-bottom: none;
}
.er-column-name {
flex: 1;
font-weight: 500;
color: #333;
}
.er-column-type {
color: #666;
font-size: 11px;
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
}
.er-primary-key {
color: #faad14;
}
.er-foreign-key {
color: #8a2be2;
}
.er-relationship-edge {
stroke: #8a2be2;
stroke-width: 2;
}
.er-relationship-edge .react-flow__edge-path {
stroke: #8a2be2;
stroke-width: 2;
}
.er-relationship-edge-animated {
stroke: #8a2be2;
stroke-width: 2;
stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite;
}
@keyframes dashdraw {
to {
stroke-dashoffset: -10;
}
}
/* Enhanced arrow markers */
.react-flow__edge.selected .react-flow__edge-path {
stroke: #faad14 !important;
stroke-width: 3 !important;
}
.react-flow__edge:hover .react-flow__edge-path {
stroke: #52c41a !important;
stroke-width: 3 !important;
}
/* Custom arrow styling */
.er-custom-arrow {
pointer-events: none;
z-index: 10;
}
/* Cardinality indicators */
.er-cardinality-indicator {
background: rgba(255, 255, 255, 0.95) !important;
border: 1px solid #8a2be2 !important;
border-radius: 3px !important;
padding: 2px 6px !important;
font-size: 10px !important;
font-weight: bold !important;
color: #8a2be2 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
}
/* Relationship direction indicators */
.er-direction-arrow {
fill: #8a2be2;
stroke: #8a2be2;
stroke-width: 1;
}
.er-direction-arrow.selected {
fill: #faad14;
stroke: #faad14;
}
.er-relationship-label {
background: rgba(138, 43, 226, 0.9);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
}
.er-relationship-label:hover + .relationship-tooltip {
opacity: 1 !important;
}
.er-add-button {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #8a2be2, #9932cc);
border: none;
color: white;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 16px rgba(138, 43, 226, 0.3);
transition: all 0.2s ease;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.er-add-button:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(138, 43, 226, 0.4);
}
.er-add-button:active {
transform: scale(0.95);
}
.er-breadcrumb-dropdown {
display: inline-flex;
align-items: center;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.er-breadcrumb-dropdown:hover {
background-color: rgba(0, 169, 157, 0.1);
}
.er-breadcrumb-dropdown .dropdown-arrow {
margin-left: 4px;
font-size: 12px;
transition: transform 0.2s ease;
}
.er-breadcrumb-dropdown.open .dropdown-arrow {
transform: rotate(180deg);
}
.er-service-menu {
background: rgba(26, 26, 26, 0.95) !important;
border: 1px solid rgba(0, 169, 157, 0.3) !important;
border-radius: 8px !important;
backdrop-filter: blur(10px) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
}
.er-service-menu .MuiMenuItem-root {
color: #ffffff !important;
padding: 12px 16px !important;
border-radius: 4px !important;
margin: 4px 8px !important;
transition: all 0.2s ease !important;
}
.er-service-menu .MuiMenuItem-root:hover {
background-color: rgba(0, 169, 157, 0.1) !important;
}
.er-service-menu .MuiListItemIcon-root {
color: #00a99d !important;
min-width: 32px !important;
}
/* Ensure ReactFlow markers are visible */
.react-flow__edges {
z-index: 1;
}
.react-flow__edge {
pointer-events: all;
}
.react-flow__edge path {
stroke: #8a2be2;
stroke-width: 2;
}
.react-flow__edge.selected path {
stroke: #faad14 !important;
stroke-width: 3 !important;
}
/* Custom arrows are always visible */
.er-custom-arrow {
pointer-events: none;
z-index: 10;
}
`;
};
// Custom Table Node Component for ER Diagram
const ERTableNode = ({ data, id }) => {
const getTableIcon = () => {
switch(data.table_type) {
case 'stage':
return ;
case 'fact':
return ;
case 'dimension':
return ;
default:
return ;
}
};
return (
{getTableIcon()}
{data.name}
{data.columns && data.columns.map((column, index) => (
-
{column.is_primary_key && }
{column.is_foreign_key && }
{column.name}
{column.data_type}
))}
);
};
// Custom Edge Component for Relationships
const ERRelationshipEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
selected
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
// Determine relationship type and styling
const relationshipType = data?.relationship_type || '1:N';
const isOneToMany = relationshipType === '1:N';
const isOneToOne = relationshipType === '1:1';
const isManyToMany = relationshipType === 'N:M';
// Calculate arrow position and angle
const dx = targetX - sourceX;
const dy = targetY - sourceY;
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
// Arrow position (closer to target)
const arrowDistance = 20;
const arrowX = targetX - (arrowDistance * Math.cos(Math.atan2(dy, dx)));
const arrowY = targetY - (arrowDistance * Math.sin(Math.atan2(dy, dx)));
// Start arrow position for many-to-many
const startArrowX = sourceX + (arrowDistance * Math.cos(Math.atan2(dy, dx)));
const startArrowY = sourceY + (arrowDistance * Math.sin(Math.atan2(dy, dx)));
return (
<>
{/* Main edge path */}
{/* Custom Arrow at target */}
{/* Custom Arrow at source for many-to-many */}
{isManyToMany && (
)}
{/* Relationship cardinality indicators - positioned directly on the line */}
{/* Source cardinality (near source table) - positioned on the line */}
{isOneToMany || isOneToOne ? '1' : 'N'}
{/* Target cardinality (near target table) - positioned on the line */}
{isOneToOne ? '1' : 'N'}
{/* Main relationship label - centered on the line */}
{relationshipType}
{/* Column mapping tooltip - shown on hover */}
{data?.source_column && data?.target_column && (
{data.source_column} → {data.target_column}
)}
>
);
};
// Database Wrapper Node Component
const ERDatabaseWrapperNode = ({ data, id }) => {
// Auto-calculate dimensions based on schema count and content
const schemaCount = data.schemaCount || 0;
const totalTables = data.totalTables || 0;
// Dynamic sizing for database wrapper - much larger to contain all schemas
const autoWidth = Math.max(1400, data.contentWidth + 200); // Base width + content + padding
const autoHeight = Math.max(1000, data.contentHeight + 200); // Base height + content + padding
return (
{data.name}
({schemaCount} schemas • {totalTables} tables)
);
};
// Schema Group Node Component with Auto-sizing
const ERSchemaGroupNode = ({ data, id }) => {
// Auto-calculate dimensions based on table count and content
const tableCount = data.tableCount || 0;
const tablesPerRow = Math.ceil(Math.sqrt(tableCount));
const rows = Math.ceil(tableCount / tablesPerRow);
// Dynamic sizing based on content
const autoWidth = Math.max(800, tablesPerRow * 320 + 160); // Base width + table width + padding
const autoHeight = Math.max(600, rows * 280 + 200); // Base height + table height + padding
return (
{data.name}
({tableCount} tables)
{tableCount === 0 && (
No tables in this schema
)}
);
};
// Node types for ER Diagram
const nodeTypes = {
erTable: ERTableNode,
erSchemaGroup: ERSchemaGroupNode,
erDatabaseWrapper: ERDatabaseWrapperNode,
};
// Edge types for ER Diagram
const edgeTypes = {
erRelationship: ERRelationshipEdge,
};
// Mock data for services and data sources
const mockServices = [
{
id: 'plan-plus',
name: 'Plan Plus',
icon: ,
dataSources: [
{ id: 'postgres-main', name: 'PostgreSQL Main', type: 'PostgreSQL', icon: },
{ id: 'mysql-analytics', name: 'MySQL Analytics', type: 'MySQL', icon: },
{ id: 'mongodb-logs', name: 'MongoDB Logs', type: 'MongoDB', icon: }
]
},
{
id: 'project-1',
name: 'Project 1',
icon: ,
dataSources: [
{ id: 'snowflake-dw', name: 'Snowflake DW', type: 'Snowflake', icon: },
{ id: 'redshift-analytics', name: 'Redshift Analytics', type: 'Redshift', icon: }
]
},
{
id: 'project-2',
name: 'Project 2',
icon: ,
dataSources: [
{ id: 'bigquery-main', name: 'BigQuery Main', type: 'BigQuery', icon: },
{ id: 'postgres-backup', name: 'PostgreSQL Backup', type: 'PostgreSQL', icon: }
]
}
];
// Hierarchical Breadcrumb Component
const HierarchicalBreadcrumb = ({
selectedService,
selectedDataSource,
onServiceChange,
onDataSourceChange
}) => {
const [serviceMenuAnchor, setServiceMenuAnchor] = useState(null);
const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null);
const handleServiceClick = (event) => {
setServiceMenuAnchor(event.currentTarget);
};
const handleDataSourceClick = (event) => {
setDataSourceMenuAnchor(event.currentTarget);
};
const handleServiceSelect = (service) => {
onServiceChange(service);
setServiceMenuAnchor(null);
// Reset data source when service changes
if (service.dataSources.length > 0) {
onDataSourceChange(service.dataSources[0]);
}
};
const handleDataSourceSelect = (dataSource) => {
onDataSourceChange(dataSource);
setDataSourceMenuAnchor(null);
};
const handleCloseMenus = () => {
setServiceMenuAnchor(null);
setDataSourceMenuAnchor(null);
};
return (
<>
e.preventDefault()}
sx={{ fontWeight: 500 }}
>
Qubit
ER Diagram
{/* DBTEZ Services Dropdown */}
DBTEZ Services
{/* Selected Service */}
{selectedService && (
{selectedService.icon}
{selectedService.name}
)}
{/* Services Menu */}
{/* Data Sources Menu */}
{selectedService && (
)}
>
);
};
// Main ER Diagram Canvas Component
const ERDiagramCanvasContent = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [isLoading, setIsLoading] = useState(true);
const [databases, setDatabases] = useState([]);
const [selectedDatabase, setSelectedDatabase] = useState(null);
const [showAddMenu, setShowAddMenu] = useState(false);
// ReactFlow instance for programmatic control
const { fitView } = useReactFlow();
// Service and Data Source selection state
const [selectedService, setSelectedService] = useState(mockServices[0]); // Default to Plan Plus
const [selectedDataSource, setSelectedDataSource] = useState(mockServices[0].dataSources[0]); // Default to first data source
const [isUsingMockData, setIsUsingMockData] = useState(false);
const [apiError, setApiError] = useState(null);
const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null);
// Add Table Modal state
const [isAddTableModalOpen, setIsAddTableModalOpen] = useState(false);
const [availableSchemas, setAvailableSchemas] = useState([]);
const [existingTables, setExistingTables] = useState([]);
// API Configuration
const API_BASE_URL = 'https://sandbox.kezel.io/api';
const token = "abdhsg";
const orgSlug = "sN05Pjv11qvH";
// API endpoints
const ENDPOINTS = {
DATABASE_LIST: `${API_BASE_URL}/qbt_database_list_get`,
SCHEMA_LIST: `${API_BASE_URL}/qbt_schema_list_get`,
TABLE_LIST: `${API_BASE_URL}/qbt_table_list_get`,
COLUMN_LIST: `${API_BASE_URL}/qbt_column_list_get`,
SCHEMA_CREATE: `${API_BASE_URL}/qbt_schema_create`,
SCHEMA_DELETE: `${API_BASE_URL}/qbt_schema_delete`,
SCHEMA_UPDATE: `${API_BASE_URL}/qbt_schema_update`,
TABLE_CREATE: `${API_BASE_URL}/qbt_table_create`,
TABLE_UPDATE: `${API_BASE_URL}/qbt_table_update`,
TABLE_DELETE: `${API_BASE_URL}/qbt_table_delete`,
COLUMN_CREATE: `${API_BASE_URL}/qbt_column_create`,
COLUMN_UPDATE: `${API_BASE_URL}/qbt_column_update`,
COLUMN_DELETE: `${API_BASE_URL}/qbt_column_delete`
};
// Mock database data representing ONE database with multiple schemas (fallback only)
// const mockDatabaseData = {
// id: 1,
// name: "Sample Database",
// slug: "sample_database",
// description: "Sample Database Structure (Mock Data)",
// service: selectedService?.name || "Unknown Service",
// dataSource: selectedDataSource?.name || "Unknown DataSource",
// schemas: [
// {
// sch: "FINANCE_MART",
// tables: [
// {
// id: 1,
// name: "accounts",
// table_type: "dimension",
// columns: [
// { name: "account_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "account_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false },
// { name: "account_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "account_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "balance", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false },
// { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 2,
// name: "transactions",
// table_type: "fact",
// columns: [
// { name: "transaction_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "transaction_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false },
// { name: "amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false },
// { name: "transaction_type", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false },
// { name: "description", data_type: "VARCHAR(255)", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 3,
// name: "customers",
// table_type: "dimension",
// columns: [
// { name: "customer_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "first_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "last_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "phone", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false },
// { name: "registration_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 4,
// name: "financial_reports",
// table_type: "fact",
// columns: [
// { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "report_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false },
// { name: "balance_amount", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false },
// { name: "profit_loss", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false }
// ]
// }
// ]
// },
// {
// sch: "Schema100",
// tables: [
// {
// id: 5,
// name: "products",
// table_type: "dimension",
// columns: [
// { name: "product_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "product_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "category", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "price", data_type: "DECIMAL(10,2)", is_primary_key: false, is_foreign_key: false },
// { name: "stock_quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 6,
// name: "orders",
// table_type: "fact",
// columns: [
// { name: "order_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "order_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false },
// { name: "quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false },
// { name: "total_amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 7,
// name: "inventory",
// table_type: "stage",
// columns: [
// { name: "inventory_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "warehouse_location", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "stock_level", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false },
// { name: "last_updated", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 8,
// name: "suppliers",
// table_type: "dimension",
// columns: [
// { name: "supplier_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "supplier_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "contact_email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "phone_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }
// ]
// }
// ]
// },
// {
// sch: "New_sch",
// tables: [
// {
// id: 9,
// name: "analytics_summary",
// table_type: "fact",
// columns: [
// { name: "summary_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "date", data_type: "DATE", is_primary_key: false, is_foreign_key: false },
// { name: "total_revenue", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false },
// { name: "total_orders", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false },
// { name: "unique_customers", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 10,
// name: "user_sessions",
// table_type: "stage",
// columns: [
// { name: "session_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true },
// { name: "session_start", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false },
// { name: "session_end", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false },
// { name: "pages_viewed", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false },
// { name: "device_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }
// ]
// },
// {
// id: 11,
// name: "reports",
// table_type: "dimension",
// columns: [
// { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false },
// { name: "report_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false },
// { name: "report_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "created_by", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false },
// { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }
// ]
// }
// ]
// }
// ]
// };
// API Functions for fetching real data
const fetchDatabases = async () => {
try {
const response = await axios.post(
ENDPOINTS.DATABASE_LIST,
{
token: token,
org: orgSlug,
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log('Database list response:', response.data);
const databases = response.data.items || [];
console.log(`Found ${databases.length} databases:`, databases);
return databases.filter(db => db.con); // Filter out databases without connection slug
} catch (error) {
console.error('Error fetching databases:', error);
throw error;
}
};
const fetchSchemas = async (dbSlug) => {
try {
console.log(`Fetching schemas for database slug: ${dbSlug}`);
const response = await axios.post(
ENDPOINTS.SCHEMA_LIST,
{
token: token,
org: orgSlug,
con: dbSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Schema list for database ${dbSlug}:`, response.data);
let schemas = [];
if (Array.isArray(response.data.items)) {
schemas = response.data.items.map(item => ({
name: item.name,
sch: item.sch, // Use 'sch' as the slug
description: item.description || "",
created_at: item.created_at,
is_validated: item.is_validated,
database: dbSlug
}));
}
console.log(`Number of schemas found for database ${dbSlug}: ${schemas.length}`);
console.log('Schema details:', schemas.map(s => ({ name: s.name, sch: s.sch })));
return schemas;
} catch (error) {
console.error(`Error fetching schemas for database ${dbSlug}:`, error);
throw error;
}
};
const fetchTables = async (dbSlug, schemaSlug) => {
try {
console.log(`Fetching tables for database: ${dbSlug}, schema: ${schemaSlug}`);
const response = await axios.post(
ENDPOINTS.TABLE_LIST,
{
token: token,
org: orgSlug,
con: dbSlug,
sch: schemaSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Table list for schema ${schemaSlug}:`, response.data);
let tables = [];
if (Array.isArray(response.data.items)) {
tables = response.data.items.map(item => ({
id: item.id || `${dbSlug}-${schemaSlug}-${item.name}`,
name: item.name,
tbl: item.tbl, // Table slug
description: item.description || "",
table_type: item.table_type || "dimension", // Default to dimension if not specified
created_at: item.created_at,
database: dbSlug,
schema: schemaSlug
}));
}
console.log(`Number of tables found for schema ${schemaSlug}: ${tables.length}`);
return tables;
} catch (error) {
console.error(`Error fetching tables for schema ${schemaSlug}:`, error);
throw error;
}
};
const fetchColumns = async (dbSlug, schemaSlug, tableSlug) => {
try {
console.log(`Fetching columns for database: ${dbSlug}, schema: ${schemaSlug}, table: ${tableSlug}`);
const response = await axios.post(
ENDPOINTS.COLUMN_LIST,
{
token: token,
org: orgSlug,
con: dbSlug,
sch: schemaSlug,
tbl: tableSlug
},
{
headers: {
'Content-Type': 'application/json'
}
}
);
console.log(`Column list for table ${tableSlug}:`, response.data);
let columns = [];
if (Array.isArray(response.data.items)) {
columns = response.data.items.map((item, index) => ({
name: item.name,
col: item.col, // Column slug
data_type: item.data_type || "VARCHAR(255)",
description: item.description || "",
is_primary_key: index === 0, // Assume first column is primary key
is_foreign_key: item.name.toLowerCase().includes('_id') && index > 0, // Simple heuristic for foreign keys
is_nullable: item.is_nullable || false,
database: dbSlug,
schema: schemaSlug,
table: tableSlug
}));
}
console.log(`Number of columns found for table ${tableSlug}: ${columns.length}`);
return columns;
} catch (error) {
console.error(`Error fetching columns for table ${tableSlug}:`, error);
throw error;
}
};
// Function to fetch complete database structure with schemas, tables, and columns
const fetchCompleteDatabase = async (dbSlug) => {
try {
console.log(`Fetching complete structure for database: ${dbSlug}`);
// Fetch schemas
const schemas = await fetchSchemas(dbSlug);
// Fetch tables and columns for each schema
const schemasWithTables = await Promise.all(
schemas.map(async (schema) => {
try {
const tables = await fetchTables(dbSlug, schema.sch);
// Fetch columns for each table
const tablesWithColumns = await Promise.all(
tables.map(async (table) => {
try {
const columns = await fetchColumns(dbSlug, schema.sch, table.tbl);
return {
...table,
columns: columns
};
} catch (columnError) {
console.warn(`Error fetching columns for table ${table.name}:`, columnError);
return {
...table,
columns: [] // Return table with empty columns if column fetch fails
};
}
})
);
return {
...schema,
tables: tablesWithColumns
};
} catch (tableError) {
console.warn(`Error fetching tables for schema ${schema.sch}:`, tableError);
return {
...schema,
tables: [] // Return schema with empty tables if table fetch fails
};
}
})
);
console.log(`Complete database structure for ${dbSlug}:`, schemasWithTables);
return schemasWithTables;
} catch (error) {
console.error(`Error fetching complete database structure for ${dbSlug}:`, error);
throw error;
}
};
// Generate dummy ER relationship data based on single database structure
const generateDummyERData = (database) => {
const erData = [];
if (database.schemas && database.schemas.length > 0) {
database.schemas.forEach(schema => {
if (schema.tables && schema.tables.length > 0) {
// Create relationships between tables in the same schema
for (let i = 0; i < schema.tables.length - 1; i++) {
const sourceTable = schema.tables[i];
const targetTable = schema.tables[i + 1];
// Create a dummy relationship with varied types
const relationshipTypes = ['1:N', '1:1', 'N:M'];
const randomType = relationshipTypes[Math.floor(Math.random() * relationshipTypes.length)];
erData.push({
source_column_set: [
{
table_id: sourceTable.id,
column_name: sourceTable.columns?.[0]?.name || 'id',
table_name: sourceTable.name,
schema_name: schema.sch
}
],
destination_column_set: [
{
table_id: targetTable.id,
column_name: `${sourceTable.name}_id`,
table_name: targetTable.name,
schema_name: schema.sch
}
],
relationship_type: randomType
});
}
}
});
}
return erData;
};
// Fetch databases and generate ER diagram using real API
useEffect(() => {
const fetchERData = async () => {
try {
setIsLoading(true);
console.log(`Fetching ER data for service: ${selectedService?.name}, data source: ${selectedDataSource?.name}`);
try {
// Fetch databases from API
const databases = await fetchDatabases();
console.log('Fetched databases:', databases);
if (databases && databases.length > 0) {
// Select database based on selectedDataSource or use first available
const targetDatabase = databases.find(db =>
selectedDataSource?.name === db.name ||
selectedDataSource?.name === db.con ||
selectedDataSource?.slug === db.con
) || databases[0]; // Fallback to first database if no match
console.log('Selected database for ER diagram:', targetDatabase);
console.log('Available databases:', databases.map(db => ({ name: db.name, con: db.con })));
// Fetch complete structure (schemas, tables, columns) for the selected database
const schemasWithTables = await fetchCompleteDatabase(targetDatabase.con);
// Create the complete database object using actual API data
const completeDatabase = {
id: targetDatabase.id,
name: targetDatabase.name,
slug: targetDatabase.con,
description: targetDatabase.description || `Database: ${targetDatabase.name}`,
service: selectedService?.name,
dataSource: selectedDataSource?.name,
schemas: schemasWithTables
};
console.log('Complete database structure:', completeDatabase);
console.log('Schemas in database:', completeDatabase.schemas.map(s => ({
name: s.name,
sch: s.sch,
tableCount: s.tables?.length || 0,
tables: s.tables?.map(t => t.name) || []
})));
setDatabases([completeDatabase]); // Wrap in array for compatibility
setSelectedDatabase(completeDatabase); // Set selected database for modal
generateERDiagram(completeDatabase);
setIsUsingMockData(false);
setApiError(null);
console.log('Successfully fetched and generated ER diagram from API data');
} else {
throw new Error('No databases found from API');
}
} catch (apiError) {
// Handle specific error types
let errorMessage = 'Unknown API error';
if (apiError.code === 'ERR_NETWORK' || apiError.message.includes('CORS')) {
errorMessage = 'CORS error - API not accessible from browser';
console.warn('CORS error detected, using mock data:', apiError.message);
} else if (apiError.response?.status === 403) {
errorMessage = '403 Forbidden - Check API token and permissions';
console.warn('403 Forbidden error, using mock data. Check your API token and permissions.');
} else if (apiError.response?.status === 401) {
errorMessage = '401 Unauthorized - Invalid API token';
console.warn('401 Unauthorized error, using mock data. Check your API token.');
} else {
errorMessage = `API Error: ${apiError.message}`;
console.warn('API error, using mock data:', apiError.message);
}
setApiError(errorMessage);
setIsUsingMockData(true);
// Fallback to mock data
const singleDbData = {
...mockDatabaseData,
service: selectedService?.name,
dataSource: selectedDataSource?.name
};
setDatabases([singleDbData]); // Wrap in array for compatibility
setSelectedDatabase(singleDbData); // Set selected database for modal
generateERDiagram(singleDbData);
console.log('Using mock data for ER diagram due to API error');
}
} catch (error) {
console.error('Unexpected error in fetchERData:', error);
setApiError('Unexpected error occurred');
setIsUsingMockData(true);
// Fallback to mock data
const singleDbData = {
...mockDatabaseData,
service: selectedService?.name,
dataSource: selectedDataSource?.name
};
setDatabases([singleDbData]);
setSelectedDatabase(singleDbData); // Set selected database for modal
generateERDiagram(singleDbData);
} finally {
setIsLoading(false);
}
};
// Only fetch if we have selected service and data source
if (selectedService && selectedDataSource) {
fetchERData();
}
}, [selectedService, selectedDataSource]); // Re-fetch when service or data source changes
// Auto-fit view when nodes are updated
useEffect(() => {
if (nodes.length > 0 && !isLoading) {
const timer = setTimeout(() => {
fitView({
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.15,
maxZoom: 0.8,
duration: 600
});
}, 300);
return () => clearTimeout(timer);
}
}, [nodes, isLoading, fitView]);
// Auto-alignment layout calculator
const calculateOptimalLayout = (databaseData) => {
const layouts = [];
let totalWidth = 0;
let maxHeight = 0;
databaseData.forEach((db) => {
if (db.schemas && db.schemas.length > 0) {
const dbLayout = { schemas: [], totalWidth: 0, maxHeight: 0 };
db.schemas.forEach((schema) => {
if (schema.tables && schema.tables.length > 0) {
const tableCount = schema.tables.length;
const tablesPerRow = Math.min(4, Math.ceil(Math.sqrt(tableCount))); // Max 4 tables per row
const rows = Math.ceil(tableCount / tablesPerRow);
// Calculate optimal schema dimensions
const schemaWidth = Math.max(800, tablesPerRow * 320 + 160);
const schemaHeight = Math.max(600, rows * 280 + 200);
dbLayout.schemas.push({
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow,
rows
});
dbLayout.totalWidth += schemaWidth + 100; // Add spacing
dbLayout.maxHeight = Math.max(dbLayout.maxHeight, schemaHeight);
}
});
layouts.push(dbLayout);
totalWidth = Math.max(totalWidth, dbLayout.totalWidth);
maxHeight += dbLayout.maxHeight + 100; // Add vertical spacing
}
});
return { layouts, totalWidth, maxHeight };
};
// Handle adding a new table
const handleAddTable = async (tableData) => {
try {
console.log('Adding new table:', tableData);
// Here you would typically make an API call to create the table
// For now, we'll add it to the current diagram
// Find the target schema
const targetSchema = availableSchemas.find(schema => schema.sch === tableData.schema);
if (!targetSchema) {
throw new Error('Target schema not found');
}
// Create a new table object
const newTable = {
id: `new-table-${Date.now()}`,
name: tableData.name,
description: tableData.description,
table_type: tableData.table_type,
columns: tableData.columns,
schema: tableData.schema,
database: selectedDatabase?.name || 'Unknown'
};
// Add the table to existing tables list
setExistingTables(prev => [...prev, newTable]);
// Update the current database structure and regenerate the diagram
const updatedDatabase = { ...selectedDatabase };
const schemaIndex = updatedDatabase.schemas.findIndex(s => s.sch === tableData.schema);
if (schemaIndex !== -1) {
if (!updatedDatabase.schemas[schemaIndex].tables) {
updatedDatabase.schemas[schemaIndex].tables = [];
}
updatedDatabase.schemas[schemaIndex].tables.push(newTable);
// Regenerate the ER diagram with the new table
generateERDiagram(updatedDatabase);
console.log('Table added successfully:', newTable);
}
} catch (error) {
console.error('Error adding table:', error);
throw error;
}
};
// Generate ER diagram with Database Wrapper structure
const generateERDiagram = (database) => {
const newNodes = [];
const newEdges = [];
// Generate dummy relationships
const relationships = generateDummyERData(database);
// Process the selected database
console.log('Generating ER diagram for database:', database?.name);
console.log('Total schemas in database:', database?.schemas?.length);
console.log('Schema details:', database?.schemas?.map(s => ({ name: s.name, sch: s.sch, tableCount: s.tables?.length || 0 })));
if (database && database.schemas && database.schemas.length > 0) {
// Calculate database wrapper dimensions
let totalSchemaWidth = 0;
let maxSchemaHeight = 0;
let totalTables = 0;
// Calculate layout for all schemas
const schemaLayouts = database.schemas.map(schema => {
const tableCount = (schema.tables && schema.tables.length) || 0;
totalTables += tableCount;
if (tableCount > 0) {
const tablesPerRow = Math.min(3, Math.ceil(Math.sqrt(tableCount))); // Max 3 tables per row for better fit
const rows = Math.ceil(tableCount / tablesPerRow);
// Calculate schema dimensions with proper padding for tables
const tableWidth = 260; // Width of each table node
const tableHeight = 280; // Height of each table node (including spacing)
const tableSpacingCalc = 330; // Horizontal spacing between tables
const tableRowSpacingCalc = 320; // Vertical spacing between table rows
const schemaPadding = 200; // Padding around the schema content
// Calculate required width and height based on table layout
const tableStartX = 90; // Starting X position within schema
const tableStartY = 120; // Starting Y position within schema
const requiredWidth = tableStartX + (tablesPerRow * tableSpacingCalc) + 100; // Extra margin
const requiredHeight = tableStartY + (rows * tableRowSpacingCalc) + 100; // Extra margin
const schemaWidth = Math.max(900, requiredWidth);
const schemaHeight = Math.max(650, requiredHeight);
console.log(`Schema "${schema.name || schema.sch}" layout: ${tableCount} tables, ${tablesPerRow} per row, ${rows} rows`);
console.log(`Required dimensions: ${requiredWidth}x${requiredHeight}, Final: ${schemaWidth}x${schemaHeight}`);
totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas
maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight);
return {
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow,
rows,
tableCount
};
} else {
// Handle schemas with no tables - show empty schema container
const schemaWidth = 900; // Minimum width for empty schema
const schemaHeight = 400; // Minimum height for empty schema
totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas
maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight);
return {
schema,
width: schemaWidth,
height: schemaHeight,
tablesPerRow: 0,
rows: 0,
tableCount: 0
};
}
});
console.log('Schema layouts created:', schemaLayouts.length);
console.log('Schema layout details:', schemaLayouts.map(sl => ({
name: sl.schema.name || sl.schema.sch,
sch: sl.schema.sch,
tableCount: sl.tableCount,
width: sl.width,
height: sl.height
})));
// Create Database Wrapper Node
const databaseWrapperId = `database-${database.slug}`;
const databaseWrapperWidth = Math.max(2000, totalSchemaWidth + 300);
const databaseWrapperHeight = Math.max(1400, maxSchemaHeight + 400);
newNodes.push({
id: databaseWrapperId,
type: 'erDatabaseWrapper',
position: { x: 50, y: 50 },
data: {
name: database.name,
schemaCount: database.schemas.length,
totalTables: totalTables,
width: databaseWrapperWidth,
height: databaseWrapperHeight,
contentWidth: totalSchemaWidth,
contentHeight: maxSchemaHeight
},
draggable: true,
selectable: false,
style: {
width: databaseWrapperWidth,
height: databaseWrapperHeight,
zIndex: -2
}
});
// Create Schema Group Nodes within Database Wrapper
let schemaXOffset = 180; // Start position within database wrapper
const schemaYOffset = 180; // Y position within database wrapper
schemaLayouts.forEach((schemaLayout, schemaIndex) => {
const schemaGroupId = `schema-${database.slug}-${schemaLayout.schema.sch}`;
// Create schema group node
newNodes.push({
id: schemaGroupId,
type: 'erSchemaGroup',
position: { x: schemaXOffset, y: schemaYOffset },
data: {
name: schemaLayout.schema.name || schemaLayout.schema.sch, // Use schema name, fallback to sch
sch: schemaLayout.schema.sch, // Keep sch for identification
tableCount: schemaLayout.tableCount,
width: schemaLayout.width,
height: schemaLayout.height
},
draggable: true,
selectable: false,
parentNode: databaseWrapperId,
extent: 'parent',
style: {
width: schemaLayout.width,
height: schemaLayout.height,
zIndex: -1
}
});
// Create table nodes within schema (only if tables exist)
if (schemaLayout.schema.tables && schemaLayout.schema.tables.length > 0) {
const tableSpacing = 330; // Horizontal spacing between tables
const tableRowSpacing = 320; // Vertical spacing between table rows
const tableStartX = 90; // Starting X position within schema (accounting for schema padding)
const tableStartY = 120; // Starting Y position within schema (accounting for schema padding and label)
let currentRow = 0;
let currentCol = 0;
schemaLayout.schema.tables.forEach((table, tableIndex) => {
const tableId = `table-${table.id || `${database.slug}-${schemaLayout.schema.sch}-${table.name}`}`;
// Calculate table position within schema (relative to schema, not absolute)
const tableX = tableStartX + (currentCol * tableSpacing);
const tableY = tableStartY + (currentRow * tableRowSpacing);
// Debug table positioning
if (tableIndex === 0) {
console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" dimensions: ${schemaLayout.width}x${schemaLayout.height}`);
console.log(`Tables per row: ${schemaLayout.tablesPerRow}, Total rows: ${schemaLayout.rows}`);
console.log(`Table count: ${schemaLayout.tableCount}`);
}
// Ensure table position is within schema bounds
const maxTableX = schemaLayout.width - 280; // Table width + some margin
const maxTableY = schemaLayout.height - 300; // Table height + some margin
if (tableX > maxTableX || tableY > maxTableY) {
console.warn(`Table "${table.name}" position (${tableX}, ${tableY}) exceeds schema bounds (${schemaLayout.width}x${schemaLayout.height})`);
}
// Add primary key and foreign key indicators to columns
const enhancedColumns = (table.columns || []).map((col, colIndex) => ({
...col,
is_primary_key: colIndex === 0, // First column as primary key
is_foreign_key: relationships.some(rel =>
rel.destination_column_set.some(dest =>
dest.column_name === col.name && dest.table_name === table.name
)
)
}));
newNodes.push({
id: tableId,
type: 'erTable',
position: { x: tableX, y: tableY },
data: {
...table,
columns: enhancedColumns,
schema: schemaLayout.schema.sch,
database: database.name
},
draggable: true,
parentNode: schemaGroupId,
extent: 'parent'
});
// Move to next position
currentCol++;
if (currentCol >= schemaLayout.tablesPerRow) {
currentCol = 0;
currentRow++;
}
});
} else {
// Schema has no tables - show empty state message
console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" (${schemaLayout.schema.sch}) has no tables`);
}
schemaXOffset += schemaLayout.width + 150; // Move to next schema position
});
}
// Create edges for relationships
relationships.forEach((rel, index) => {
const sourceTableId = `table-${rel.source_column_set[0]?.table_id || `${rel.source_column_set[0]?.schema_name}-${rel.source_column_set[0]?.table_name}`}`;
const targetTableId = `table-${rel.destination_column_set[0]?.table_id || `${rel.destination_column_set[0]?.schema_name}-${rel.destination_column_set[0]?.table_name}`}`;
// Check if both nodes exist
const sourceExists = newNodes.some(node => node.id === sourceTableId);
const targetExists = newNodes.some(node => node.id === targetTableId);
if (sourceExists && targetExists) {
newEdges.push({
id: `relationship-${index}`,
type: 'erRelationship',
source: sourceTableId,
target: targetTableId,
data: {
relationship_type: rel.relationship_type,
source_column: rel.source_column_set[0]?.column_name,
target_column: rel.destination_column_set[0]?.column_name
},
style: {
stroke: '#8a2be2',
strokeWidth: 2
},
animated: false
});
}
});
setNodes(newNodes);
setEdges(newEdges);
// Update available schemas and existing tables for the modal
if (database && database.schemas) {
setAvailableSchemas(database.schemas);
// Collect all existing tables from all schemas
const allTables = [];
database.schemas.forEach(schema => {
if (schema.tables) {
schema.tables.forEach(table => {
allTables.push({
...table,
schema: schema.sch,
schemaName: schema.name || schema.sch
});
});
}
});
setExistingTables(allTables);
}
// Auto-fit view after a short delay to ensure nodes are rendered
setTimeout(() => {
if (newNodes.length > 0) {
fitView({
padding: 0.1,
includeHiddenNodes: false,
minZoom: 0.15,
maxZoom: 0.8,
duration: 800 // Smooth animation
});
}
}, 300);
};
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge({
...params,
type: 'erRelationship',
data: {
relationship_type: '1:N',
source_column: 'id',
target_column: 'foreign_key_id'
},
style: {
stroke: '#8a2be2',
strokeWidth: 2
},
animated: true
}, eds)),
[setEdges]
);
const handleAddClick = () => {
setShowAddMenu(!showAddMenu);
};
const handleDataSourceSelect = (dataSource) => {
setSelectedDataSource(dataSource);
setDataSourceMenuAnchor(null);
};
const handleCloseMenus = () => {
setDataSourceMenuAnchor(null);
};
if (isLoading) {
return (
{/* Breadcrumb Header */}
{/* Loading Content */}
Loading ER Diagram...
Fetching schema for {selectedService?.name} - {selectedDataSource?.name}
);
}
return (
{/* Breadcrumb Header */}
{/* ReactFlow Container */}
{
if (node.type === 'erTable') {
switch(node.data.table_type) {
case 'stage': return '#00a99d';
case 'fact': return '#fa8c16';
case 'dimension': return '#52c41a';
default: return '#8a2be2';
}
}
return '#666';
}}
/>
Entity Relationship Diagram
Tables: {nodes.filter(n => n.type === 'erTable').length} •
Relationships: {edges.length} •
Schemas: {nodes.filter(n => n.type === 'erSchemaGroup').length}
{isUsingMockData && (
Using mock data - {apiError}
)}
{/* Relationship Legend Panel */}
Relationship Types
→
1:N - One to Many
→
1:1 - One to One
↔
N:M - Many to Many
• Arrows show relationship direction
• Numbers show cardinality
• Click edges to select them
{/* Data Sources Menu */}
{selectedService && (
)}
{/* Add Table Button */}
{/* Add Table Modal */}
setIsAddTableModalOpen(false)}
onAddTable={handleAddTable}
schemas={availableSchemas}
existingTables={existingTables}
/>
);
};
// Wrapper component with ReactFlowProvider
const ERDiagramCanvas = () => {
return (
);
};
export default ERDiagramCanvas;