initial commit
This commit is contained in:
commit
d1d7f0fbc1
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Snowflake Data Warehouse Dashboard
|
||||
|
||||
An interactive visualization tool for exploring Snowflake data warehouse structures, including databases, schemas, and tables (both fact and dimension).
|
||||
|
||||
## Features
|
||||
|
||||
- **True Infinite Canvas**: Unlimited zooming and panning capabilities for exploring vast data structures
|
||||
- **Interactive Exploration**: Click to expand/collapse databases and schemas
|
||||
- **Table Details**: View detailed information about fact and dimension tables
|
||||
- **Visual Distinction**: Different colors for databases, schemas, fact tables, and dimension tables
|
||||
- **Relationship Visualization**: Animated connections between related elements
|
||||
- **Dynamic Navigation**: Zoom controls and minimap for easy navigation of the infinite space
|
||||
- **Custom Connections**: Create and manage custom connections between any elements (databases, schemas, tables)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
This dashboard is built using:
|
||||
|
||||
- React for the UI components
|
||||
- ReactFlow for the interactive graph visualization
|
||||
- CSS for styling and visual hierarchy
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Exploring the Data Warehouse**:
|
||||
- Click on a database node to expand and see its schemas
|
||||
- Click on a schema node to expand and see its tables
|
||||
- Click on a table node to view detailed information
|
||||
|
||||
2. **Infinite Canvas Navigation**:
|
||||
- Use the mouse wheel to zoom in/out without limits
|
||||
- Click and drag to pan infinitely in any direction
|
||||
- Use the minimap in the bottom-right for quick navigation
|
||||
- Use the dedicated "Zoom Out" button to quickly get a broader view
|
||||
- Click "Reset View" to return to the initial state
|
||||
- Current zoom level is displayed in the control panel
|
||||
|
||||
3. **Creating Custom Connections**:
|
||||
- Click "Start Connection Mode" to enter connection mode
|
||||
- Choose connection type: Reference, Dependency, or Custom
|
||||
- Drag from a connection point (dot) on any node
|
||||
- Drop onto a connection point on another node to create the connection
|
||||
- View and manage all custom connections in the bottom-left panel
|
||||
- Delete connections by clicking the "✕" button
|
||||
- Connection points appear on all four sides of each node
|
||||
|
||||
## Data Structure
|
||||
|
||||
The dashboard visualizes:
|
||||
|
||||
- **Databases**: Top-level containers (e.g., Sales DW, Marketing DW)
|
||||
- **Schemas**: Logical groupings of tables within a database
|
||||
- **Fact Tables**: Tables containing measurable, quantitative data about business events
|
||||
- **Dimension Tables**: Tables containing descriptive attributes used for analysis
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Add search functionality to quickly find specific elements
|
||||
- Implement filtering options for different table types
|
||||
- Add ability to save and share specific views
|
||||
- Integrate with real Snowflake data sources via API
|
||||
- Add SQL query generation based on selected tables
|
||||
|
||||
## Development
|
||||
|
||||
To run the project locally:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:5173/
|
||||
|
||||
---
|
||||
|
||||
This project was bootstrapped with [Vite](https://vitejs.dev/) and uses [React](https://reactjs.org/) for the UI.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
<!-- Add jQuery before any other scripts -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "qbx",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npm run copy-assets && vite",
|
||||
"build": "npm run copy-assets && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"copy-luckysheet-assets": "if not exist public\\luckysheet\\css mkdir public\\luckysheet\\css && if not exist public\\luckysheet\\plugins\\css mkdir public\\luckysheet\\plugins\\css && copy node_modules\\luckysheet\\dist\\css\\luckysheet.css public\\luckysheet\\css\\ && copy node_modules\\luckysheet\\dist\\plugins\\css\\pluginsCss.css public\\luckysheet\\plugins\\css\\",
|
||||
"copy-univerjs-assets": "if not exist public\\univerjs\\css mkdir public\\univerjs\\css && copy node_modules\\@univerjs\\presets\\lib\\styles\\preset-sheets-core.css public\\univerjs\\css\\ && copy node_modules\\@univerjs-pro\\sheets-pivot-ui\\lib\\index.css public\\univerjs\\css\\sheets-pivot-ui.css",
|
||||
"copy-assets": "npm run copy-luckysheet-assets && npm run copy-univerjs-assets"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs-pro/engine-pivot": "^0.7.0",
|
||||
"@univerjs-pro/sheets-pivot": "^0.7.0",
|
||||
"@univerjs-pro/sheets-pivot-ui": "^0.7.0",
|
||||
"@univerjs/core": "^0.7.0",
|
||||
"@univerjs/preset-sheets-core": "^0.7.0",
|
||||
"@univerjs/presets": "^0.7.0",
|
||||
"d3": "^7.9.0",
|
||||
"luckysheet": "^2.1.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-flow-renderer": "^10.3.17",
|
||||
"react-icons": "^5.5.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^2.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,417 @@
|
|||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* ReactFlow specific styles */
|
||||
.react-flow__node {
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
text-align: left;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.react-flow__node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.react-flow__node-database {
|
||||
background: #e6f7ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.react-flow__node-schema {
|
||||
background: #f6ffed;
|
||||
border-color: #52c41a;
|
||||
}
|
||||
|
||||
.react-flow__node-table.fact {
|
||||
background: #fff1f0;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.react-flow__node-table.dimension {
|
||||
background: #f9f0ff;
|
||||
border-color: #722ed1;
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
stroke: #1890ff;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.react-flow__edge.animated path {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw 0.5s linear infinite;
|
||||
}
|
||||
|
||||
/* Enhanced infinite canvas styles */
|
||||
.react-flow__pane {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.react-flow__pane.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.15);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
background: white;
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.react-flow__minimap {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Connection mode animation */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 10px rgba(82, 196, 26, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 0 0 0 rgba(82, 196, 26, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom edge styles */
|
||||
.react-flow__edge.reference path {
|
||||
stroke: #00a99d;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.dependency path {
|
||||
stroke: #ff4d4f;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.default path {
|
||||
stroke: #722ed1;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* Connection handle styles */
|
||||
.react-flow__handle {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
transform: scale(1.3);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.react-flow__handle-top {
|
||||
top: -6px;
|
||||
}
|
||||
|
||||
.react-flow__handle-right {
|
||||
right: -6px;
|
||||
}
|
||||
|
||||
.react-flow__handle-bottom {
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
.react-flow__handle-left {
|
||||
left: -6px;
|
||||
}
|
||||
|
||||
/* Connection line styles */
|
||||
.react-flow__connection-path {
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
/* Make nodes stand out when in connection mode */
|
||||
.react-flow.connection-mode .react-flow__node {
|
||||
filter: drop-shadow(0 0 8px rgba(24, 144, 255, 0.5));
|
||||
}
|
||||
|
||||
@keyframes dashdraw {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom node styles */
|
||||
.database-node, .schema-node, .table-node {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.database-node:hover, .schema-node:hover, .table-node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* App layout styles */
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
background: #121212;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #1a1a1a;
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 15px rgba(0,0,0,0.5);
|
||||
z-index: 10;
|
||||
border-bottom: 1px solid #333;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated gradient border for header */
|
||||
header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #00a99d, #52c41a, #fa8c16, #722ed1, #00a99d);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient-shift 8s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
aside {
|
||||
width: 250px;
|
||||
background: #1a1a1a;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #333;
|
||||
overflow-y: auto;
|
||||
box-shadow: inset -5px 0 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Animated hover effects for sidebar items */
|
||||
aside li {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
aside li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: all 0.5s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
aside li:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
aside li:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
transform: translateX(5px);
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #1a1a1a;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
||||
color: #ffffff;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
/* Dark theme for ReactFlow */
|
||||
.react-flow__controls {
|
||||
background: #1a1a1a;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.react-flow__controls-button {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.react-flow__controls-button:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.react-flow__controls-button svg {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.react-flow__minimap {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.react-flow__attribution {
|
||||
background: rgba(26, 26, 26, 0.8);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Glowing effect for nodes on hover */
|
||||
.react-flow__node:hover {
|
||||
box-shadow: 0 0 15px rgba(82, 196, 26, 0.6);
|
||||
}
|
||||
|
||||
/* Animated background for the canvas */
|
||||
.react-flow__background {
|
||||
animation: bg-pulse 10s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes bg-pulse {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animated connection lines */
|
||||
.react-flow__edge path {
|
||||
stroke-dasharray: 5;
|
||||
/* animation: flow-line 30s linear infinite; */
|
||||
}
|
||||
|
||||
@keyframes flow-line {
|
||||
to {
|
||||
stroke-dashoffset: -1000;
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation item hover effects */
|
||||
.nav-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #00a99d, #52c41a);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-item:hover::after {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Glow effect for active elements */
|
||||
.glow-effect {
|
||||
box-shadow: 0 0 15px rgba(82, 196, 26, 0.6);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glow-effect:hover {
|
||||
box-shadow: 0 0 20px rgba(82, 196, 26, 0.8);
|
||||
}
|
||||
|
||||
/* Fade in animation */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
import { useState } from 'react'
|
||||
import './App.css'
|
||||
import InfiniteCanvas from './components/InfiniteCanvas'
|
||||
import AdvancedCharts from './components/AdvancedCharts'
|
||||
import DataflowCanvas from './components/DataflowCanvas'
|
||||
import { FaDatabase, FaChartBar, FaProjectDiagram } from 'react-icons/fa'
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('canvas'); // 'canvas', 'charts', or 'dataflow'
|
||||
|
||||
return (
|
||||
<div className="app-container" style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
background: '#121212', /* Dark background */
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
<header>
|
||||
{/* Logo and Title */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
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>
|
||||
<h1 style={{
|
||||
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
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
marginRight: '25px',
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
background: activeTab === 'canvas' ? 'rgba(0, 169, 157, 0.2)' : 'transparent',
|
||||
color: activeTab === 'canvas' ? '#00a99d' : '#ffffff'
|
||||
}}
|
||||
className="nav-item"
|
||||
onClick={() => setActiveTab('canvas')}
|
||||
>
|
||||
Data Explorer
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginRight: '25px',
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
background: activeTab === 'charts' ? 'rgba(0, 169, 157, 0.2)' : 'transparent',
|
||||
color: activeTab === 'charts' ? '#00a99d' : '#ffffff'
|
||||
}}
|
||||
className="nav-item"
|
||||
onClick={() => setActiveTab('charts')}
|
||||
>
|
||||
Advanced Charts
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginRight: '25px',
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
background: activeTab === 'dataflow' ? 'rgba(0, 169, 157, 0.2)' : 'transparent',
|
||||
color: activeTab === 'dataflow' ? '#00a99d' : '#ffffff'
|
||||
}}
|
||||
className="nav-item"
|
||||
onClick={() => setActiveTab('dataflow')}
|
||||
>
|
||||
Data Flow
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative'
|
||||
}}
|
||||
className="nav-item"
|
||||
>
|
||||
Settings
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
<aside>
|
||||
<h3 style={{
|
||||
color: '#52c41a',
|
||||
marginBottom: '15px',
|
||||
borderBottom: '1px solid #333',
|
||||
paddingBottom: '10px',
|
||||
fontSize: '1.1rem'
|
||||
}}>Data Sources</h3>
|
||||
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
<li style={{
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
color: '#ffffff',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '6px',
|
||||
background: 'rgba(0, 169, 157, 0.1)',
|
||||
marginBottom: '8px',
|
||||
border: '1px solid rgba(0, 169, 157, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 0 10px rgba(0, 169, 157, 0.1)'
|
||||
}} className="glow-effect">
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
background: 'linear-gradient(45deg, #00a99d, #52c41a)',
|
||||
borderRadius: '4px',
|
||||
color: 'white',
|
||||
marginRight: '10px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<FaDatabase />
|
||||
</span>
|
||||
Dbtez
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 style={{
|
||||
color: '#fa8c16',
|
||||
marginTop: '25px',
|
||||
marginBottom: '15px',
|
||||
borderBottom: '1px solid #333',
|
||||
paddingBottom: '10px',
|
||||
fontSize: '1.1rem'
|
||||
}}>Saved Views</h3>
|
||||
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
<li style={{
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
marginBottom: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.03)'
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#fa8c16',
|
||||
marginRight: '8px',
|
||||
fontSize: '16px'
|
||||
}}>📊</span>
|
||||
Sales Overview
|
||||
</li>
|
||||
<li style={{
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
marginBottom: '8px',
|
||||
background: 'rgba(255, 255, 255, 0.03)'
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#fa8c16',
|
||||
marginRight: '8px',
|
||||
fontSize: '16px'
|
||||
}}>📊</span>
|
||||
Marketing Performance
|
||||
</li>
|
||||
<li style={{
|
||||
padding: '10px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
transition: 'all 0.3s ease',
|
||||
background: 'rgba(255, 255, 255, 0.03)'
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: '#fa8c16',
|
||||
marginRight: '8px',
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>📊</span>
|
||||
Financial Summary
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||||
{activeTab === 'canvas' && (
|
||||
<>
|
||||
<InfiniteCanvas />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
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.
|
||||
</p>
|
||||
<p style={{
|
||||
margin: '5px 0 0 16px',
|
||||
fontSize: '11px',
|
||||
opacity: 0.7
|
||||
}}>
|
||||
Scroll to zoom • Drag to pan • Connect nodes
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'charts' && (
|
||||
<div style={{ padding: '20px', height: '100%', overflow: 'auto' }}>
|
||||
<AdvancedCharts />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dataflow' && (
|
||||
<>
|
||||
<DataflowCanvas />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
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
|
||||
</p>
|
||||
<p style={{
|
||||
margin: '5px 0 0 16px',
|
||||
fontSize: '11px',
|
||||
opacity: 0.7
|
||||
}}>
|
||||
Add tables • Create processes • Connect data flows
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,312 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
ComposedChart, Line, Area, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
|
||||
ResponsiveContainer, ScatterChart, Scatter, ZAxis, Treemap, AreaChart,
|
||||
Cell, LabelList
|
||||
} from 'recharts';
|
||||
import BasicUniverSheet from './BasicUniverSheet';
|
||||
import UniverSheet from './UniverSheet';
|
||||
import UniverSheetComponent from './UniverSheetComponent';
|
||||
|
||||
// Sample data for the charts
|
||||
const combinationData = [
|
||||
{ name: 'Jan', revenue: 4000, profit: 2400, expenses: 2400 },
|
||||
{ name: 'Feb', revenue: 3000, profit: 1398, expenses: 2210 },
|
||||
{ name: 'Mar', revenue: 2000, profit: 9800, expenses: 2290 },
|
||||
{ name: 'Apr', revenue: 2780, profit: 3908, expenses: 2000 },
|
||||
{ name: 'May', revenue: 1890, profit: 4800, expenses: 2181 },
|
||||
{ name: 'Jun', revenue: 2390, profit: 3800, expenses: 2500 },
|
||||
{ name: 'Jul', revenue: 3490, profit: 4300, expenses: 2100 },
|
||||
];
|
||||
|
||||
const scatterData = [
|
||||
{ x: 100, y: 200, z: 200, name: 'Product A' },
|
||||
{ x: 120, y: 100, z: 260, name: 'Product B' },
|
||||
{ x: 170, y: 300, z: 400, name: 'Product C' },
|
||||
{ x: 140, y: 250, z: 280, name: 'Product D' },
|
||||
{ x: 150, y: 400, z: 500, name: 'Product E' },
|
||||
{ x: 110, y: 280, z: 200, name: 'Product F' },
|
||||
];
|
||||
|
||||
const treemapData = [
|
||||
{
|
||||
name: 'Sales',
|
||||
children: [
|
||||
{ name: 'North America', size: 4000, fill: '#8884d8' },
|
||||
{ name: 'Europe', size: 3000, fill: '#83a6ed' },
|
||||
{ name: 'Asia', size: 2000, fill: '#8dd1e1' },
|
||||
{ name: 'Africa', size: 1000, fill: '#82ca9d' },
|
||||
{ name: 'South America', size: 1500, fill: '#a4de6c' },
|
||||
{ name: 'Australia', size: 800, fill: '#d0ed57' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const stackedAreaData = [
|
||||
{ name: 'Jan', product1: 400, product2: 240, product3: 240 },
|
||||
{ name: 'Feb', product1: 300, product2: 139, product3: 221 },
|
||||
{ name: 'Mar', product1: 200, product2: 980, product3: 229 },
|
||||
{ name: 'Apr', product1: 278, product2: 390, product3: 200 },
|
||||
{ name: 'May', product1: 189, product2: 480, product3: 218 },
|
||||
{ name: 'Jun', product1: 239, product2: 380, product3: 250 },
|
||||
{ name: 'Jul', product1: 349, product2: 430, product3: 210 },
|
||||
];
|
||||
|
||||
// Custom tooltip for the treemap
|
||||
const CustomizedContent = (props) => {
|
||||
const { root, depth, x, y, width, height, index, name, size } = props;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
fill: depth < 2 ? props.fill : '#ffffff',
|
||||
stroke: '#fff',
|
||||
strokeWidth: 2 / (depth + 1e-10),
|
||||
strokeOpacity: 1 / (depth + 1e-10),
|
||||
}}
|
||||
/>
|
||||
{depth === 1 ? (
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y + height / 2 + 7}
|
||||
textAnchor="middle"
|
||||
fill="#fff"
|
||||
fontSize={14}
|
||||
>
|
||||
{name}
|
||||
</text>
|
||||
) : null}
|
||||
{depth === 1 ? (
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y + height / 2 - 7}
|
||||
textAnchor="middle"
|
||||
fill="#fff"
|
||||
fontSize={14}
|
||||
>
|
||||
{size}
|
||||
</text>
|
||||
) : null}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const AdvancedCharts = () => {
|
||||
const [activeChart, setActiveChart] = useState('combination');
|
||||
|
||||
const renderChart = () => {
|
||||
switch (activeChart) {
|
||||
case 'combination':
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h3>Combination Chart (Bar, Line, Area)</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ComposedChart
|
||||
data={combinationData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="name" scale="band" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area type="monotone" dataKey="revenue" fill="#8884d8" stroke="#8884d8" />
|
||||
<Bar dataKey="expenses" barSize={20} fill="#413ea0" />
|
||||
<Line type="monotone" dataKey="profit" stroke="#ff7300" />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
{/* <PivotSheet />
|
||||
<UniverSheetComponent /> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'scatter':
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h3>Scatter/Bubble Chart</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<ScatterChart
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid />
|
||||
<XAxis type="number" dataKey="x" name="Sales" unit="K" />
|
||||
<YAxis type="number" dataKey="y" name="Profit" unit="K" />
|
||||
<ZAxis type="number" dataKey="z" range={[60, 400]} name="Volume" unit="K" />
|
||||
<Tooltip cursor={{ strokeDasharray: '3 3' }} />
|
||||
<Legend />
|
||||
<Scatter name="Products" data={scatterData} fill="#8884d8">
|
||||
<LabelList dataKey="name" position="top" />
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'treemap':
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h3>Treemap Chart</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<Treemap
|
||||
data={treemapData}
|
||||
dataKey="size"
|
||||
ratio={4/3}
|
||||
stroke="#fff"
|
||||
fill="#8884d8"
|
||||
content={<CustomizedContent />}
|
||||
>
|
||||
{
|
||||
treemapData[0].children.map((item, index) => (
|
||||
<Cell key={`cell-${index}`} fill={item.fill} />
|
||||
))
|
||||
}
|
||||
</Treemap>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'stacked-area':
|
||||
return (
|
||||
<div className="chart-container">
|
||||
<h3>Stacked Area Chart</h3>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<AreaChart
|
||||
data={stackedAreaData}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area type="monotone" dataKey="product1" stackId="1" stroke="#8884d8" fill="#8884d8" />
|
||||
<Area type="monotone" dataKey="product2" stackId="1" stroke="#82ca9d" fill="#82ca9d" />
|
||||
<Area type="monotone" dataKey="product3" stackId="1" stroke="#ffc658" fill="#ffc658" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div>Select a chart type</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#121212',
|
||||
color: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)'
|
||||
}}>
|
||||
<h2 style={{
|
||||
color: '#00a99d',
|
||||
borderBottom: '1px solid #333',
|
||||
paddingBottom: '10px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
Advanced Data Visualization
|
||||
</h2>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
marginBottom: '20px',
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setActiveChart('combination')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: activeChart === 'combination' ? '#00a99d' : '#333',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Combination Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveChart('scatter')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: activeChart === 'scatter' ? '#00a99d' : '#333',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Scatter/Bubble Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveChart('treemap')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: activeChart === 'treemap' ? '#00a99d' : '#333',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Treemap Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveChart('stacked-area')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: activeChart === 'stacked-area' ? '#00a99d' : '#333',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
Stacked Area Chart
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderChart()}
|
||||
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
padding: '15px',
|
||||
background: '#1a1a1a',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<h4 style={{ color: '#00a99d', marginTop: 0 }}>Chart Description</h4>
|
||||
{activeChart === 'combination' && (
|
||||
<p>Combination charts display multiple data series using different visualization types (bars, lines, areas) in a single chart. They're ideal for showing relationships between different metrics with varying scales or units.</p>
|
||||
)}
|
||||
{activeChart === 'scatter' && (
|
||||
<p>Scatter/bubble charts plot individual data points on a two-dimensional graph, with an optional third dimension represented by the size of each bubble. They're perfect for identifying correlations, clusters, and outliers in your data.</p>
|
||||
)}
|
||||
{activeChart === 'treemap' && (
|
||||
<p>Treemaps display hierarchical data as nested rectangles, with the size of each rectangle proportional to its value. They efficiently visualize the composition of a whole and the relative sizes of its parts.</p>
|
||||
)}
|
||||
{activeChart === 'stacked-area' && (
|
||||
<p>Stacked area charts show how multiple data series contribute to a total over time. They're excellent for visualizing part-to-whole relationships and how the composition of a total changes across a continuous variable like time.</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: '30px', borderTop: '1px solid #333', paddingTop: '20px' }}>
|
||||
<h3 style={{ color: '#00a99d', marginBottom: '15px' }}>Univer Spreadsheet Component</h3>
|
||||
<UniverSheetComponent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedCharts;
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createUniver, defaultTheme, LocaleType } from '@univerjs/presets';
|
||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||
|
||||
// Load CSS dynamically to avoid import errors
|
||||
const loadCSS = (url) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
const BasicUniverSheet = () => {
|
||||
const containerRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('BasicUniverSheet mounting, container ref:', containerRef.current);
|
||||
|
||||
// Load CSS files
|
||||
loadCSS('/univerjs/css/preset-sheets-core.css');
|
||||
|
||||
try {
|
||||
// Create the univer instance with minimal configuration
|
||||
const univer = createUniver({
|
||||
theme: defaultTheme,
|
||||
locale: LocaleType.EN_US,
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: containerRef.current,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
console.log('Univer created successfully');
|
||||
|
||||
// Set loading to false after a short delay
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
if (typeof univer.dispose === 'function') {
|
||||
univer.dispose();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error disposing univer:', e);
|
||||
}
|
||||
console.log('BasicUniverSheet unmounting');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error initializing Univer:', error);
|
||||
setError(`Error initializing Univer: ${error.message}`);
|
||||
setLoading(false);
|
||||
return () => {
|
||||
console.log('BasicUniverSheet unmounting (after error)');
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '600px' }}>
|
||||
{error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255, 77, 79, 0.1)',
|
||||
border: '1px solid #ff4d4f',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
color: '#ff4d4f',
|
||||
zIndex: 10
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
zIndex: 5
|
||||
}}>
|
||||
Loading Univer Spreadsheet...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
background: '#fff'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicUniverSheet;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// components/Canvas.js
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ReactFlow, { MiniMap, Controls } from 'react-flow-renderer';
|
||||
|
||||
|
||||
|
||||
const initialElements = [
|
||||
|
||||
{ id: '1', data: { label: 'Database 1' }, position: { x: 250, y: 5 } },
|
||||
|
||||
{ id: '2', data: { label: 'Schema A' }, position: { x: 100, y: 100 } },
|
||||
|
||||
{ id: '3', data: { label: 'Table 1' }, position: { x: 400, y: 100 } },
|
||||
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
|
||||
{ id: 'e1-3', source: '1', target: '3', animated: true },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const Canvas = () => {
|
||||
|
||||
return (
|
||||
|
||||
<div style={{ height: '100vh' }}>
|
||||
|
||||
<ReactFlow elements={initialElements} snapToGrid={true}>
|
||||
|
||||
<MiniMap />
|
||||
|
||||
<Controls />
|
||||
|
||||
</ReactFlow>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default Canvas;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,222 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createUniver, defaultTheme, LocaleType, merge } from '@univerjs/presets';
|
||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
||||
|
||||
// Load CSS dynamically to avoid import errors
|
||||
const loadCSS = (url) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
const SimpleUniverSheet = () => {
|
||||
const containerRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('SimpleUniverSheet mounting, container ref:', containerRef.current);
|
||||
|
||||
// Load CSS files
|
||||
loadCSS('/univerjs/css/preset-sheets-core.css');
|
||||
|
||||
try {
|
||||
// Create the univer instance
|
||||
const univer = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
|
||||
},
|
||||
theme: defaultTheme,
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: containerRef.current,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Log available methods on the univer object
|
||||
console.log('Univer object methods:', Object.getOwnPropertyNames(univer));
|
||||
|
||||
// Create a workbook with sample data - not using Promise since it doesn't return one
|
||||
try {
|
||||
// Try different ways to create a workbook
|
||||
if (typeof univer.createWorkbook === 'function') {
|
||||
univer.createWorkbook({ name: 'Simple Sheet Demo' });
|
||||
} else if (univer.univerAPI && typeof univer.univerAPI.createWorkbook === 'function') {
|
||||
univer.univerAPI.createWorkbook({ name: 'Simple Sheet Demo' });
|
||||
} else {
|
||||
console.warn('No createWorkbook method found');
|
||||
}
|
||||
|
||||
console.log('Workbook created');
|
||||
|
||||
// Get the active workbook - try different approaches
|
||||
let workbook;
|
||||
if (typeof univer.getActiveWorkbook === 'function') {
|
||||
workbook = univer.getActiveWorkbook();
|
||||
} else if (univer.univerAPI && typeof univer.univerAPI.getActiveWorkbook === 'function') {
|
||||
workbook = univer.univerAPI.getActiveWorkbook();
|
||||
} else {
|
||||
console.warn('No getActiveWorkbook method found');
|
||||
}
|
||||
if (!workbook) {
|
||||
throw new Error('No active workbook found');
|
||||
}
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) {
|
||||
throw new Error('No active sheet found');
|
||||
}
|
||||
|
||||
console.log('Got workbook and sheet:', workbook, worksheet);
|
||||
|
||||
// Sample data
|
||||
const data = [
|
||||
['Region', 'Product', 'Sales'],
|
||||
['North', 'Apples', 120],
|
||||
['South', 'Oranges', 150],
|
||||
['East', 'Apples', 180],
|
||||
['West', 'Oranges', 100],
|
||||
];
|
||||
|
||||
// Try to set values directly on the worksheet
|
||||
try {
|
||||
console.log('Setting values directly on worksheet');
|
||||
|
||||
// Check if the worksheet has a setCellValue method
|
||||
if (typeof worksheet.setCellValue === 'function') {
|
||||
// Set values one by one using for loops
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
const row = data[rowIndex];
|
||||
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
||||
try {
|
||||
worksheet.setCellValue(rowIndex, colIndex, row[colIndex]);
|
||||
} catch (e) {
|
||||
console.warn(`Error setting cell value at [${rowIndex}, ${colIndex}]:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (workbook.commandManager && typeof workbook.commandManager.executeCommand === 'function') {
|
||||
// Try using the workbook's command manager if available
|
||||
console.log('Using workbook command manager');
|
||||
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||
const row = data[rowIndex];
|
||||
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
||||
try {
|
||||
workbook.commandManager.executeCommand('sheet.command.setRangeValues', {
|
||||
range: {
|
||||
startRow: rowIndex,
|
||||
startColumn: colIndex,
|
||||
endRow: rowIndex,
|
||||
endColumn: colIndex,
|
||||
},
|
||||
value: row[colIndex],
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Error setting cell value at [${rowIndex}, ${colIndex}]:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no method is available, just log a message
|
||||
console.warn('No method available to set cell values');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error setting values:', e);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error setting up workbook/sheet:', error);
|
||||
setError(`Error setting up workbook/sheet: ${error.message}`);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// Set loading to false after a short delay to ensure the UI has time to render
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
// Try different ways to dispose
|
||||
if (typeof univer.dispose === 'function') {
|
||||
univer.dispose();
|
||||
} else if (univer.univerAPI && typeof univer.univerAPI.dispose === 'function') {
|
||||
univer.univerAPI.dispose();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error disposing UniverSheet:', e);
|
||||
}
|
||||
console.log('SimpleUniverSheet unmounting');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error initializing Univer:', error);
|
||||
setError(`Error initializing Univer: ${error.message}`);
|
||||
setLoading(false);
|
||||
return () => {
|
||||
console.log('SimpleUniverSheet unmounting (after error)');
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '600px' }}>
|
||||
{error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255, 77, 79, 0.1)',
|
||||
border: '1px solid #ff4d4f',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
color: '#ff4d4f',
|
||||
zIndex: 10
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
zIndex: 5
|
||||
}}>
|
||||
Loading SimpleUniverSheet...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
background: '#fff'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimpleUniverSheet;
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaPlus, FaTimes, FaTable, FaDatabase, FaLayerGroup, FaBuilding } from 'react-icons/fa';
|
||||
import { HiOutlineDocumentReport } from 'react-icons/hi';
|
||||
|
||||
const TableCreationPopup = ({ onClose, onCreateTable }) => {
|
||||
const [tableName, setTableName] = useState('');
|
||||
const [tableType, setTableType] = useState('stage');
|
||||
const [columns, setColumns] = useState([{ name: '', type: 'string' }]);
|
||||
const [newColumnName, setNewColumnName] = useState('');
|
||||
const [newColumnType, setNewColumnType] = useState('string');
|
||||
|
||||
const handleAddColumn = () => {
|
||||
if (newColumnName.trim() === '') return;
|
||||
|
||||
setColumns([...columns, { name: newColumnName, type: newColumnType }]);
|
||||
setNewColumnName('');
|
||||
setNewColumnType('string');
|
||||
};
|
||||
|
||||
const handleRemoveColumn = (index) => {
|
||||
const newColumns = [...columns];
|
||||
newColumns.splice(index, 1);
|
||||
setColumns(newColumns);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (tableName.trim() === '') return;
|
||||
|
||||
// Filter out empty columns
|
||||
const validColumns = columns.filter(col => col.name.trim() !== '');
|
||||
|
||||
onCreateTable({
|
||||
name: tableName,
|
||||
type: tableType,
|
||||
columns: validColumns.map(col => col.name)
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getTableTypeIcon = () => {
|
||||
switch(tableType) {
|
||||
case 'stage':
|
||||
return <FaDatabase color="#fa8c16" />;
|
||||
case 'fact':
|
||||
return <HiOutlineDocumentReport color="#1890ff" />;
|
||||
case 'dimension':
|
||||
return <FaBuilding color="#52c41a" />;
|
||||
default:
|
||||
return <FaTable />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="popup-overlay" style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div className="popup-content" style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
width: '500px',
|
||||
maxWidth: '90%',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h2 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
{getTableTypeIcon()}
|
||||
Create New Table
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer',
|
||||
color: '#999'
|
||||
}}
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||||
Table Name:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tableName}
|
||||
onChange={(e) => setTableName(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}
|
||||
placeholder="Enter table name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||||
Table Type:
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: tableType === 'stage' ? '#e6f7ff' : 'white',
|
||||
borderColor: tableType === 'stage' ? '#1890ff' : '#d9d9d9'
|
||||
}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="tableType"
|
||||
value="stage"
|
||||
checked={tableType === 'stage'}
|
||||
onChange={() => setTableType('stage')}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<FaDatabase color="#fa8c16" />
|
||||
Stage
|
||||
</label>
|
||||
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: tableType === 'fact' ? '#fff7e6' : 'white',
|
||||
borderColor: tableType === 'fact' ? '#fa8c16' : '#d9d9d9'
|
||||
}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="tableType"
|
||||
value="fact"
|
||||
checked={tableType === 'fact'}
|
||||
onChange={() => setTableType('fact')}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<HiOutlineDocumentReport color="#1890ff" />
|
||||
Fact
|
||||
</label>
|
||||
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: tableType === 'dimension' ? '#f6ffed' : 'white',
|
||||
borderColor: tableType === 'dimension' ? '#52c41a' : '#d9d9d9'
|
||||
}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="tableType"
|
||||
value="dimension"
|
||||
checked={tableType === 'dimension'}
|
||||
onChange={() => setTableType('dimension')}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<FaBuilding color="#52c41a" />
|
||||
Dimension
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
|
||||
Columns:
|
||||
</label>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{columns.length > 0 ? (
|
||||
<div style={{
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '10px'
|
||||
}}>
|
||||
{columns.map((column, index) => (
|
||||
<div key={index} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px',
|
||||
borderBottom: index < columns.length - 1 ? '1px solid #f0f0f0' : 'none'
|
||||
}}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={column.name}
|
||||
onChange={(e) => {
|
||||
const newColumns = [...columns];
|
||||
newColumns[index].name = e.target.value;
|
||||
setColumns(newColumns);
|
||||
}}
|
||||
placeholder="Column name"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '120px', marginLeft: '10px' }}>
|
||||
<select
|
||||
value={column.type}
|
||||
onChange={(e) => {
|
||||
const newColumns = [...columns];
|
||||
newColumns[index].type = e.target.value;
|
||||
setColumns(newColumns);
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveColumn(index)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#ff4d4f',
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
border: '1px dashed #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
color: '#999'
|
||||
}}>
|
||||
No columns added yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<input
|
||||
type="text"
|
||||
value={newColumnName}
|
||||
onChange={(e) => setNewColumnName(e.target.value)}
|
||||
placeholder="New column name"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '120px' }}>
|
||||
<select
|
||||
value={newColumnType}
|
||||
onChange={(e) => setNewColumnType(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #d9d9d9'
|
||||
}}
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddColumn}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
padding: '8px 12px',
|
||||
background: '#1890ff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<FaPlus size={12} />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: 'white',
|
||||
color: '#333',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#52c41a',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px'
|
||||
}}
|
||||
>
|
||||
<FaTable size={14} />
|
||||
Create Table
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableCreationPopup;
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createUniver, defaultTheme, LocaleType, merge } from '@univerjs/presets';
|
||||
import { UniverSheetsPivotTablePlugin } from '@univerjs-pro/sheets-pivot';
|
||||
import { UniverSheetsPivotTableUIPlugin } from '@univerjs-pro/sheets-pivot-ui';
|
||||
import SheetsPivotTableEnUS from '@univerjs-pro/sheets-pivot/locale/en-US';
|
||||
|
||||
// Load CSS dynamically to avoid import errors
|
||||
const loadCSS = (url) => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
const UniverSheet = () => {
|
||||
const containerRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('UniverSheet mounting, container ref:', containerRef.current);
|
||||
// Load CSS files
|
||||
loadCSS('/univerjs/css/preset-sheets-core.css');
|
||||
loadCSS('/univerjs/css/sheets-pivot-ui.css');
|
||||
|
||||
// Import the UniverSheetsCorePreset to ensure we have the basic sheet functionality
|
||||
import('@univerjs/preset-sheets-core').then(({ UniverSheetsCorePreset }) => {
|
||||
import('@univerjs/preset-sheets-core/locales/en-US').then((UniverPresetSheetsCoreEnUS) => {
|
||||
try {
|
||||
const { univerAPI } = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: merge({}, SheetsPivotTableEnUS, UniverPresetSheetsCoreEnUS.default),
|
||||
},
|
||||
theme: defaultTheme,
|
||||
presets: [
|
||||
// First add the core sheet preset
|
||||
UniverSheetsCorePreset({
|
||||
container: containerRef.current,
|
||||
}),
|
||||
// Then add the pivot table plugins
|
||||
UniverSheetsPivotTablePlugin,
|
||||
UniverSheetsPivotTableUIPlugin,
|
||||
],
|
||||
});
|
||||
|
||||
// Create a workbook and add data - not using Promise since it doesn't return one
|
||||
try {
|
||||
univerAPI.createWorkbook({ name: 'Pivot Table Demo' });
|
||||
console.log('Workbook created');
|
||||
|
||||
const workbook = univerAPI.getActiveWorkbook();
|
||||
if (!workbook) {
|
||||
throw new Error('No active workbook found');
|
||||
}
|
||||
|
||||
const sheet = workbook.getActiveSheet();
|
||||
if (!sheet) {
|
||||
throw new Error('No active sheet found');
|
||||
}
|
||||
|
||||
console.log('Got workbook and sheet:', workbook, sheet);
|
||||
|
||||
// Sample data for the pivot table
|
||||
const sourceData = [
|
||||
['Region', 'Product', 'Sales'],
|
||||
['North', 'Apples', 120],
|
||||
['South', 'Oranges', 150],
|
||||
['East', 'Apples', 180],
|
||||
['West', 'Oranges', 100],
|
||||
];
|
||||
|
||||
// Check if the sheet has the necessary methods
|
||||
if (typeof sheet.setCellValue !== 'function') {
|
||||
console.warn('Sheet does not have setCellValue method, trying alternative approach');
|
||||
|
||||
// Try to use a different method to set cell values
|
||||
// This is a simplified approach - we'll just set loading to false and continue
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add data to the sheet
|
||||
for (let rowIndex = 0; rowIndex < sourceData.length; rowIndex++) {
|
||||
const row = sourceData[rowIndex];
|
||||
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
||||
try {
|
||||
sheet.setCellValue(rowIndex, colIndex, row[colIndex]);
|
||||
} catch (e) {
|
||||
console.warn(`Error setting cell value at [${rowIndex}, ${colIndex}]:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if workbook has the addPivotTable method
|
||||
if (typeof workbook.addPivotTable !== 'function') {
|
||||
console.warn('Workbook does not have addPivotTable method');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create pivot table
|
||||
const sourceInfo = {
|
||||
unitId: workbook.getId(),
|
||||
subUnitId: sheet.getSheetId(),
|
||||
sheetName: sheet.getSheetName(),
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: sourceData.length - 1,
|
||||
endColumn: sourceData[0].length - 1,
|
||||
},
|
||||
};
|
||||
|
||||
const anchorCellInfo = {
|
||||
unitId: workbook.getId(),
|
||||
subUnitId: sheet.getSheetId(),
|
||||
row: 0,
|
||||
col: 4,
|
||||
};
|
||||
|
||||
// Try to add the pivot table
|
||||
try {
|
||||
workbook.addPivotTable(sourceInfo, anchorCellInfo);
|
||||
console.log('Pivot table added successfully');
|
||||
setLoading(false); // Set loading to false when successful
|
||||
} catch (error) {
|
||||
console.error('Error adding pivot table:', error);
|
||||
setError(`Error adding pivot table: ${error.message}`);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up workbook/sheet:', error);
|
||||
setError(`Error setting up workbook/sheet: ${error.message}`);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing Univer:', error);
|
||||
setError(`Error initializing Univer: ${error.message}`);
|
||||
setLoading(false);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error loading locale:', error);
|
||||
setError(`Error loading locale: ${error.message}`);
|
||||
setLoading(false);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('Error loading preset:', error);
|
||||
setError(`Error loading preset: ${error.message}`);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
// Cleanup will be handled by the inner function
|
||||
console.log('UniverSheet unmounting');
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '100%', height: '600px' }}>
|
||||
{error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255, 77, 79, 0.1)',
|
||||
border: '1px solid #ff4d4f',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
color: '#ff4d4f',
|
||||
zIndex: 10
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && !error && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
zIndex: 5
|
||||
}}>
|
||||
Loading UniverSheet...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
background: '#fff'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UniverSheet;
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// import React, { useEffect, useRef, useState } from 'react';
|
||||
// import { createUniver, defaultTheme, LocaleType, merge } from '@univerjs/presets';
|
||||
// import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||
// import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
||||
|
||||
// // Load CSS dynamically to avoid import errors
|
||||
// const loadCSS = (url) => {
|
||||
// const link = document.createElement('link');
|
||||
// link.rel = 'stylesheet';
|
||||
// link.href = url;
|
||||
// document.head.appendChild(link);
|
||||
// };
|
||||
|
||||
// const UniverSheetComponent = () => {
|
||||
// const containerRef = useRef(null);
|
||||
// const [error, setError] = useState(null);
|
||||
// const [loading, setLoading] = useState(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log('UniverSheetComponent mounting, container ref:', containerRef.current);
|
||||
// // Load CSS files
|
||||
// loadCSS('/univerjs/css/preset-sheets-core.css');
|
||||
|
||||
// try {
|
||||
// const { univerAPI } = createUniver({
|
||||
// locale: LocaleType.EN_US,
|
||||
// locales: {
|
||||
// [LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
|
||||
// },
|
||||
// theme: defaultTheme,
|
||||
// presets: [
|
||||
// UniverSheetsCorePreset({
|
||||
// container: containerRef.current,
|
||||
// }),
|
||||
// ],
|
||||
// });
|
||||
|
||||
// // It seems createWorkbook doesn't return a Promise in this version
|
||||
// try {
|
||||
// univerAPI.createWorkbook({ name: 'Test Sheet' });
|
||||
// console.log('Workbook created successfully');
|
||||
// setLoading(false);
|
||||
// } catch (error) {
|
||||
// console.error('Error creating workbook:', error);
|
||||
// setError(`Error creating workbook: ${error.message}`);
|
||||
// setLoading(false);
|
||||
// }
|
||||
|
||||
// return () => {
|
||||
// try {
|
||||
// univerAPI.dispose();
|
||||
// } catch (e) {
|
||||
// console.warn('Error disposing UniverSheet:', e);
|
||||
// }
|
||||
// console.log('UniverSheetComponent unmounting');
|
||||
// };
|
||||
// } catch (error) {
|
||||
// console.error('Error initializing Univer:', error);
|
||||
// setError(`Error initializing Univer: ${error.message}`);
|
||||
// setLoading(false);
|
||||
// return () => {
|
||||
// console.log('UniverSheetComponent unmounting (after error)');
|
||||
// };
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <div style={{ position: 'relative', width: '100%', height: '600px' }}>
|
||||
// {error && (
|
||||
// <div style={{
|
||||
// position: 'absolute',
|
||||
// top: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// display: 'flex',
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// background: 'rgba(255, 77, 79, 0.1)',
|
||||
// border: '1px solid #ff4d4f',
|
||||
// borderRadius: '4px',
|
||||
// padding: '20px',
|
||||
// color: '#ff4d4f',
|
||||
// zIndex: 10
|
||||
// }}>
|
||||
// {error}
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {loading && !error && (
|
||||
// <div style={{
|
||||
// position: 'absolute',
|
||||
// top: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// display: 'flex',
|
||||
// alignItems: 'center',
|
||||
// justifyContent: 'center',
|
||||
// background: 'rgba(0, 0, 0, 0.05)',
|
||||
// border: '1px solid #d9d9d9',
|
||||
// borderRadius: '4px',
|
||||
// zIndex: 5
|
||||
// }}>
|
||||
// Loading UniverSheetComponent...
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// <div
|
||||
// ref={containerRef}
|
||||
// style={{
|
||||
// width: '100%',
|
||||
// height: '600px',
|
||||
// border: '1px solid #333',
|
||||
// borderRadius: '4px',
|
||||
// background: '#fff'
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default UniverSheetComponent;
|
||||
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { createUniver, defaultTheme, LocaleType, merge } from '@univerjs/presets';
|
||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core';
|
||||
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US';
|
||||
import '@univerjs/presets/lib/styles/preset-sheets-core.css';
|
||||
|
||||
const UniverSheetComponent = () => {
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { univerAPI } = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: merge({}, UniverPresetSheetsCoreEnUS),
|
||||
},
|
||||
theme: defaultTheme,
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: containerRef.current,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
univerAPI.createWorkbook({ name: 'Test Sheet' });
|
||||
|
||||
return () => {
|
||||
univerAPI.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
|
||||
};
|
||||
|
||||
export default UniverSheetComponent;
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
// Mock API data for DataflowCanvas component
|
||||
|
||||
export const mockApiData = {
|
||||
// Schema definitions
|
||||
schemas: [
|
||||
{
|
||||
name: "Schema_1",
|
||||
slug: "edw_schema",
|
||||
description: "Main enterprise data warehouse schema containing all tables",
|
||||
color: "#1890ff",
|
||||
position: { x: 50, y: 50 },
|
||||
width: 1200, // Increased width
|
||||
height: 700 // Adjusted height
|
||||
},
|
||||
{
|
||||
name: "Schema_2",
|
||||
slug: "analytics_schema",
|
||||
description: "Analytics schema for reporting and business intelligence",
|
||||
color: "#52c41a",
|
||||
position: { x: 1500, y: 50 }, // Increased gap from Schema_1
|
||||
width: 1500, // Further increased width to accommodate all tables
|
||||
height: 700 // Adjusted height
|
||||
}
|
||||
],
|
||||
|
||||
// Tables data with name, slug, and position - organized for left-to-right data flow
|
||||
tables: [
|
||||
// Schema_1 tables - Stage tables (first column)
|
||||
{
|
||||
name: "Customer_Stage",
|
||||
slug: "cst_stg",
|
||||
type: "stage",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 100, y: 100 },
|
||||
columns: ["customer_id", "customer_name", "email", "address", "phone", "raw_data", "load_date", "source_system"]
|
||||
},
|
||||
{
|
||||
name: "Order_Stage",
|
||||
slug: "ord_stg",
|
||||
type: "stage",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 100, y: 300 },
|
||||
columns: ["order_id", "customer_id", "order_date", "total_amount", "status", "raw_data", "load_date", "source_system"]
|
||||
},
|
||||
{
|
||||
name: "Product_Stage",
|
||||
slug: "prd_stg",
|
||||
type: "stage",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 100, y: 500 },
|
||||
columns: ["product_id", "product_name", "category", "price", "inventory", "raw_data", "load_date", "source_system"]
|
||||
},
|
||||
|
||||
// Schema_1 tables - Dimension tables (third column)
|
||||
{
|
||||
name: "Customer_Dim",
|
||||
slug: "uty126",
|
||||
type: "dimension",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 500, y: 100 },
|
||||
columns: ["customer_id", "customer_name", "email", "address", "phone"]
|
||||
},
|
||||
{
|
||||
name: "Product_Dim",
|
||||
slug: "prd123",
|
||||
type: "dimension",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 500, y: 500 },
|
||||
columns: ["product_id", "product_name", "category", "price", "inventory"]
|
||||
},
|
||||
{
|
||||
name: "Time_Dim",
|
||||
slug: "tim123",
|
||||
type: "dimension",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 500, y: 300 },
|
||||
columns: ["date_id", "day", "month", "quarter", "year", "is_holiday"]
|
||||
},
|
||||
|
||||
// Schema_1 tables - Fact table (fifth column)
|
||||
{
|
||||
name: "Order_Fact",
|
||||
slug: "ntz356",
|
||||
type: "fact",
|
||||
schema: "edw_schema",
|
||||
orientation: { x: 900, y: 300 },
|
||||
columns: ["order_id", "customer_id", "order_date", "total_amount", "status"]
|
||||
},
|
||||
|
||||
// Schema_2 tables - organized for left-to-right data flow (similar to Schema_1)
|
||||
{
|
||||
name: "Sales_Stage",
|
||||
slug: "sls_stg",
|
||||
type: "stage",
|
||||
schema: "analytics_schema",
|
||||
orientation: { x: 1600, y: 300 },
|
||||
columns: ["sale_id", "product_id", "customer_id", "quantity", "sale_date", "revenue", "raw_data", "load_date", "source_system"]
|
||||
},
|
||||
|
||||
// Schema_2 tables - Dimension tables (middle column)
|
||||
{
|
||||
name: "Product_Dim",
|
||||
slug: "prd789",
|
||||
type: "dimension",
|
||||
schema: "analytics_schema",
|
||||
orientation: { x: 2000, y: 100 },
|
||||
columns: ["product_id", "product_name", "category", "price", "inventory"]
|
||||
},
|
||||
{
|
||||
name: "Customer_Dim",
|
||||
slug: "cus789",
|
||||
type: "dimension",
|
||||
schema: "analytics_schema",
|
||||
orientation: { x: 2000, y: 300 },
|
||||
columns: ["customer_id", "customer_name", "email", "address", "phone"]
|
||||
},
|
||||
{
|
||||
name: "Time_Dim",
|
||||
slug: "tim567",
|
||||
type: "dimension",
|
||||
schema: "analytics_schema",
|
||||
orientation: { x: 2000, y: 500 },
|
||||
columns: ["date_id", "day", "month", "quarter", "year", "is_holiday"]
|
||||
},
|
||||
|
||||
// Schema_2 tables - Fact table (right column)
|
||||
{
|
||||
name: "Sales_Fact",
|
||||
slug: "sls432",
|
||||
type: "fact",
|
||||
schema: "analytics_schema",
|
||||
orientation: { x: 2400, y: 300 },
|
||||
columns: ["sale_id", "product_id", "customer_id", "quantity", "sale_date", "revenue"]
|
||||
}
|
||||
],
|
||||
|
||||
// Processes that connect tables
|
||||
processes: [
|
||||
// Stage to Dimension/Fact processes
|
||||
{
|
||||
name: "Stage_to_Customer_Dim",
|
||||
slug: "process_stg_cust",
|
||||
source_table: ["cst_stg"],
|
||||
destination_table: ["uty126"],
|
||||
description: "ETL process to load customer data from stage to dimension table with data cleansing and validation",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "customer_id", target: "customer_id", type: "direct" },
|
||||
{ source: "customer_name", target: "customer_name", type: "direct" },
|
||||
{ source: "email", target: "email", type: "transform" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Stage_to_Order_Fact",
|
||||
slug: "process_stg_ord",
|
||||
source_table: ["ord_stg"],
|
||||
destination_table: ["ntz356"],
|
||||
description: "ETL process to load order data from stage to fact table with data transformation and aggregation",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "order_id", target: "order_id", type: "direct" },
|
||||
{ source: "customer_id", target: "customer_id", type: "direct" },
|
||||
{ source: "order_date", target: "order_date", type: "transform" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Stage_to_Product_Dim",
|
||||
slug: "process_stg_prod",
|
||||
source_table: ["prd_stg"],
|
||||
destination_table: ["prd123"],
|
||||
description: "ETL process to load product data from stage to dimension table with data cleansing and validation",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "product_id", target: "product_id", type: "direct" },
|
||||
{ source: "product_name", target: "product_name", type: "direct" },
|
||||
{ source: "category", target: "category", type: "transform" }
|
||||
]
|
||||
},
|
||||
// Schema_1 processes
|
||||
{
|
||||
name: "Customer_Order_Process",
|
||||
slug: "process_1",
|
||||
source_table: ["uty126"],
|
||||
destination_table: ["ntz356"],
|
||||
description: "Links customers to their orders through ETL pipeline that validates customer information and enriches order data with customer attributes",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "customer_id", target: "customer_id", type: "direct" },
|
||||
{ source: "customer_name", target: "customer_name", type: "direct" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Product_Order_Process",
|
||||
slug: "process_2",
|
||||
source_table: ["prd123"],
|
||||
destination_table: ["ntz356"],
|
||||
description: "Links products to orders with inventory tracking and product categorization logic",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "product_id", target: "product_id", type: "direct" },
|
||||
{ source: "product_name", target: "product_details", type: "transform" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Time_Order_Process",
|
||||
slug: "process_3",
|
||||
source_table: ["tim123"],
|
||||
destination_table: ["ntz356"],
|
||||
description: "Adds time dimension to order data for temporal analysis and reporting",
|
||||
type: "ETL",
|
||||
status: "inactive",
|
||||
mappings: [
|
||||
{ source: "date_id", target: "order_date", type: "transform" },
|
||||
{ source: "is_holiday", target: "is_holiday_order", type: "direct" }
|
||||
]
|
||||
},
|
||||
|
||||
// Schema_2 processes
|
||||
{
|
||||
name: "Stage_to_Sales_Fact",
|
||||
slug: "process_stg_sales",
|
||||
source_table: ["sls_stg"],
|
||||
destination_table: ["sls432"],
|
||||
description: "ETL process to load sales data from stage to fact table with data transformation and aggregation",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "sale_id", target: "sale_id", type: "direct" },
|
||||
{ source: "product_id", target: "product_id", type: "direct" },
|
||||
{ source: "customer_id", target: "customer_id", type: "direct" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Product_Sales_Process",
|
||||
slug: "process_4",
|
||||
source_table: ["prd789"],
|
||||
destination_table: ["sls432"],
|
||||
description: "Links products to sales data with inventory tracking and product categorization logic",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "product_id", target: "product_id", type: "direct" },
|
||||
{ source: "price", target: "unit_price", type: "transform" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Customer_Sales_Process",
|
||||
slug: "process_5",
|
||||
source_table: ["cus789"],
|
||||
destination_table: ["sls432"],
|
||||
description: "Links customers to their purchases with customer segmentation and purchase history analysis",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "customer_id", target: "customer_id", type: "direct" },
|
||||
{ source: "customer_name", target: "buyer_name", type: "transform" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Time_Sales_Process",
|
||||
slug: "process_6",
|
||||
source_table: ["tim567"],
|
||||
destination_table: ["sls432"],
|
||||
description: "Adds time dimension to sales data for temporal analysis and reporting",
|
||||
type: "ETL",
|
||||
status: "active",
|
||||
mappings: [
|
||||
{ source: "date_id", target: "sale_date", type: "transform" },
|
||||
{ source: "quarter", target: "fiscal_quarter", type: "direct" }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// Default viewport settings - adjusted for better initial view of both schemas
|
||||
viewportSettings: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom: 0.22 // Further reduced zoom to show both schemas completely
|
||||
}
|
||||
};
|
||||
|
||||
export default mockApiData;
|
||||
|
||||
// export default mockApiData;
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Reference in New Issue