Compare commits

...

2 Commits

8 changed files with 958 additions and 333 deletions

View File

@ -493,3 +493,238 @@ main {
transform: translateY(0); transform: translateY(0);
} }
} }
/* Header specific styles */
.header-logo-container {
display: flex;
align-items: center;
}
.header-logo {
width: 40px;
height: 40px;
background: linear-gradient(45deg, #00a99d, #52c41a);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
box-shadow: 0 0 10px rgba(82, 196, 26, 0.5);
animation: pulse 2s infinite;
}
.header-logo-icon {
font-size: 20px;
color: white;
}
.header-title {
margin: 0;
font-size: 1.5rem;
background: linear-gradient(90deg, #00a99d, #52c41a, #fa8c16);
/* -webkit-background-clip: text; */
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(82, 196, 26, 0.3);
}
.header-controls {
display: flex;
align-items: center;
}
.user-profile-icon {
margin-right: 8px;
}
/* Main layout styles */
.main-layout {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
}
.sidebar-nav {
width: 220px;
background: #1a1a1a;
border-right: 1px solid #333;
padding: 20px 0;
display: flex;
flex-direction: column;
}
.sidebar-nav-content {
display: flex;
flex-direction: column;
gap: 5px;
padding-left: 15px;
}
.nav-button.main-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-radius: 6px;
cursor: pointer;
background: rgba(0, 169, 157, 0.15);
transition: all 0.2s ease;
margin-bottom: 5px;
}
.main-item-icon {
font-size: 20px;
color: #00a99d;
margin-right: 12px;
}
.main-item-text {
color: #fff;
font-weight: 600;
flex: 1;
}
.dropdown-chevron {
color: #aaa;
font-size: 14px;
}
.dropdown-container {
flex-direction: column;
gap: 4px;
padding-left: 15px;
margin-left: 10px;
border-left: 1px solid rgba(170, 170, 170, 0.2);
transition: all 0.3s ease;
}
.dropdown-container.open {
display: flex;
}
.dropdown-container.closed {
display: none;
}
.nav-button.dropdown-item {
display: flex;
align-items: center;
padding: 8px 15px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.nav-button.dropdown-item:not(.active) {
background: transparent;
}
.nav-button.dropdown-item.active {
background: rgba(0, 169, 157, 0.1);
}
.dropdown-item-icon {
font-size: 16px;
margin-right: 12px;
}
.dropdown-item-icon.active {
color: #00a99d;
}
.dropdown-item-icon.inactive {
color: #aaa;
}
.dropdown-item-text {
font-weight: normal;
}
.dropdown-item-text.active {
color: #fff;
font-weight: 500;
}
.dropdown-item-text.inactive {
color: #aaa;
}
/* Main content area */
.main-content {
flex: 1;
overflow: hidden;
position: relative;
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
color: #ffffff;
}
.charts-container {
padding: 20px;
height: 100%;
overflow: auto;
}
/* Info panels */
.info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(26, 26, 26, 0.8);
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-size: 12px;
pointer-events: none;
backdrop-filter: blur(5px);
color: #ffffff;
max-width: 300px;
animation: fadeIn 0.5s ease;
}
.info-panel.canvas {
border: 1px solid rgba(82, 196, 26, 0.3);
}
.info-panel.dataflow {
border: 1px solid rgba(250, 140, 22, 0.3);
}
.info-panel-main {
margin: 0;
display: flex;
align-items: center;
font-size: 13px;
}
.info-panel-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.info-panel-indicator.canvas {
background: #52c41a;
box-shadow: 0 0 8px #52c41a;
}
.info-panel-indicator.dataflow {
background: #fa8c16;
box-shadow: 0 0 8px #fa8c16;
}
.info-panel-sub {
margin: 5px 0 0 16px;
font-size: 11px;
opacity: 0.7;
}

View File

@ -4,6 +4,7 @@ import InfiniteCanvas from './components/InfiniteCanvas'
import AdvancedCharts from './components/AdvancedCharts' import AdvancedCharts from './components/AdvancedCharts'
import DataflowCanvas from './components/DataflowCanvas' import DataflowCanvas from './components/DataflowCanvas'
import ERDiagramCanvas from './components/ERDiagramCanvas' import ERDiagramCanvas from './components/ERDiagramCanvas'
import PipelinesCanvas from './components/PipelinesCanvas'
import { FaDatabase, FaChartBar, FaProjectDiagram, FaSitemap, FaFolder, FaCog, FaChevronDown, FaChevronRight, FaTachometerAlt } from 'react-icons/fa' import { FaDatabase, FaChartBar, FaProjectDiagram, FaSitemap, FaFolder, FaCog, FaChevronDown, FaChevronRight, FaTachometerAlt } from 'react-icons/fa'
import { Breadcrumbs, Link, Typography } from '@mui/material' import { Breadcrumbs, Link, Typography } from '@mui/material'
import { createTheme, ThemeProvider } from '@mui/material/styles' import { createTheme, ThemeProvider } from '@mui/material/styles'
@ -36,6 +37,7 @@ function App() {
if (pathname === '/charts') return 'charts'; if (pathname === '/charts') return 'charts';
if (pathname === '/data_mappings') return 'data_mappigs'; if (pathname === '/data_mappings') return 'data_mappigs';
if (pathname === '/er_diagram') return 'er_diagram'; if (pathname === '/er_diagram') return 'er_diagram';
if (pathname === '/pipelines') return 'pipelines';
if (pathname === '/settings') return 'settings'; if (pathname === '/settings') return 'settings';
// Default to canvas/qubit_service // Default to canvas/qubit_service
window.history.pushState(null, '', '/qubit_service'); window.history.pushState(null, '', '/qubit_service');
@ -58,6 +60,7 @@ function App() {
else if (pathname === '/charts') setActiveTab('charts'); else if (pathname === '/charts') setActiveTab('charts');
else if (pathname === '/data_mappings') setActiveTab('data_mappigs'); else if (pathname === '/data_mappings') setActiveTab('data_mappigs');
else if (pathname === '/er_diagram') setActiveTab('er_diagram'); else if (pathname === '/er_diagram') setActiveTab('er_diagram');
else if (pathname === '/pipelines') setActiveTab('pipelines');
// Settings tab would be handled here too // Settings tab would be handled here too
}; };
@ -98,6 +101,7 @@ function App() {
if (event.detail === 'charts') path = '/charts'; if (event.detail === 'charts') path = '/charts';
if (event.detail === 'data_mappigs') path = '/data_mappings'; if (event.detail === 'data_mappigs') path = '/data_mappings';
if (event.detail === 'er_diagram') path = '/er_diagram'; if (event.detail === 'er_diagram') path = '/er_diagram';
if (event.detail === 'pipelines') path = '/pipelines';
if (event.detail === 'settings') path = '/settings'; if (event.detail === 'settings') path = '/settings';
window.history.pushState(null, '', path); window.history.pushState(null, '', path);
@ -117,48 +121,20 @@ function App() {
return ( return (
<ThemeProvider theme={darkTheme}> <ThemeProvider theme={darkTheme}>
<div className="app-container" style={{ <div className="app-container">
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
background: '#121212', /* Dark background */
color: '#ffffff'
}}>
<header> <header>
{/* Logo and Title */} {/* Logo and Title */}
<div style={{ display: 'flex', alignItems: 'center' }}> <div className="header-logo-container">
<div style={{ <div className="header-logo">
width: '40px', <FaDatabase className="header-logo-icon" />
height: '40px',
background: 'linear-gradient(45deg, #00a99d, #52c41a)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '15px',
boxShadow: '0 0 10px rgba(82, 196, 26, 0.5)',
animation: 'pulse 2s infinite'
}}>
<FaDatabase style={{ fontSize: '20px', color: 'white' }} />
</div> </div>
<h1 style={{ <h1 className="header-title">
margin: 0,
fontSize: '1.5rem',
background: 'linear-gradient(90deg, #00a99d, #52c41a, #fa8c16)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
textShadow: '0 0 20px rgba(82, 196, 26, 0.3)'
}}>
{/* The Metricverse
*/}
Qubit Service Qubit Service
</h1> </h1>
</div> </div>
{/* User profile and controls */} {/* User profile and controls */}
<div style={{ display: 'flex', alignItems: 'center' }}> <div className="header-controls">
<div className="user-profile"> <div className="user-profile">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -170,7 +146,7 @@ function App() {
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
style={{ marginRight: '8px' }} className="user-profile-icon"
> >
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle> <circle cx="12" cy="7" r="4"></circle>
@ -197,235 +173,119 @@ function App() {
</div> </div>
</header> </header>
<div style={{ flex: 1, display: 'flex', flexDirection: 'row', overflow: 'hidden' }}> <div className="main-layout">
{/* Sidebar navigation with dropdown */} {/* Sidebar navigation with dropdown */}
<div className="sidebar-nav" style={{ <div className="sidebar-nav">
width: '220px', <div className="sidebar-nav-content">
background: '#1a1a1a',
borderRight: '1px solid #333',
padding: '20px 0',
display: 'flex',
flexDirection: 'column'
}}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px', paddingLeft: '15px' }}>
{/* Main Qbit Item with Dropdown */} {/* Main Qbit Item with Dropdown */}
<div <div
className="nav-button main-item" className="nav-button main-item"
onClick={() => setIsDropdownOpen(!isDropdownOpen)} onClick={() => setIsDropdownOpen(!isDropdownOpen)}
style={{
display: 'flex',
alignItems: 'center',
padding: '12px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: 'rgba(0, 169, 157, 0.15)',
transition: 'all 0.2s ease',
marginBottom: '5px'
}}
> >
<FaFolder style={{ <FaFolder className="main-item-icon" />
fontSize: '20px', <span className="main-item-text">
color: '#00a99d',
marginRight: '12px'
}} />
<span style={{
color: '#fff',
fontWeight: '600',
flex: 1
}}>
Qbit Qbit
</span> </span>
{isDropdownOpen ? {isDropdownOpen ?
<FaChevronDown style={{ color: '#aaa', fontSize: '14px' }} /> : <FaChevronDown className="dropdown-chevron" /> :
<FaChevronRight style={{ color: '#aaa', fontSize: '14px' }} /> <FaChevronRight className="dropdown-chevron" />
} }
</div> </div>
{/* Dropdown items container with animation */} {/* Dropdown items container with animation */}
<div <div className={`dropdown-container ${isDropdownOpen ? 'open' : 'closed'}`}>
className="dropdown-container"
style={{
display: isDropdownOpen ? 'flex' : 'none',
flexDirection: 'column',
gap: '4px',
paddingLeft: '15px',
marginLeft: '10px',
borderLeft: '1px solid rgba(170, 170, 170, 0.2)',
transition: 'all 0.3s ease'
}}
>
{/* Overview Item */} {/* Overview Item */}
<div <div
className={`nav-button ${activeTab === 'canvas' ? 'active' : ''}`} className={`nav-button dropdown-item ${activeTab === 'canvas' ? 'active' : ''}`}
onClick={() => { onClick={() => {
setActiveTab('canvas'); setActiveTab('canvas');
window.history.pushState(null, '', '/qubit_service'); window.history.pushState(null, '', '/qubit_service');
}} }}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === 'canvas' ? 'rgba(0, 169, 157, 0.1)' : 'transparent',
transition: 'all 0.2s ease'
}}
> >
<FaProjectDiagram style={{ <FaProjectDiagram className={`dropdown-item-icon ${activeTab === 'canvas' ? 'active' : 'inactive'}`} />
fontSize: '16px', <span className={`dropdown-item-text ${activeTab === 'canvas' ? 'active' : 'inactive'}`}>
color: activeTab === 'canvas' ? '#00a99d' : '#aaa', Services
marginRight: '12px'
}} />
<span style={{
color: activeTab === 'canvas' ? '#fff' : '#aaa',
fontWeight: activeTab === 'canvas' ? '500' : 'normal'
}}>
Overview
</span> </span>
</div> </div>
{/* Data Entity Item */} {/* Data Entity Item */}
<div <div
className={`nav-button ${activeTab === 'er_diagram' ? 'active' : ''}`} className={`nav-button dropdown-item ${activeTab === 'er_diagram' ? 'active' : ''}`}
onClick={() => { onClick={() => {
setActiveTab('er_diagram'); setActiveTab('er_diagram');
window.history.pushState(null, '', '/er_diagram'); window.history.pushState(null, '', '/er_diagram');
}} }}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === 'er_diagram' ? 'rgba(0, 169, 157, 0.1)' : 'transparent',
transition: 'all 0.2s ease'
}}
> >
<FaSitemap style={{ <FaSitemap className={`dropdown-item-icon ${activeTab === 'er_diagram' ? 'active' : 'inactive'}`} />
fontSize: '16px', <span className={`dropdown-item-text ${activeTab === 'er_diagram' ? 'active' : 'inactive'}`}>
color: activeTab === 'er_diagram' ? '#00a99d' : '#aaa',
marginRight: '12px'
}} />
<span style={{
color: activeTab === 'er_diagram' ? '#fff' : '#aaa',
fontWeight: activeTab === 'er_diagram' ? '500' : 'normal'
}}>
Data Entity Data Entity
</span> </span>
</div> </div>
{/* Data Mapping Item */} {/* Data Mapping Item */}
<div <div
className={`nav-button ${activeTab === 'dataflow' ? 'active' : ''}`} className={`nav-button dropdown-item ${activeTab === 'dataflow' ? 'active' : ''}`}
onClick={() => { onClick={() => {
setActiveTab('dataflow'); setActiveTab('dataflow');
window.history.pushState(null, '', '/data_mappings'); window.history.pushState(null, '', '/data_mappings');
}} }}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === 'dataflow' ? 'rgba(0, 169, 157, 0.1)' : 'transparent',
transition: 'all 0.2s ease'
}}
> >
<FaDatabase style={{ <FaDatabase className={`dropdown-item-icon ${activeTab === 'dataflow' ? 'active' : 'inactive'}`} />
fontSize: '16px', <span className={`dropdown-item-text ${activeTab === 'dataflow' ? 'active' : 'inactive'}`}>
color: activeTab === 'dataflow' ? '#00a99d' : '#aaa', Process
marginRight: '12px'
}} />
<span style={{
color: activeTab === 'dataflow' ? '#fff' : '#aaa',
fontWeight: activeTab === 'dataflow' ? '500' : 'normal'
}}>
Data Mapping
</span> </span>
</div> </div>
{/* Settings Item */} {/* Pipelines Item */}
<div <div
className={`nav-button ${activeTab === 'settings' ? 'active' : ''}`} className={`nav-button dropdown-item ${activeTab === 'pipelines' ? 'active' : ''}`}
onClick={() => { onClick={() => {
alert('Settings functionality not implemented yet'); setActiveTab('pipelines');
}} window.history.pushState(null, '', '/pipelines');
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === 'settings' ? 'rgba(0, 169, 157, 0.1)' : 'transparent',
transition: 'all 0.2s ease'
}} }}
> >
<FaTachometerAlt <FaTachometerAlt className={`dropdown-item-icon ${activeTab === 'pipelines' ? 'active' : 'inactive'}`} />
style={{ <span className={`dropdown-item-text ${activeTab === 'pipelines' ? 'active' : 'inactive'}`}>
fontSize: '16px', Pipelines
color: activeTab === 'settings' ? '#00a99d' : '#aaa',
marginRight: '12px'
}}
/>
<span style={{
color: activeTab === 'settings' ? '#fff' : '#aaa',
fontWeight: activeTab === 'settings' ? '500' : 'normal'
}}>
Dashboard
</span> </span>
</div> </div>
{/* Advanced Charts Item */} {/* Triggers Item */}
{/* <div <div
className={`nav-button ${activeTab === 'charts' ? 'active' : ''}`} className={`nav-button dropdown-item ${activeTab === 'triggers' ? 'active' : ''}`}
onClick={() => { onClick={() => {
setActiveTab('charts'); alert('Triggers functionality not implemented yet');
window.history.pushState(null, '', '/charts');
}}
style={{
display: 'flex',
alignItems: 'center',
padding: '8px 15px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === 'charts' ? 'rgba(0, 169, 157, 0.1)' : 'transparent',
transition: 'all 0.2s ease'
}} }}
> >
<FaChartBar style={{ <FaTachometerAlt className={`dropdown-item-icon ${activeTab === 'triggers' ? 'active' : 'inactive'}`} />
fontSize: '16px', <span className={`dropdown-item-text ${activeTab === 'triggers' ? 'active' : 'inactive'}`}>
color: activeTab === 'charts' ? '#00a99d' : '#aaa', Triggers
marginRight: '12px'
}} />
<span style={{
color: activeTab === 'charts' ? '#fff' : '#aaa',
fontWeight: activeTab === 'charts' ? '500' : 'normal'
}}>
Advanced Charts
</span> </span>
</div> */} </div>
{/* Schedulers Item */}
<div
className={`nav-button dropdown-item ${activeTab === 'schedulers' ? 'active' : ''}`}
onClick={() => {
alert('Schedulers functionality not implemented yet');
}}
>
<FaTachometerAlt className={`dropdown-item-icon ${activeTab === 'schedulers' ? 'active' : 'inactive'}`} />
<span className={`dropdown-item-text ${activeTab === 'schedulers' ? 'active' : 'inactive'}`}>
Schedulers
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<main style={{ <main className="main-content">
flex: 1,
overflow: 'hidden',
position: 'relative',
padding: '20px'
}}>
{activeTab === 'canvas' && ( {activeTab === 'canvas' && (
<> <>
<div style={{ <div className="page-header">
marginBottom: '20px' <h1 className="page-title">
}}>
<h1 style={{
fontSize: '28px',
fontWeight: '600',
margin: '0 0 8px 0',
color: '#ffffff'
}}>
Overview Overview
</h1> </h1>
<Breadcrumbs <Breadcrumbs
@ -455,56 +315,18 @@ function App() {
</Breadcrumbs> </Breadcrumbs>
</div> </div>
<InfiniteCanvas /> <InfiniteCanvas />
<div style={{ <div className="info-panel canvas">
position: 'absolute', <p className="info-panel-main">
bottom: '20px', <span className="info-panel-indicator canvas"></span>
left: '20px',
background: 'rgba(26, 26, 26, 0.8)',
padding: '12px 16px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
fontSize: '12px',
pointerEvents: 'none',
backdropFilter: 'blur(5px)',
border: '1px solid rgba(82, 196, 26, 0.3)',
color: '#ffffff',
maxWidth: '300px',
animation: 'fadeIn 0.5s ease'
}}>
<p style={{
margin: 0,
display: 'flex',
alignItems: 'center',
fontSize: '13px'
}}>
<span style={{
display: 'inline-block',
width: '8px',
height: '8px',
background: '#52c41a',
borderRadius: '50%',
marginRight: '8px',
boxShadow: '0 0 8px #52c41a'
}}></span>
{/* Infinite Canvas - Zoom and pan without limits */}
The Only Limit is Your Imagination. The Only Limit is Your Imagination.
</p> </p>
<p style={{ <p className="info-panel-sub">
margin: '5px 0 0 16px',
fontSize: '11px',
opacity: 0.7
}}>
Scroll to zoom Drag to pan Connect nodes Scroll to zoom Drag to pan Connect nodes
</p> </p>
</div> </div>
</> </>
)} )}
{activeTab === 'charts' && (
<div style={{ padding: '20px', height: '100%', overflow: 'auto' }}>
{/* <AdvancedCharts /> */}
</div>
)}
{activeTab === 'dataflow' && ( {activeTab === 'dataflow' && (
<> <>
@ -513,44 +335,12 @@ function App() {
dbSlug={currentDbSlug} dbSlug={currentDbSlug}
hasSchemas={hasSchemas} hasSchemas={hasSchemas}
/> />
<div style={{ <div className="info-panel dataflow">
position: 'absolute', <p className="info-panel-main">
bottom: '20px', <span className="info-panel-indicator dataflow"></span>
left: '20px',
background: 'rgba(26, 26, 26, 0.8)',
padding: '12px 16px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
fontSize: '12px',
pointerEvents: 'none',
backdropFilter: 'blur(5px)',
border: '1px solid rgba(250, 140, 22, 0.3)',
color: '#ffffff',
maxWidth: '300px',
animation: 'fadeIn 0.5s ease'
}}>
<p style={{
margin: 0,
display: 'flex',
alignItems: 'center',
fontSize: '13px'
}}>
<span style={{
display: 'inline-block',
width: '8px',
height: '8px',
background: '#fa8c16',
borderRadius: '50%',
marginRight: '8px',
boxShadow: '0 0 8px #fa8c16'
}}></span>
Visualize data flows between tables Visualize data flows between tables
</p> </p>
<p style={{ <p className="info-panel-sub">
margin: '5px 0 0 16px',
fontSize: '11px',
opacity: 0.7
}}>
Add tables Create processes Connect data flows Add tables Create processes Connect data flows
</p> </p>
</div> </div>
@ -559,21 +349,18 @@ function App() {
{activeTab === 'er_diagram' && ( {activeTab === 'er_diagram' && (
<> <>
<div style={{ <div className="page-header">
marginBottom: '20px' <h1 className="page-title">
}}>
<h1 style={{
fontSize: '28px',
fontWeight: '600',
margin: '0 0 8px 0',
color: '#ffffff'
}}>
Data Entity Data Entity
</h1> </h1>
</div> </div>
<ERDiagramCanvas /> <ERDiagramCanvas />
</> </>
)} )}
{activeTab === 'pipelines' && (
<PipelinesCanvas />
)}
</main> </main>
</div> </div>
</div> </div>

View File

@ -1150,20 +1150,7 @@ const AddTableModal = ({
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>
{/* <Grid item xs={12} md={2}>
<FormControl fullWidth size="small">
<InputLabel>Relation Type</InputLabel>
<Select
value={relation.relationType}
onChange={(e) => updateRelation(relation.id, 'relationType', e.target.value)}
label="Relation Type"
>
<MenuItem value="1:1">One to One</MenuItem>
<MenuItem value="1:N">One to Many</MenuItem>
<MenuItem value="N:M">Many to Many</MenuItem>
</Select>
</FormControl>
</Grid> */}
<Grid item xs={12} md={1.5}> <Grid item xs={12} md={1.5}>
<IconButton <IconButton
onClick={() => removeRelation(relation.id)} onClick={() => removeRelation(relation.id)}

View File

@ -34,11 +34,13 @@ import {
FaChevronRight, FaChevronRight,
FaServer, FaServer,
FaCloud, FaCloud,
FaHdd FaHdd,
FaEdit
} from 'react-icons/fa'; } from 'react-icons/fa';
import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon } from './CustomIcons'; import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon } from './CustomIcons';
import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material'; import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import AddTableModal from './AddTableModal'; import AddTableModal from './AddTableModal';
// import UpdateTableModal from './UpdateTableModal';
@ -69,9 +71,21 @@ const ERTableNode = ({ data, id }) => {
/> />
<div className={`er-table-header ${data.table_type || 'default'}`}> <div className={`er-table-header ${data.table_type || 'default'}`}>
<div className="er-table-header-content">
{getTableIcon()} {getTableIcon()}
<span>{data.name}</span> <span>{data.name}</span>
</div> </div>
{/* <button
className="er-table-update-btn"
onClick={(e) => {
e.stopPropagation();
data.onUpdateTable && data.onUpdateTable(data);
}}
title="Update Table"
>
<FaEdit />
</button> */}
</div>
<ul className="er-column-list"> <ul className="er-column-list">
{data.columns && data.columns.map((column, index) => { {data.columns && data.columns.map((column, index) => {
@ -570,6 +584,10 @@ const ERDiagramCanvasContent = () => {
const [availableSchemas, setAvailableSchemas] = useState([]); const [availableSchemas, setAvailableSchemas] = useState([]);
const [existingTables, setExistingTables] = useState([]); const [existingTables, setExistingTables] = useState([]);
// Update Table Modal state
const [isUpdateTableModalOpen, setIsUpdateTableModalOpen] = useState(false);
const [selectedTableForUpdate, setSelectedTableForUpdate] = useState(null);
// API Configuration // API Configuration
const API_BASE_URL = 'https://sandbox.kezel.io/api'; const API_BASE_URL = 'https://sandbox.kezel.io/api';
const token = "abdhsg"; const token = "abdhsg";
@ -1259,6 +1277,40 @@ const ERDiagramCanvasContent = () => {
} }
}; };
// Handle update table action
const handleUpdateTable = (tableData) => {
console.log('Opening update modal for table:', tableData);
setSelectedTableForUpdate(tableData);
setIsUpdateTableModalOpen(true);
};
// Handle table update submission
const handleUpdateTableSubmit = async (updatedTableData) => {
try {
console.log('Updating table with data:', updatedTableData);
// The API call is handled in the UpdateTableModal component
// After successful update, we need to refresh the diagram
// Close the modal
setIsUpdateTableModalOpen(false);
setSelectedTableForUpdate(null);
// Refresh the diagram by re-fetching data
// You can implement a more efficient update by modifying the existing nodes
// For now, let's trigger a refresh of the current database
if (selectedDatabase) {
generateERDiagram(selectedDatabase);
}
console.log('Table updated successfully');
} catch (error) {
console.error('Error updating table:', error);
// Error handling is done in the UpdateTableModal component
}
};
// Generate Data Entity with Database Wrapper structure // Generate Data Entity with Database Wrapper structure
const generateERDiagram = (database) => { const generateERDiagram = (database) => {
console.log('🔄 Starting Data Entity generation...'); console.log('🔄 Starting Data Entity generation...');
@ -1525,6 +1577,7 @@ const ERDiagramCanvasContent = () => {
columns: enhancedColumns, columns: enhancedColumns,
schema: schemaLayout.schema.sch, schema: schemaLayout.schema.sch,
database: database.name database: database.name
// onUpdateTable: handleUpdateTable
}, },
draggable: true, draggable: true,
parentNode: schemaGroupId, parentNode: schemaGroupId,
@ -1875,6 +1928,20 @@ const ERDiagramCanvasContent = () => {
position="bottom-right" position="bottom-right"
/> />
{/* Update Table Modal */}
{/* <UpdateTableModal
open={isUpdateTableModalOpen}
onClose={() => {
setIsUpdateTableModalOpen(false);
setSelectedTableForUpdate(null);
}}
onUpdateTable={handleUpdateTableSubmit}
tableData={selectedTableForUpdate}
schemas={availableSchemas}
existingTables={existingTables}
position="center"
/> */}
</div> </div>
); );
}; };

View File

@ -200,7 +200,7 @@
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: space-between;
font-weight: bold; font-weight: bold;
font-size: 15px; font-size: 15px;
@ -215,6 +215,38 @@
&.dimension { &.dimension {
background: linear-gradient(135deg, #52c41a, #73d13d); background: linear-gradient(135deg, #52c41a, #73d13d);
} }
.er-table-header-content {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.er-table-update-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 4px;
color: white;
padding: 6px 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 12px;
opacity: 0.8;
&:hover {
background: rgba(255, 255, 255, 0.3);
opacity: 1;
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
} }
.er-column-list { .er-column-list {

View File

@ -0,0 +1,163 @@
import React from 'react';
import { FaTachometerAlt, FaPlus, FaPlay, FaPause, FaStop, FaCog } from 'react-icons/fa';
import './PipelinesCanvas.scss';
const PipelinesCanvas = () => {
// Mock pipeline data
const mockPipelines = [
{
id: 1,
name: 'Data Ingestion Pipeline',
status: 'running',
lastRun: '2024-01-15 10:30:00',
nextRun: '2024-01-15 11:30:00',
description: 'Ingests data from external sources into the data warehouse'
},
{
id: 2,
name: 'ETL Processing Pipeline',
status: 'stopped',
lastRun: '2024-01-15 09:00:00',
nextRun: '2024-01-15 12:00:00',
description: 'Transforms and loads processed data into target tables'
},
{
id: 3,
name: 'Data Quality Pipeline',
status: 'scheduled',
lastRun: '2024-01-14 23:00:00',
nextRun: '2024-01-15 23:00:00',
description: 'Validates data quality and generates quality reports'
}
];
const getStatusColor = (status) => {
switch (status) {
case 'running':
return '#52c41a';
case 'stopped':
return '#ff4d4f';
case 'scheduled':
return '#faad14';
default:
return '#aaa';
}
};
const getStatusIcon = (status) => {
switch (status) {
case 'running':
return <FaPlay />;
case 'stopped':
return <FaStop />;
case 'scheduled':
return <FaPause />;
default:
return <FaCog />;
}
};
return (
<div className="pipelines-container">
{/* Header */}
<div className="pipelines-header">
<div className="header-content">
<div className="header-title">
<FaTachometerAlt className="header-icon" />
<h1>Data Pipelines</h1>
</div>
<p className="header-description">
Manage and monitor your data processing pipelines
</p>
</div>
<button className="add-pipeline-btn">
<FaPlus />
Create Pipeline
</button>
</div>
{/* Pipeline Stats */}
<div className="pipeline-stats">
<div className="stat-card">
<div className="stat-value">3</div>
<div className="stat-label">Total Pipelines</div>
</div>
<div className="stat-card">
<div className="stat-value">1</div>
<div className="stat-label">Running</div>
</div>
<div className="stat-card">
<div className="stat-value">1</div>
<div className="stat-label">Scheduled</div>
</div>
<div className="stat-card">
<div className="stat-value">1</div>
<div className="stat-label">Stopped</div>
</div>
</div>
{/* Pipelines List */}
<div className="pipelines-content">
<div className="pipelines-grid">
{mockPipelines.map((pipeline) => (
<div key={pipeline.id} className="pipeline-card">
<div className="pipeline-header">
<div className="pipeline-title">
<h3>{pipeline.name}</h3>
<div
className="pipeline-status"
style={{ color: getStatusColor(pipeline.status) }}
>
{getStatusIcon(pipeline.status)}
<span>{pipeline.status.charAt(0).toUpperCase() + pipeline.status.slice(1)}</span>
</div>
</div>
</div>
<div className="pipeline-description">
<p>{pipeline.description}</p>
</div>
<div className="pipeline-details">
<div className="detail-item">
<span className="detail-label">Last Run:</span>
<span className="detail-value">{pipeline.lastRun}</span>
</div>
<div className="detail-item">
<span className="detail-label">Next Run:</span>
<span className="detail-value">{pipeline.nextRun}</span>
</div>
</div>
<div className="pipeline-actions">
<button className="action-btn primary">
<FaPlay />
Run
</button>
<button className="action-btn secondary">
<FaCog />
Configure
</button>
<button className="action-btn danger">
<FaStop />
Stop
</button>
</div>
</div>
))}
</div>
</div>
{/* Coming Soon Notice */}
<div className="coming-soon-notice">
<div className="notice-content">
<FaTachometerAlt className="notice-icon" />
<h3>Pipeline Management Coming Soon</h3>
<p>Advanced pipeline creation, scheduling, and monitoring features are under development.</p>
</div>
</div>
</div>
);
};
export default PipelinesCanvas;

View File

@ -0,0 +1,317 @@
// PipelinesCanvas SCSS Styles
.pipelines-container {
width: 100%;
height: 100vh;
background: #121212;
color: #fff;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
// Header Section
.pipelines-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px 0;
border-bottom: 1px solid #333;
.header-content {
.header-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.header-icon {
font-size: 28px;
color: #00a99d;
}
h1 {
margin: 0;
font-size: 2rem;
font-weight: 600;
background: linear-gradient(90deg, #00a99d, #52c41a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
.header-description {
margin: 0;
color: #aaa;
font-size: 1rem;
}
}
.add-pipeline-btn {
background: linear-gradient(135deg, #00a99d, #52c41a);
border: none;
border-radius: 8px;
color: white;
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 169, 157, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 169, 157, 0.4);
}
&:active {
transform: translateY(0);
}
}
}
// Pipeline Stats
.pipeline-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
.stat-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 20px;
text-align: center;
transition: all 0.2s ease;
&:hover {
border-color: #00a99d;
box-shadow: 0 4px 12px rgba(0, 169, 157, 0.1);
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #00a99d;
margin-bottom: 8px;
}
.stat-label {
color: #aaa;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
}
}
// Pipelines Content
.pipelines-content {
flex: 1;
.pipelines-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
.pipeline-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
&:hover {
border-color: #00a99d;
box-shadow: 0 4px 16px rgba(0, 169, 157, 0.1);
transform: translateY(-2px);
}
.pipeline-header {
margin-bottom: 16px;
.pipeline-title {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #fff;
}
.pipeline-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 500;
text-transform: capitalize;
}
}
}
.pipeline-description {
margin-bottom: 16px;
p {
margin: 0;
color: #aaa;
font-size: 0.9rem;
line-height: 1.4;
}
}
.pipeline-details {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 8px;
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
.detail-label {
color: #aaa;
font-size: 0.85rem;
}
.detail-value {
color: #fff;
font-size: 0.85rem;
font-family: 'Courier New', monospace;
}
}
}
.pipeline-actions {
display: flex;
gap: 8px;
.action-btn {
flex: 1;
border: none;
border-radius: 6px;
padding: 8px 12px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s ease;
&.primary {
background: #52c41a;
color: white;
&:hover {
background: #73d13d;
}
}
&.secondary {
background: #faad14;
color: white;
&:hover {
background: #ffc53d;
}
}
&.danger {
background: #ff4d4f;
color: white;
&:hover {
background: #ff7875;
}
}
&:active {
transform: scale(0.95);
}
}
}
}
}
}
// Coming Soon Notice
.coming-soon-notice {
background: linear-gradient(135deg, rgba(0, 169, 157, 0.1), rgba(82, 196, 26, 0.1));
border: 1px solid rgba(0, 169, 157, 0.3);
border-radius: 12px;
padding: 30px;
text-align: center;
margin-top: 20px;
.notice-content {
.notice-icon {
font-size: 3rem;
color: #00a99d;
margin-bottom: 16px;
}
h3 {
margin: 0 0 12px 0;
font-size: 1.5rem;
color: #fff;
}
p {
margin: 0;
color: #aaa;
font-size: 1rem;
line-height: 1.5;
}
}
}
}
// Responsive Design
@media (max-width: 768px) {
.pipelines-container {
padding: 15px;
.pipelines-header {
flex-direction: column;
gap: 20px;
align-items: stretch;
.add-pipeline-btn {
align-self: flex-start;
}
}
.pipeline-stats {
grid-template-columns: repeat(2, 1fr);
}
.pipelines-content .pipelines-grid {
grid-template-columns: 1fr;
}
}
}
@media (max-width: 480px) {
.pipelines-container {
.pipeline-stats {
grid-template-columns: 1fr;
}
.pipeline-card .pipeline-actions {
flex-direction: column;
}
}
}

View File

@ -73,7 +73,10 @@ const UpdateTableModal = ({
name: '', name: '',
description: '', description: '',
tableType: '', tableType: '',
schema: '' schema: '',
tbl: '',
con: '',
sch: ''
}); });
// Dynamic sections state // Dynamic sections state
@ -118,7 +121,10 @@ const UpdateTableModal = ({
name: tableData.name || '', name: tableData.name || '',
description: tableData.description || '', description: tableData.description || '',
tableType: tableData.table_type || '', tableType: tableData.table_type || '',
schema: tableData.schema || '' schema: tableData.schema || '',
tbl: tableData.tbl || tableData.slug || '',
con: tableData.con || 'my_dwh',
sch: tableData.sch || tableData.schema || ''
}); });
// Populate columns // Populate columns
@ -283,7 +289,10 @@ const UpdateTableModal = ({
name: '', name: '',
description: '', description: '',
tableType: '', tableType: '',
schema: '' schema: '',
tbl: '',
con: '',
sch: ''
}); });
setColumns([]); setColumns([]);
setKeys([]); setKeys([]);
@ -469,42 +478,65 @@ const UpdateTableModal = ({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const tableUpdateData = { // Prepare the payload according to the API specification
id: tableData.id, const payload = {
token: "abdhsg",
org: "sN05Pjv11qvH",
con: formData.con || tableData?.con || "my_dwh",
sch: formData.sch || tableData?.sch || formData.schema,
name: formData.name.trim(), name: formData.name.trim(),
description: formData.description.trim(), tbl: formData.tbl || tableData?.tbl || tableData?.slug,
table_type: formData.tableType, table_type: formData.tableType,
schema: formData.schema, description: formData.description.trim(),
columns: columns.map(col => ({ columns: columns.map(col => ({
id: col.id, column_name: col.name.trim(),
name: col.name.trim(),
data_type: col.type, data_type: col.type,
is_primary_key: col.isPrimaryKey,
is_foreign_key: col.isForeignKey,
is_nullable: col.isNullable is_nullable: col.isNullable
})), })),
keys: keys.flatMap(key => keys: keys.map(key => ({
(key.keyColumns || []).map(kc => ({ key_name: key.name.trim(),
name: key.name.trim(),
column_name: columns.find(col => col.id === kc.columnId)?.name || '',
key_type: key.keyType, key_type: key.keyType,
key_columns: (key.keyColumns || []).map(kc => {
const column = columns.find(col => col.id === kc.columnId);
return {
column_name: column ? column.name.trim() : '',
sequence: kc.sequence sequence: kc.sequence
})) };
), }).filter(kc => kc.column_name !== '')
})),
relations: relations.map(rel => ({ relations: relations.map(rel => ({
target_table: rel.targetTable, target_table_name: rel.targetTable,
source_key: rel.sourceKey, source_key: rel.sourceKey,
target_key: rel.targetKey, reference_key: rel.targetKey,
table_key: rel.tableKey, reference_name: rel.tableKey
relation_type: rel.relationType
})) }))
}; };
await onUpdateTable(tableUpdateData); console.log('Update table payload:', payload);
// Call the update API
const response = await fetch('https://sandbox.kezel.io/api/qbt_table_update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.status === 200) {
console.log('Table updated successfully:', result);
await onUpdateTable(payload);
onClose(); onClose();
} else {
console.error('Failed to update table:', result.message);
setErrors({ submit: result.message || 'Failed to update table' });
}
} catch (error) { } catch (error) {
console.error('Error updating table:', error); console.error('Error updating table:', error);
setErrors({ submit: 'Network error occurred while updating table' });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -548,6 +580,11 @@ const UpdateTableModal = ({
<DialogContent sx={{ p: 0, overflow: 'hidden' }}> <DialogContent sx={{ p: 0, overflow: 'hidden' }}>
<Box sx={{ p: 3, height: '100%', overflow: 'auto' }}> <Box sx={{ p: 3, height: '100%', overflow: 'auto' }}>
{errors.submit && (
<Alert severity="error" sx={{ mb: 2 }}>
{errors.submit}
</Alert>
)}
{/* Basic Information */} {/* Basic Information */}
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}> <Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>