Compare commits
No commits in common. "333c3061af663f7f82570f923c163049a699e852" and "5fa97985655e9191c9eb611bdfbe0248056d715c" have entirely different histories.
333c3061af
...
5fa9798565
235
src/App.css
235
src/App.css
|
|
@ -493,238 +493,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
367
src/App.jsx
367
src/App.jsx
|
|
@ -4,7 +4,6 @@ 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'
|
||||||
|
|
@ -37,7 +36,6 @@ 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');
|
||||||
|
|
@ -60,7 +58,6 @@ 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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,7 +98,6 @@ 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);
|
||||||
|
|
@ -121,20 +117,48 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
<div className="app-container">
|
<div className="app-container" style={{
|
||||||
|
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 className="header-logo-container">
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<div className="header-logo">
|
<div style={{
|
||||||
<FaDatabase className="header-logo-icon" />
|
width: '40px',
|
||||||
|
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 className="header-title">
|
<h1 style={{
|
||||||
Qubit Service
|
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
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User profile and controls */}
|
{/* User profile and controls */}
|
||||||
<div className="header-controls">
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<div className="user-profile">
|
<div className="user-profile">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -146,7 +170,7 @@ function App() {
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="user-profile-icon"
|
style={{ marginRight: '8px' }}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
@ -173,119 +197,235 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="main-layout">
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'row', overflow: 'hidden' }}>
|
||||||
{/* Sidebar navigation with dropdown */}
|
{/* Sidebar navigation with dropdown */}
|
||||||
<div className="sidebar-nav">
|
<div className="sidebar-nav" style={{
|
||||||
<div className="sidebar-nav-content">
|
width: '220px',
|
||||||
|
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 className="main-item-icon" />
|
<FaFolder style={{
|
||||||
<span className="main-item-text">
|
fontSize: '20px',
|
||||||
|
color: '#00a99d',
|
||||||
|
marginRight: '12px'
|
||||||
|
}} />
|
||||||
|
<span style={{
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
flex: 1
|
||||||
|
}}>
|
||||||
Qbit
|
Qbit
|
||||||
</span>
|
</span>
|
||||||
{isDropdownOpen ?
|
{isDropdownOpen ?
|
||||||
<FaChevronDown className="dropdown-chevron" /> :
|
<FaChevronDown style={{ color: '#aaa', fontSize: '14px' }} /> :
|
||||||
<FaChevronRight className="dropdown-chevron" />
|
<FaChevronRight style={{ color: '#aaa', fontSize: '14px' }} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dropdown items container with animation */}
|
{/* Dropdown items container with animation */}
|
||||||
<div className={`dropdown-container ${isDropdownOpen ? 'open' : 'closed'}`}>
|
<div
|
||||||
|
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 dropdown-item ${activeTab === 'canvas' ? 'active' : ''}`}
|
className={`nav-button ${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 className={`dropdown-item-icon ${activeTab === 'canvas' ? 'active' : 'inactive'}`} />
|
<FaProjectDiagram style={{
|
||||||
<span className={`dropdown-item-text ${activeTab === 'canvas' ? 'active' : 'inactive'}`}>
|
fontSize: '16px',
|
||||||
Services
|
color: activeTab === 'canvas' ? '#00a99d' : '#aaa',
|
||||||
|
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 dropdown-item ${activeTab === 'er_diagram' ? 'active' : ''}`}
|
className={`nav-button ${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 className={`dropdown-item-icon ${activeTab === 'er_diagram' ? 'active' : 'inactive'}`} />
|
<FaSitemap style={{
|
||||||
<span className={`dropdown-item-text ${activeTab === 'er_diagram' ? 'active' : 'inactive'}`}>
|
fontSize: '16px',
|
||||||
|
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 dropdown-item ${activeTab === 'dataflow' ? 'active' : ''}`}
|
className={`nav-button ${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 className={`dropdown-item-icon ${activeTab === 'dataflow' ? 'active' : 'inactive'}`} />
|
<FaDatabase style={{
|
||||||
<span className={`dropdown-item-text ${activeTab === 'dataflow' ? 'active' : 'inactive'}`}>
|
fontSize: '16px',
|
||||||
Process
|
color: activeTab === 'dataflow' ? '#00a99d' : '#aaa',
|
||||||
|
marginRight: '12px'
|
||||||
|
}} />
|
||||||
|
<span style={{
|
||||||
|
color: activeTab === 'dataflow' ? '#fff' : '#aaa',
|
||||||
|
fontWeight: activeTab === 'dataflow' ? '500' : 'normal'
|
||||||
|
}}>
|
||||||
|
Data Mapping
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Pipelines Item */}
|
{/* Settings Item */}
|
||||||
<div
|
<div
|
||||||
className={`nav-button dropdown-item ${activeTab === 'pipelines' ? 'active' : ''}`}
|
className={`nav-button ${activeTab === 'settings' ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab('pipelines');
|
alert('Settings functionality not implemented yet');
|
||||||
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 className={`dropdown-item-icon ${activeTab === 'pipelines' ? 'active' : 'inactive'}`} />
|
<FaTachometerAlt
|
||||||
<span className={`dropdown-item-text ${activeTab === 'pipelines' ? 'active' : 'inactive'}`}>
|
style={{
|
||||||
Pipelines
|
fontSize: '16px',
|
||||||
|
color: activeTab === 'settings' ? '#00a99d' : '#aaa',
|
||||||
|
marginRight: '12px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{
|
||||||
|
color: activeTab === 'settings' ? '#fff' : '#aaa',
|
||||||
|
fontWeight: activeTab === 'settings' ? '500' : 'normal'
|
||||||
|
}}>
|
||||||
|
Dashboard
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Triggers Item */}
|
{/* Advanced Charts Item */}
|
||||||
<div
|
{/* <div
|
||||||
className={`nav-button dropdown-item ${activeTab === 'triggers' ? 'active' : ''}`}
|
className={`nav-button ${activeTab === 'charts' ? 'active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
alert('Triggers functionality not implemented yet');
|
setActiveTab('charts');
|
||||||
|
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'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaTachometerAlt className={`dropdown-item-icon ${activeTab === 'triggers' ? 'active' : 'inactive'}`} />
|
<FaChartBar style={{
|
||||||
<span className={`dropdown-item-text ${activeTab === 'triggers' ? 'active' : 'inactive'}`}>
|
fontSize: '16px',
|
||||||
Triggers
|
color: activeTab === 'charts' ? '#00a99d' : '#aaa',
|
||||||
|
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 className="main-content">
|
<main style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '20px'
|
||||||
|
}}>
|
||||||
{activeTab === 'canvas' && (
|
{activeTab === 'canvas' && (
|
||||||
<>
|
<>
|
||||||
<div className="page-header">
|
<div style={{
|
||||||
<h1 className="page-title">
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
<h1 style={{
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: '600',
|
||||||
|
margin: '0 0 8px 0',
|
||||||
|
color: '#ffffff'
|
||||||
|
}}>
|
||||||
Overview
|
Overview
|
||||||
</h1>
|
</h1>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
|
|
@ -315,18 +455,56 @@ function App() {
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
<InfiniteCanvas />
|
<InfiniteCanvas />
|
||||||
<div className="info-panel canvas">
|
<div style={{
|
||||||
<p className="info-panel-main">
|
position: 'absolute',
|
||||||
<span className="info-panel-indicator canvas"></span>
|
bottom: '20px',
|
||||||
|
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 className="info-panel-sub">
|
<p style={{
|
||||||
|
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' && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -335,12 +513,44 @@ function App() {
|
||||||
dbSlug={currentDbSlug}
|
dbSlug={currentDbSlug}
|
||||||
hasSchemas={hasSchemas}
|
hasSchemas={hasSchemas}
|
||||||
/>
|
/>
|
||||||
<div className="info-panel dataflow">
|
<div style={{
|
||||||
<p className="info-panel-main">
|
position: 'absolute',
|
||||||
<span className="info-panel-indicator dataflow"></span>
|
bottom: '20px',
|
||||||
|
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 className="info-panel-sub">
|
<p style={{
|
||||||
|
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>
|
||||||
|
|
@ -349,18 +559,21 @@ function App() {
|
||||||
|
|
||||||
{activeTab === 'er_diagram' && (
|
{activeTab === 'er_diagram' && (
|
||||||
<>
|
<>
|
||||||
<div className="page-header">
|
<div style={{
|
||||||
<h1 className="page-title">
|
marginBottom: '20px'
|
||||||
|
}}>
|
||||||
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -1150,7 +1150,20 @@ 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)}
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,11 @@ 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';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,20 +69,8 @@ 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>
|
|
||||||
{/* <button
|
|
||||||
className="er-table-update-btn"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
data.onUpdateTable && data.onUpdateTable(data);
|
|
||||||
}}
|
|
||||||
title="Update Table"
|
|
||||||
>
|
|
||||||
<FaEdit />
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="er-column-list">
|
<ul className="er-column-list">
|
||||||
|
|
@ -584,10 +570,6 @@ 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";
|
||||||
|
|
@ -1277,40 +1259,6 @@ 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...');
|
||||||
|
|
@ -1577,7 +1525,6 @@ 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,
|
||||||
|
|
@ -1927,20 +1874,6 @@ const ERDiagramCanvasContent = () => {
|
||||||
existingTables={existingTables}
|
existingTables={existingTables}
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
justify-content: space-between;
|
gap: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
|
@ -215,38 +215,6 @@
|
||||||
&.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 {
|
||||||
|
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -73,10 +73,7 @@ const UpdateTableModal = ({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
tableType: '',
|
tableType: '',
|
||||||
schema: '',
|
schema: ''
|
||||||
tbl: '',
|
|
||||||
con: '',
|
|
||||||
sch: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dynamic sections state
|
// Dynamic sections state
|
||||||
|
|
@ -121,10 +118,7 @@ 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
|
||||||
|
|
@ -289,10 +283,7 @@ const UpdateTableModal = ({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
tableType: '',
|
tableType: '',
|
||||||
schema: '',
|
schema: ''
|
||||||
tbl: '',
|
|
||||||
con: '',
|
|
||||||
sch: ''
|
|
||||||
});
|
});
|
||||||
setColumns([]);
|
setColumns([]);
|
||||||
setKeys([]);
|
setKeys([]);
|
||||||
|
|
@ -478,65 +469,42 @@ const UpdateTableModal = ({
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
// Prepare the payload according to the API specification
|
const tableUpdateData = {
|
||||||
const payload = {
|
id: tableData.id,
|
||||||
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(),
|
||||||
tbl: formData.tbl || tableData?.tbl || tableData?.slug,
|
|
||||||
table_type: formData.tableType,
|
|
||||||
description: formData.description.trim(),
|
description: formData.description.trim(),
|
||||||
|
table_type: formData.tableType,
|
||||||
|
schema: formData.schema,
|
||||||
columns: columns.map(col => ({
|
columns: columns.map(col => ({
|
||||||
column_name: col.name.trim(),
|
id: col.id,
|
||||||
|
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.map(key => ({
|
keys: keys.flatMap(key =>
|
||||||
key_name: key.name.trim(),
|
(key.keyColumns || []).map(kc => ({
|
||||||
key_type: key.keyType,
|
name: key.name.trim(),
|
||||||
key_columns: (key.keyColumns || []).map(kc => {
|
column_name: columns.find(col => col.id === kc.columnId)?.name || '',
|
||||||
const column = columns.find(col => col.id === kc.columnId);
|
key_type: key.keyType,
|
||||||
return {
|
sequence: kc.sequence
|
||||||
column_name: column ? column.name.trim() : '',
|
}))
|
||||||
sequence: kc.sequence
|
),
|
||||||
};
|
|
||||||
}).filter(kc => kc.column_name !== '')
|
|
||||||
})),
|
|
||||||
relations: relations.map(rel => ({
|
relations: relations.map(rel => ({
|
||||||
target_table_name: rel.targetTable,
|
target_table: rel.targetTable,
|
||||||
source_key: rel.sourceKey,
|
source_key: rel.sourceKey,
|
||||||
reference_key: rel.targetKey,
|
target_key: rel.targetKey,
|
||||||
reference_name: rel.tableKey
|
table_key: rel.tableKey,
|
||||||
|
relation_type: rel.relationType
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Update table payload:', payload);
|
await onUpdateTable(tableUpdateData);
|
||||||
|
onClose();
|
||||||
// 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();
|
|
||||||
} 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);
|
||||||
}
|
}
|
||||||
|
|
@ -580,11 +548,6 @@ 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 }}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue