mirror of
https://github.com/apache/superset.git
synced 2026-06-13 11:39:16 +00:00
Compare commits
5 Commits
move-contr
...
examples
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0796aa1c6d | ||
|
|
14e6ec7d9f | ||
|
|
14ffa69e0b | ||
|
|
ef4cf2b430 | ||
|
|
48d8c91b19 |
40
.claude_rc
40
.claude_rc
@@ -1,40 +0,0 @@
|
||||
# Claude Code RC for move-controls
|
||||
|
||||
This is a claudette-managed Apache Superset development environment.
|
||||
|
||||
## Project: move-controls
|
||||
- Worktree Path: /Users/evan_1/.claudette/worktrees/move-controls
|
||||
- Frontend Port: 9004
|
||||
- Frontend URL: http://localhost:9004
|
||||
|
||||
## Quick Commands
|
||||
|
||||
Start services:
|
||||
```bash
|
||||
claudette docker up
|
||||
```
|
||||
|
||||
Access frontend:
|
||||
```bash
|
||||
open http://localhost:9004
|
||||
```
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
# Backend
|
||||
pytest tests/unit_tests/
|
||||
|
||||
# Frontend
|
||||
cd superset-frontend && npm test
|
||||
```
|
||||
|
||||
## Environment Details
|
||||
- Python venv: `.venv/` (auto-activated in claudette shell)
|
||||
- Node modules: `superset-frontend/node_modules/`
|
||||
- Docker prefix: `move-controls_`
|
||||
|
||||
## Development Tips
|
||||
- Always use `claudette shell` to work in this project
|
||||
- Run `pre-commit run --all-files` before committing
|
||||
- Use `claudette docker` instead of docker-compose directly
|
||||
- The frontend dev server runs on port 9004 to avoid conflicts
|
||||
@@ -1,20 +0,0 @@
|
||||
# Keep this in sync with the base image in the main Dockerfile (ARG PY_VER)
|
||||
FROM python:3.11.13-bookworm AS base
|
||||
|
||||
# Install system dependencies that Superset needs
|
||||
# This layer will be cached across Codespace sessions
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libsasl2-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
tmux \
|
||||
gh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv for fast Python package management
|
||||
# This will also be cached in the image
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
||||
echo 'export PATH="/root/.cargo/bin:$PATH"' >> /etc/bash.bashrc
|
||||
|
||||
# Set the cargo/bin directory in PATH for all users
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
@@ -3,14 +3,3 @@
|
||||
For complete documentation on using GitHub Codespaces with Apache Superset, please see:
|
||||
|
||||
**[Setting up a Development Environment - GitHub Codespaces](https://superset.apache.org/docs/contributing/development#github-codespaces-cloud-development)**
|
||||
|
||||
## Pre-installed Development Environment
|
||||
|
||||
When you create a new Codespace from this repository, it automatically:
|
||||
|
||||
1. **Creates a Python virtual environment** using `uv venv`
|
||||
2. **Installs all development dependencies** via `uv pip install -r requirements/development.txt`
|
||||
3. **Sets up pre-commit hooks** with `pre-commit install`
|
||||
4. **Activates the virtual environment** automatically in all terminals
|
||||
|
||||
The virtual environment is located at `/workspaces/{repository-name}/.venv` and is automatically activated through environment variables set in the devcontainer configuration.
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
# Superset Codespaces environment setup
|
||||
# This file is appended to ~/.bashrc during Codespace setup
|
||||
|
||||
# Find the workspace directory (handles both 'superset' and 'superset-2' names)
|
||||
WORKSPACE_DIR=$(find /workspaces -maxdepth 1 -name "superset*" -type d | head -1)
|
||||
|
||||
if [ -n "$WORKSPACE_DIR" ]; then
|
||||
# Check if virtual environment exists
|
||||
if [ -d "$WORKSPACE_DIR/.venv" ]; then
|
||||
# Activate the virtual environment
|
||||
source "$WORKSPACE_DIR/.venv/bin/activate"
|
||||
echo "✅ Python virtual environment activated"
|
||||
|
||||
# Verify pre-commit is installed and set up
|
||||
if command -v pre-commit &> /dev/null; then
|
||||
echo "✅ pre-commit is available ($(pre-commit --version))"
|
||||
# Install git hooks if not already installed
|
||||
if [ -d "$WORKSPACE_DIR/.git" ] && [ ! -f "$WORKSPACE_DIR/.git/hooks/pre-commit" ]; then
|
||||
echo "🪝 Installing pre-commit hooks..."
|
||||
cd "$WORKSPACE_DIR" && pre-commit install
|
||||
fi
|
||||
else
|
||||
echo "⚠️ pre-commit not found. Run: pip install pre-commit"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Python virtual environment not found at $WORKSPACE_DIR/.venv"
|
||||
echo " Run: cd $WORKSPACE_DIR && .devcontainer/setup-dev.sh"
|
||||
fi
|
||||
|
||||
# Always cd to the workspace directory for convenience
|
||||
cd "$WORKSPACE_DIR"
|
||||
fi
|
||||
|
||||
# Add helpful aliases for Superset development
|
||||
alias start-superset="$WORKSPACE_DIR/.devcontainer/start-superset.sh"
|
||||
alias setup-dev="$WORKSPACE_DIR/.devcontainer/setup-dev.sh"
|
||||
|
||||
# Show helpful message on login
|
||||
echo ""
|
||||
echo "🚀 Superset Codespaces Environment"
|
||||
echo "=================================="
|
||||
|
||||
# Check if Superset is running
|
||||
if docker ps 2>/dev/null | grep -q "superset"; then
|
||||
echo "✅ Superset is running!"
|
||||
echo " - Check the 'Ports' tab for your live Superset URL"
|
||||
echo " - Initial startup takes 10-20 minutes"
|
||||
echo " - Login: admin/admin"
|
||||
else
|
||||
echo "⚠️ Superset is not running. Use: start-superset"
|
||||
# Check if there's a startup log
|
||||
if [ -f "/tmp/superset-startup.log" ]; then
|
||||
echo " 📋 Startup log found: cat /tmp/superset-startup.log"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Quick commands:"
|
||||
echo " start-superset - Start Superset with Docker Compose"
|
||||
echo " setup-dev - Set up Python environment (if not already done)"
|
||||
echo " pre-commit run - Run pre-commit checks on staged files"
|
||||
echo ""
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script to build and push the devcontainer image to GitHub Container Registry
|
||||
# This allows caching the image between Codespace sessions
|
||||
|
||||
# You'll need to run this with appropriate GitHub permissions
|
||||
# gh auth login --scopes write:packages
|
||||
|
||||
REGISTRY="ghcr.io"
|
||||
OWNER="apache"
|
||||
REPO="superset"
|
||||
TAG="devcontainer-base"
|
||||
|
||||
echo "Building devcontainer image..."
|
||||
docker build -t $REGISTRY/$OWNER/$REPO:$TAG .devcontainer/
|
||||
|
||||
echo "Pushing to GitHub Container Registry..."
|
||||
docker push $REGISTRY/$OWNER/$REPO:$TAG
|
||||
|
||||
echo "Done! Update .devcontainer/devcontainer.json to use:"
|
||||
echo " \"image\": \"$REGISTRY/$OWNER/$REPO:$TAG\""
|
||||
@@ -1,15 +1,8 @@
|
||||
{
|
||||
"name": "Apache Superset Development",
|
||||
// Option 1: Use pre-built image directly
|
||||
// "image": "ghcr.io/apache/superset:devcontainer-base",
|
||||
|
||||
// Option 2: Build from Dockerfile with cache (current approach)
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": ".",
|
||||
// Cache from the Apache registry image
|
||||
"cacheFrom": ["ghcr.io/apache/superset:devcontainer-base"]
|
||||
},
|
||||
// Keep this in sync with the base image in Dockerfile (ARG PY_VER)
|
||||
// Using the same base as Dockerfile, but non-slim for dev tools
|
||||
"image": "python:3.11.13-bookworm",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
@@ -39,17 +32,10 @@
|
||||
},
|
||||
|
||||
// Run commands after container is created
|
||||
"postCreateCommand": "bash .devcontainer/setup-dev.sh || echo '⚠️ Setup had issues - run .devcontainer/setup-dev.sh manually'",
|
||||
"postCreateCommand": "chmod +x .devcontainer/setup-dev.sh && .devcontainer/setup-dev.sh",
|
||||
|
||||
// Auto-start Superset after ensuring Docker is ready
|
||||
// Run in foreground to see any errors, but don't block on failures
|
||||
"postStartCommand": "bash -c 'echo \"Waiting 30s for services to initialize...\"; sleep 30; .devcontainer/start-superset.sh || echo \"⚠️ Auto-start failed - run start-superset manually\"'",
|
||||
|
||||
// Set environment variables
|
||||
"remoteEnv": {
|
||||
// Removed automatic venv activation to prevent startup issues
|
||||
// The setup script will handle this
|
||||
},
|
||||
// Auto-start Superset on Codespace resume
|
||||
"postStartCommand": ".devcontainer/start-superset.sh",
|
||||
|
||||
// VS Code customizations
|
||||
"customizations": {
|
||||
|
||||
@@ -3,76 +3,30 @@
|
||||
|
||||
echo "🔧 Setting up Superset development environment..."
|
||||
|
||||
# System dependencies and uv are now pre-installed in the Docker image
|
||||
# This speeds up Codespace creation significantly!
|
||||
# The universal image has most tools, just need Superset-specific libs
|
||||
echo "📦 Installing Superset-specific dependencies..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libsasl2-dev \
|
||||
libldap2-dev \
|
||||
libpq-dev \
|
||||
tmux \
|
||||
gh
|
||||
|
||||
# Create virtual environment using uv
|
||||
echo "🐍 Creating Python virtual environment..."
|
||||
if ! uv venv; then
|
||||
echo "❌ Failed to create virtual environment"
|
||||
exit 1
|
||||
fi
|
||||
# Install uv for fast Python package management
|
||||
echo "📦 Installing uv..."
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Install Python dependencies
|
||||
echo "📦 Installing Python dependencies..."
|
||||
if ! uv pip install -r requirements/development.txt; then
|
||||
echo "❌ Failed to install Python dependencies"
|
||||
echo "💡 You may need to run this manually after the Codespace starts"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install pre-commit hooks
|
||||
echo "🪝 Installing pre-commit hooks..."
|
||||
if source .venv/bin/activate && pre-commit install; then
|
||||
echo "✅ Pre-commit hooks installed"
|
||||
else
|
||||
echo "⚠️ Pre-commit hooks installation failed (non-critical)"
|
||||
fi
|
||||
# Add cargo/bin to PATH for uv
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
|
||||
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
|
||||
|
||||
# Install Claude Code CLI via npm
|
||||
echo "🤖 Installing Claude Code..."
|
||||
if npm install -g @anthropic-ai/claude-code; then
|
||||
echo "✅ Claude Code installed"
|
||||
else
|
||||
echo "⚠️ Claude Code installation failed (non-critical)"
|
||||
fi
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
|
||||
# Make the start script executable
|
||||
chmod +x .devcontainer/start-superset.sh
|
||||
|
||||
# Add bashrc additions for automatic venv activation
|
||||
echo "🔧 Setting up automatic environment activation..."
|
||||
if [ -f ~/.bashrc ]; then
|
||||
# Check if we've already added our additions
|
||||
if ! grep -q "Superset Codespaces environment setup" ~/.bashrc; then
|
||||
echo "" >> ~/.bashrc
|
||||
cat .devcontainer/bashrc-additions >> ~/.bashrc
|
||||
echo "✅ Added automatic venv activation to ~/.bashrc"
|
||||
else
|
||||
echo "✅ Bashrc additions already present"
|
||||
fi
|
||||
else
|
||||
# Create bashrc if it doesn't exist
|
||||
cat .devcontainer/bashrc-additions > ~/.bashrc
|
||||
echo "✅ Created ~/.bashrc with automatic venv activation"
|
||||
fi
|
||||
|
||||
# Also add to zshrc since that's the default shell
|
||||
if [ -f ~/.zshrc ] || [ -n "$ZSH_VERSION" ]; then
|
||||
if ! grep -q "Superset Codespaces environment setup" ~/.zshrc; then
|
||||
echo "" >> ~/.zshrc
|
||||
cat .devcontainer/bashrc-additions >> ~/.zshrc
|
||||
echo "✅ Added automatic venv activation to ~/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Development environment setup complete!"
|
||||
echo ""
|
||||
echo "📝 The virtual environment will be automatically activated in new terminals"
|
||||
echo ""
|
||||
echo "🔄 To activate in this terminal, run:"
|
||||
echo " source ~/.bashrc"
|
||||
echo ""
|
||||
echo "🚀 To start Superset:"
|
||||
echo " start-superset"
|
||||
echo ""
|
||||
echo "🚀 Run '.devcontainer/start-superset.sh' to start Superset"
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Startup script for Superset in Codespaces
|
||||
|
||||
# Log to a file for debugging
|
||||
LOG_FILE="/tmp/superset-startup.log"
|
||||
echo "[$(date)] Starting Superset startup script" >> "$LOG_FILE"
|
||||
echo "[$(date)] User: $(whoami), PWD: $(pwd)" >> "$LOG_FILE"
|
||||
|
||||
echo "🚀 Starting Superset in Codespaces..."
|
||||
echo "🌐 Frontend will be available at port 9001"
|
||||
|
||||
@@ -18,37 +13,10 @@ else
|
||||
echo "📁 Using current directory: $(pwd)"
|
||||
fi
|
||||
|
||||
# Wait for Docker to be available
|
||||
echo "⏳ Waiting for Docker to start..."
|
||||
echo "[$(date)] Waiting for Docker..." >> "$LOG_FILE"
|
||||
max_attempts=30
|
||||
attempt=0
|
||||
while ! docker info > /dev/null 2>&1; do
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
echo "❌ Docker failed to start after $max_attempts attempts"
|
||||
echo "[$(date)] Docker failed to start after $max_attempts attempts" >> "$LOG_FILE"
|
||||
echo "🔄 Please restart the Codespace or run this script manually later"
|
||||
exit 1
|
||||
fi
|
||||
echo " Attempt $((attempt + 1))/$max_attempts..."
|
||||
echo "[$(date)] Docker check attempt $((attempt + 1))/$max_attempts" >> "$LOG_FILE"
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
echo "✅ Docker is ready!"
|
||||
echo "[$(date)] Docker is ready" >> "$LOG_FILE"
|
||||
|
||||
# Check if Superset containers are already running
|
||||
if docker ps | grep -q "superset"; then
|
||||
echo "✅ Superset containers are already running!"
|
||||
echo ""
|
||||
echo "🌐 To access Superset:"
|
||||
echo " 1. Click the 'Ports' tab at the bottom of VS Code"
|
||||
echo " 2. Find port 9001 and click the globe icon to open"
|
||||
echo " 3. Wait 10-20 minutes for initial startup"
|
||||
echo ""
|
||||
echo "📝 Login credentials: admin/admin"
|
||||
exit 0
|
||||
# Check if docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "⏳ Waiting for Docker to start..."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Clean up any existing containers
|
||||
@@ -56,33 +24,16 @@ echo "🧹 Cleaning up existing containers..."
|
||||
docker-compose -f docker-compose-light.yml down
|
||||
|
||||
# Start services
|
||||
echo "🏗️ Starting Superset in background (daemon mode)..."
|
||||
echo "🏗️ Building and starting services..."
|
||||
echo ""
|
||||
echo "📝 Once started, login with:"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin"
|
||||
echo ""
|
||||
echo "📋 Running in foreground with live logs (Ctrl+C to stop)..."
|
||||
|
||||
# Start in detached mode
|
||||
docker-compose -f docker-compose-light.yml up -d
|
||||
|
||||
echo ""
|
||||
echo "✅ Docker Compose started successfully!"
|
||||
echo ""
|
||||
echo "📋 Important information:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "⏱️ Initial startup takes 10-20 minutes"
|
||||
echo "🌐 Check the 'Ports' tab for your Superset URL (port 9001)"
|
||||
echo "👤 Login: admin / admin"
|
||||
echo ""
|
||||
echo "📊 Useful commands:"
|
||||
echo " docker-compose -f docker-compose-light.yml logs -f # Follow logs"
|
||||
echo " docker-compose -f docker-compose-light.yml ps # Check status"
|
||||
echo " docker-compose -f docker-compose-light.yml down # Stop services"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "💤 Keeping terminal open for 60 seconds to test persistence..."
|
||||
sleep 60
|
||||
echo "✅ Test complete - check if this terminal is still visible!"
|
||||
|
||||
# Show final status
|
||||
docker-compose -f docker-compose-light.yml ps
|
||||
# Run docker-compose and capture exit code
|
||||
docker-compose -f docker-compose-light.yml up
|
||||
EXIT_CODE=$?
|
||||
|
||||
# If it failed, provide helpful instructions
|
||||
|
||||
2
.github/workflows/welcome-new-users.yml
vendored
2
.github/workflows/welcome-new-users.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Welcome Message
|
||||
uses: actions/first-interaction@v2
|
||||
uses: actions/first-interaction@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,7 +26,6 @@ __pycache__
|
||||
.cache
|
||||
.bento*
|
||||
.cache-loader
|
||||
.claude_rc
|
||||
.coverage
|
||||
cover
|
||||
.DS_Store
|
||||
|
||||
@@ -55,6 +55,7 @@ esm/*
|
||||
tsconfig.tsbuildinfo
|
||||
.*ipynb
|
||||
.*yml
|
||||
.*yaml
|
||||
.*iml
|
||||
.esprintrc
|
||||
.prettierignore
|
||||
|
||||
@@ -1,802 +0,0 @@
|
||||
# Control Panel Migration Agent
|
||||
|
||||
A comprehensive guide for migrating Apache Superset control panels from the legacy config-based approach to the new React-based approach.
|
||||
|
||||
## Overview
|
||||
|
||||
This migration transforms control panels from complex string-referenced configurations (`controlPanelSections`/`controlSetRows`) to pure React components. The new approach provides:
|
||||
|
||||
- **Direct React components** instead of config objects
|
||||
- **Full TypeScript support** with proper type safety
|
||||
- **Simplified architecture** with no JSON intermediary
|
||||
- **Better developer experience** with IDE autocomplete and refactoring support
|
||||
|
||||
## Architecture Comparison
|
||||
|
||||
### Legacy Architecture
|
||||
```typescript
|
||||
// Old approach: Config-based with string references and config objects
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[GroupByControl()], // String reference
|
||||
[MetricControl()], // String reference
|
||||
[
|
||||
{
|
||||
name: 'show_labels',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Labels'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
], // Config object
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### New React-Based Architecture
|
||||
```typescript
|
||||
// New approach: Pure React components with direct JSX
|
||||
export const PieControlPanel: FC<PieControlPanelProps> = ({ ... }) => {
|
||||
return (
|
||||
<div>
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
// ... other props
|
||||
/>
|
||||
<CheckboxControl
|
||||
label={t('Show Labels')}
|
||||
value={formValues.show_labels ?? true}
|
||||
onChange={handleChange('show_labels')}
|
||||
renderTrigger
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Mark as modern panel
|
||||
(PieControlPanel as any).isModernPanel = true;
|
||||
```
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### 1. File Structure
|
||||
|
||||
Keep the existing file location but change from config to React component:
|
||||
- `controlPanel.ts` → `[ChartName]ControlPanelSimple.tsx`
|
||||
- The file remains in the same directory as the original
|
||||
|
||||
### 2. Update Imports
|
||||
|
||||
**From (Legacy):**
|
||||
```typescript
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
sharedControls,
|
||||
GroupByControl,
|
||||
MetricControl
|
||||
} from '@superset-ui/chart-controls';
|
||||
```
|
||||
|
||||
**To (Modern):**
|
||||
```typescript
|
||||
import { FC, useState } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Tabs } from 'antd';
|
||||
import {
|
||||
ColorSchemeControl,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
// Direct component imports
|
||||
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
|
||||
import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
|
||||
import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
|
||||
import TextControl from '../../../../src/explore/components/controls/TextControl';
|
||||
import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
|
||||
import SliderControl from '../../../../src/explore/components/controls/SliderControl';
|
||||
import SelectControl from '../../../../src/explore/components/controls/SelectControl';
|
||||
import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
|
||||
import ControlHeader from '../../../../src/explore/components/ControlHeader';
|
||||
import Control from '../../../../src/explore/components/Control';
|
||||
```
|
||||
|
||||
### 3. Component Structure Template
|
||||
|
||||
Create a React component with this structure:
|
||||
|
||||
```typescript
|
||||
interface [ChartName]ControlPanelProps {
|
||||
onChange?: (field: string, value: any) => void;
|
||||
value?: Record<string, any>;
|
||||
datasource?: any;
|
||||
actions?: any;
|
||||
controls?: any;
|
||||
form_data?: any;
|
||||
}
|
||||
|
||||
export const [ChartName]ControlPanel: FC<[ChartName]ControlPanelProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
datasource,
|
||||
form_data,
|
||||
actions,
|
||||
controls,
|
||||
}) => {
|
||||
// Safety checks for datasource
|
||||
if (!datasource || !form_data) {
|
||||
return <div>Loading control panel...</div>;
|
||||
}
|
||||
|
||||
// Ensure safe data structures
|
||||
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
|
||||
const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
|
||||
|
||||
// Helper for control changes
|
||||
const handleChange = (field: string) => (val: any) => {
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(field, val);
|
||||
} else if (onChange) {
|
||||
onChange(field, val);
|
||||
}
|
||||
};
|
||||
|
||||
// Get form values
|
||||
const formValues = form_data || value || {};
|
||||
|
||||
// Tab state (if using tabs)
|
||||
const [activeTab, setActiveTab] = useState('data');
|
||||
|
||||
// Component implementation here...
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* Your controls here */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// CRITICAL: Mark as modern panel
|
||||
([ChartName]ControlPanel as any).isModernPanel = true;
|
||||
|
||||
// Export wrapper config for compatibility
|
||||
const config = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: null,
|
||||
expanded: true,
|
||||
controlSetRows: [[[ChartName]ControlPanel as any]],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
// Move all defaults here
|
||||
field_name: {
|
||||
default: defaultValue,
|
||||
label: t('Field Label'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### 4. Control Mapping Reference
|
||||
|
||||
#### String References → React Components
|
||||
|
||||
| Legacy String | Modern React Component | Notes |
|
||||
|---------------|----------------------|--------|
|
||||
| `['groupby']` | `<DndColumnSelect ... />` | Multi-select columns |
|
||||
| `['metric']` | `<DndMetricSelect ... />` | Single metric select |
|
||||
| `['metrics']` | `<DndMetricSelect ... />` | Multi metric select |
|
||||
| `['adhoc_filters']` | `<DndFilterSelect ... />` | Advanced filters |
|
||||
| `['row_limit']` | `<TextControl isInt ... />` | Numeric input |
|
||||
| `['color_scheme']` | `ColorSchemeControl()` with `Control` wrapper | Special handling needed |
|
||||
|
||||
#### Config Objects → React Components
|
||||
|
||||
| Legacy Config | Modern Component | Example |
|
||||
|---------------|------------------|---------|
|
||||
| `{ type: 'TextControl', ... }` | `<TextControl ... />` | `<TextControl value={val} onChange={fn} />` |
|
||||
| `{ type: 'CheckboxControl', ... }` | `<CheckboxControl ... />` | `<CheckboxControl label="..." value={val} />` |
|
||||
| `{ type: 'SelectControl', ... }` | `<SelectControl ... />` | `<SelectControl choices={[...]} value={val} />` |
|
||||
| `{ type: 'SliderControl', ... }` | `<SliderControl ... />` | `<SliderControl {...{min: 0, max: 100}} />` |
|
||||
|
||||
### 5. Props Mapping Guide
|
||||
|
||||
| Legacy Config Property | Modern React Prop | Notes |
|
||||
|----------------------|------------------|-------|
|
||||
| `label` | `label` prop OR `<ControlHeader>` | Use ControlHeader for tooltips |
|
||||
| `description` | `description` prop OR `<ControlHeader>` | ControlHeader for complex descriptions |
|
||||
| `default` | Move to `controlOverrides` | Don't set as component prop |
|
||||
| `renderTrigger: true` | `renderTrigger` prop | Controls instant chart updates |
|
||||
| `visibility` | Conditional rendering | `{condition && <Control />}` |
|
||||
| `choices` | `choices` prop | For SelectControl |
|
||||
| `min/max/step` | Spread object | `{...{ min: 10, max: 100, step: 1 }}` |
|
||||
|
||||
### 6. Implementing Tabbed Layout
|
||||
|
||||
Most modern control panels use a Data/Customize tab structure:
|
||||
|
||||
```typescript
|
||||
const [activeTab, setActiveTab] = useState('data');
|
||||
|
||||
const dataTabContent = (
|
||||
<div>
|
||||
{/* Query-related controls: columns, metrics, filters, row limit */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<ControlHeader
|
||||
label={t('Group by')}
|
||||
description={t('Columns to group by')}
|
||||
hovered
|
||||
/>
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
options={safeColumns}
|
||||
name="groupby"
|
||||
label="" // Avoid duplicate labels
|
||||
multi
|
||||
canDelete
|
||||
ghostButtonText={t('Add dimension')}
|
||||
type="DndColumnSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
{/* More data controls... */}
|
||||
</div>
|
||||
);
|
||||
|
||||
const customizeTabContent = (
|
||||
<div>
|
||||
{/* Styling and display controls */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Chart Options')}</h4>
|
||||
{/* Styling controls... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const tabItems = [
|
||||
{ key: 'data', label: t('Data'), children: dataTabContent },
|
||||
{ key: 'customize', label: t('Customize'), children: customizeTabContent },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 7. Common Control Patterns
|
||||
|
||||
#### Drag-and-Drop Controls
|
||||
```typescript
|
||||
{/* Group By - Column Selection */}
|
||||
<DndColumnSelect
|
||||
value={formValues.groupby || []}
|
||||
onChange={handleChange('groupby')}
|
||||
options={safeColumns}
|
||||
name="groupby"
|
||||
label="" // Empty to avoid duplicate with ControlHeader
|
||||
multi
|
||||
canDelete
|
||||
ghostButtonText={t('Add dimension')}
|
||||
type="DndColumnSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
{/* Metric Selection */}
|
||||
<DndMetricSelect
|
||||
value={formValues.metric}
|
||||
onChange={handleChange('metric')}
|
||||
datasource={safeDataSource}
|
||||
name="metric"
|
||||
label=""
|
||||
multi={false}
|
||||
savedMetrics={safeMetrics}
|
||||
/>
|
||||
|
||||
{/* Filters */}
|
||||
<DndFilterSelect
|
||||
value={formValues.adhoc_filters || []}
|
||||
onChange={handleChange('adhoc_filters')}
|
||||
datasource={safeDataSource}
|
||||
columns={safeColumns}
|
||||
formData={formValues}
|
||||
name="adhoc_filters"
|
||||
savedMetrics={safeMetrics}
|
||||
selectedMetrics={formValues.metric ? [formValues.metric] : []}
|
||||
type="DndFilterSelect"
|
||||
actions={actions}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Color Scheme Control (Special Case)
|
||||
```typescript
|
||||
{/* Color Scheme requires special Control wrapper */}
|
||||
{(() => {
|
||||
const colorSchemeControl = ColorSchemeControl();
|
||||
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
|
||||
return (
|
||||
<Control
|
||||
{...cleanConfig}
|
||||
name="color_scheme"
|
||||
value={formValues.color_scheme}
|
||||
actions={{
|
||||
...actions,
|
||||
setControlValue: (field: string, val: any) => {
|
||||
handleChange('color_scheme')(val);
|
||||
},
|
||||
}}
|
||||
renderTrigger
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
```
|
||||
|
||||
#### Basic Controls
|
||||
```typescript
|
||||
{/* Text Input */}
|
||||
<TextControl
|
||||
value={formValues.row_limit}
|
||||
onChange={handleChange('row_limit')}
|
||||
isInt
|
||||
placeholder="100"
|
||||
controlId="row_limit"
|
||||
/>
|
||||
|
||||
{/* Checkbox */}
|
||||
<CheckboxControl
|
||||
label={t('Show Labels')}
|
||||
description={t('Whether to display the labels.')}
|
||||
value={formValues.show_labels ?? true}
|
||||
onChange={handleChange('show_labels')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
|
||||
{/* Select Dropdown */}
|
||||
<SelectControl
|
||||
label={t('Label Type')}
|
||||
description={t('What should be shown on the label?')}
|
||||
value={formValues.label_type || 'key'}
|
||||
onChange={handleChange('label_type')}
|
||||
choices={[
|
||||
['key', t('Category Name')],
|
||||
['value', t('Value')],
|
||||
['percent', t('Percentage')],
|
||||
]}
|
||||
clearable={false}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
|
||||
{/* Slider */}
|
||||
<SliderControl
|
||||
value={formValues.outerRadius || 70}
|
||||
onChange={handleChange('outerRadius')}
|
||||
{...{ min: 10, max: 100, step: 1 }}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Control Headers with Tooltips
|
||||
```typescript
|
||||
<ControlHeader
|
||||
label={t('Percentage threshold')}
|
||||
description={t('Minimum threshold in percentage points for showing labels.')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
```
|
||||
|
||||
#### Conditional Rendering
|
||||
```typescript
|
||||
{/* Show control only when condition is met */}
|
||||
{formValues.show_labels && (
|
||||
<CheckboxControl
|
||||
label={t('Put labels outside')}
|
||||
description={t('Put the labels outside of the pie?')}
|
||||
value={formValues.labels_outside ?? true}
|
||||
onChange={handleChange('labels_outside')}
|
||||
renderTrigger
|
||||
hovered
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Nested conditional rendering */}
|
||||
{formValues.label_type === 'template' && (
|
||||
<TextControl
|
||||
value={formValues.label_template || ''}
|
||||
onChange={handleChange('label_template')}
|
||||
placeholder="{name}: {value}"
|
||||
controlId="label_template"
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
### 8. Section Organization
|
||||
|
||||
Use HTML headers and spacing for logical groupings:
|
||||
|
||||
```typescript
|
||||
{/* Chart Options Section */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Chart Options')}</h4>
|
||||
|
||||
{/* Controls for this section */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{/* Individual control */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Labels Section */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Labels')}</h4>
|
||||
|
||||
{/* Label-related controls */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 9. Control Defaults in controlOverrides
|
||||
|
||||
Move all default values to the `controlOverrides` section:
|
||||
|
||||
```typescript
|
||||
const config = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: null,
|
||||
expanded: true,
|
||||
controlSetRows: [[PieControlPanel as any]],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
default: [],
|
||||
label: t('Group by'),
|
||||
},
|
||||
metric: {
|
||||
default: null,
|
||||
label: t('Metric'),
|
||||
},
|
||||
show_labels: {
|
||||
default: true,
|
||||
label: t('Show labels'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
color_scheme: {
|
||||
default: 'supersetColors',
|
||||
label: t('Color scheme'),
|
||||
renderTrigger: true,
|
||||
},
|
||||
// ... all other defaults
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 10. Chart Plugin Integration
|
||||
|
||||
Update the chart plugin to use the new control panel:
|
||||
|
||||
```typescript
|
||||
// In your chart's index.ts file
|
||||
import controlPanel from './PieControlPanelSimple'; // New React-based panel
|
||||
|
||||
export default class EchartsPieChartPlugin extends EchartsChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
controlPanel,
|
||||
// ... other config
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Your Migration
|
||||
|
||||
### 1. Visual Validation
|
||||
- [ ] All controls render properly in the UI
|
||||
- [ ] Tab navigation works (if using tabs)
|
||||
- [ ] Control layout matches the original
|
||||
- [ ] Conditional controls show/hide correctly
|
||||
|
||||
### 2. Functional Testing
|
||||
- [ ] Control changes update the chart immediately (if `renderTrigger: true`)
|
||||
- [ ] Form values persist when switching between tabs
|
||||
- [ ] Drag-and-drop controls work with datasource
|
||||
- [ ] Error states display appropriately
|
||||
- [ ] Default values apply correctly
|
||||
|
||||
### 3. Integration Testing
|
||||
- [ ] Control panel works in Explore view
|
||||
- [ ] Values save correctly when creating charts
|
||||
- [ ] Dashboard filters work with the controls
|
||||
- [ ] Chart reloading preserves control values
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: Double Labels on Controls
|
||||
**Problem:** Control shows both ControlHeader label and control's built-in label
|
||||
**Solution:** Set `label=""` on the control when using ControlHeader:
|
||||
```typescript
|
||||
<ControlHeader label={t('Group by')} />
|
||||
<DndColumnSelect
|
||||
label="" // Empty to prevent duplicate
|
||||
// ... other props
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: Slider Min/Max Not Working
|
||||
**Problem:** Slider doesn't respect min/max values
|
||||
**Solution:** Use spread operator with object literal:
|
||||
```typescript
|
||||
<SliderControl
|
||||
value={formValues.outerRadius || 70}
|
||||
onChange={handleChange('outerRadius')}
|
||||
{...{ min: 10, max: 100, step: 1 }} // Use spread with object
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: Controls Not Triggering Chart Updates
|
||||
**Problem:** Chart doesn't refresh when controls change
|
||||
**Solution:** Ensure `renderTrigger` is set where needed:
|
||||
```typescript
|
||||
<CheckboxControl
|
||||
// ... other props
|
||||
renderTrigger // Add this for instant updates
|
||||
/>
|
||||
```
|
||||
|
||||
### Issue: "Cannot read properties of undefined"
|
||||
**Problem:** Attempting to access undefined datasource or form_data
|
||||
**Solution:** Add safety checks and fallbacks:
|
||||
```typescript
|
||||
// Safety checks at component start
|
||||
if (!datasource || !form_data) {
|
||||
return <div>Loading control panel...</div>;
|
||||
}
|
||||
|
||||
// Safe array access
|
||||
const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
|
||||
```
|
||||
|
||||
### Issue: Color Scheme Control Not Working
|
||||
**Problem:** ColorSchemeControl doesn't integrate properly
|
||||
**Solution:** Use the special Control wrapper pattern:
|
||||
```typescript
|
||||
{(() => {
|
||||
const colorSchemeControl = ColorSchemeControl();
|
||||
const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
|
||||
return (
|
||||
<Control
|
||||
{...cleanConfig}
|
||||
name="color_scheme"
|
||||
value={formValues.color_scheme}
|
||||
actions={{
|
||||
...actions,
|
||||
setControlValue: (field: string, val: any) => {
|
||||
handleChange('color_scheme')(val);
|
||||
},
|
||||
}}
|
||||
renderTrigger
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
### Pre-Migration
|
||||
- [ ] Identify all controls in the legacy control panel
|
||||
- [ ] Note any conditional control visibility rules
|
||||
- [ ] Check for custom control configurations
|
||||
- [ ] Understand the chart's specific requirements
|
||||
|
||||
### During Migration
|
||||
- [ ] Create new `[ChartName]ControlPanelSimple.tsx` file
|
||||
- [ ] Implement component structure with proper interface
|
||||
- [ ] Map all legacy controls to React components
|
||||
- [ ] Add safety checks for datasource/form_data
|
||||
- [ ] Implement tab structure (Data/Customize)
|
||||
- [ ] Add all control defaults to `controlOverrides`
|
||||
- [ ] Mark component as modern with `isModernPanel = true`
|
||||
- [ ] Update chart plugin to import new control panel
|
||||
|
||||
### Post-Migration Testing
|
||||
- [ ] Test all control interactions
|
||||
- [ ] Verify chart updates on control changes
|
||||
- [ ] Check conditional control visibility
|
||||
- [ ] Validate default values
|
||||
- [ ] Test with different datasources
|
||||
- [ ] Run pre-commit hooks: `pre-commit run`
|
||||
- [ ] Test in Explore and Dashboard contexts
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Dynamic Control Visibility
|
||||
```typescript
|
||||
// Show additional controls based on current selection
|
||||
{formValues.chart_type === 'pie' && (
|
||||
<div>
|
||||
{/* Pie-specific controls */}
|
||||
<CheckboxControl
|
||||
label={t('Show as Donut')}
|
||||
value={formValues.donut ?? false}
|
||||
onChange={handleChange('donut')}
|
||||
/>
|
||||
|
||||
{formValues.donut && (
|
||||
<SliderControl
|
||||
value={formValues.innerRadius || 30}
|
||||
onChange={handleChange('innerRadius')}
|
||||
{...{ min: 0, max: 100, step: 1 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
### Complex Control Groups
|
||||
```typescript
|
||||
{/* Side-by-side controls */}
|
||||
<div style={{ display: 'flex', gap: 16 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ControlHeader label={t('Min Value')} />
|
||||
<TextControl
|
||||
value={formValues.min_value}
|
||||
onChange={handleChange('min_value')}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<ControlHeader label={t('Max Value')} />
|
||||
<TextControl
|
||||
value={formValues.max_value}
|
||||
onChange={handleChange('max_value')}
|
||||
isFloat
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Custom Validation
|
||||
```typescript
|
||||
// Add validation logic to handleChange
|
||||
const handleChange = (field: string) => (val: any) => {
|
||||
// Custom validation
|
||||
if (field === 'row_limit' && val && val < 1) {
|
||||
console.warn('Row limit must be positive');
|
||||
return;
|
||||
}
|
||||
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(field, val);
|
||||
} else if (onChange) {
|
||||
onChange(field, val);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Additional Migration Patterns
|
||||
|
||||
### Single Column Selection
|
||||
When a control expects a single column value (not an array):
|
||||
```typescript
|
||||
// For Sankey source/target columns
|
||||
const handleSingleColumnChange = (field: string) => (val: any) => {
|
||||
const singleValue = Array.isArray(val) ? val[0] : val;
|
||||
actions.setControlValue(field, singleValue);
|
||||
};
|
||||
|
||||
// Usage
|
||||
<DndColumnSelect
|
||||
value={formValues.source ? [formValues.source] : []}
|
||||
onChange={handleSingleColumnChange('source')}
|
||||
options={safeColumns}
|
||||
multi={false}
|
||||
/>
|
||||
```
|
||||
|
||||
### Required Field Validation
|
||||
For controls that must have values:
|
||||
```typescript
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
|
||||
// In controlOverrides
|
||||
source: {
|
||||
validators: [validateNonEmpty],
|
||||
label: t('Source Column'),
|
||||
},
|
||||
```
|
||||
|
||||
### Chart Type Descriptions
|
||||
Add helpful descriptions at the top of control panels:
|
||||
```typescript
|
||||
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
|
||||
{t('Sankey Diagram')}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.65 }}>
|
||||
{t('Visualize flow between different entities')}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Common Migration Issues & Solutions
|
||||
|
||||
### Issue 1: "Cannot read properties of undefined (reading 'map')"
|
||||
**Problem:** DndColumnSelect crashes when datasource is undefined
|
||||
**Solution:** Pass `options={datasource?.columns || []}` instead of `datasource={datasource}`
|
||||
|
||||
### Issue 2: Tabs import error ("Element type is invalid")
|
||||
**Problem:** Runtime error when loading control panel
|
||||
**Solution:** Import from 'antd' directly: `import { Tabs } from 'antd';` (NOT from '@superset-ui/core')
|
||||
|
||||
### Issue 3: React hooks error
|
||||
**Problem:** "React Hook 'useState' is called conditionally"
|
||||
**Solution:** Always declare state hooks before any conditional returns:
|
||||
```typescript
|
||||
const [activeTab, setActiveTab] = useState('data'); // FIRST
|
||||
if (!datasource) return <div>Loading...</div>; // THEN conditions
|
||||
```
|
||||
|
||||
### Issue 4: ESLint color literal warnings
|
||||
**Problem:** theme-colors/no-literal-colors ESLint rule
|
||||
**Solution:** Use opacity instead of color literals:
|
||||
```typescript
|
||||
// Bad: style={{ color: '#666' }}
|
||||
// Good: style={{ opacity: 0.65 }}
|
||||
```
|
||||
|
||||
### Issue 5: Single value vs array handling
|
||||
**Problem:** Some controls expect single values but DndColumnSelect returns arrays
|
||||
**Solution:** See "Single Column Selection" pattern above
|
||||
|
||||
### Issue 6: antd import warnings
|
||||
**Problem:** "'antd' should be listed in the project's dependencies"
|
||||
**Solution:** Use `SKIP=eslint-frontend` when committing if antd is already available
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The Pie chart control panel migration (`PieControlPanelSimple.tsx`) serves as the definitive reference implementation showing:
|
||||
|
||||
- Complete tab-based layout (Data/Customize)
|
||||
- All major control types (DndColumnSelect, CheckboxControl, SelectControl, SliderControl, etc.)
|
||||
- Conditional control rendering
|
||||
- Proper safety checks and error handling
|
||||
- Color scheme integration
|
||||
- Control grouping and organization
|
||||
- Modern React patterns and TypeScript usage
|
||||
|
||||
Study this implementation for best practices and patterns that can be applied to any chart control panel migration.
|
||||
|
||||
## Summary
|
||||
|
||||
The new React-based control panel approach provides:
|
||||
|
||||
1. **Better Developer Experience** - Direct React components with TypeScript
|
||||
2. **Improved Maintainability** - Clear component structure and patterns
|
||||
3. **Enhanced Flexibility** - Easy conditional rendering and dynamic controls
|
||||
4. **Type Safety** - Full TypeScript support with proper interfaces
|
||||
5. **Simplified Architecture** - No complex config intermediaries
|
||||
|
||||
The migration process involves converting string references and config objects to direct React components, implementing proper safety checks, and organizing controls in a logical tab-based structure. The key is to maintain compatibility with the existing Superset infrastructure while providing a more modern and maintainable development experience.
|
||||
@@ -1,153 +0,0 @@
|
||||
# Control Panel Modernization Guide
|
||||
|
||||
## Current State
|
||||
|
||||
Apache Superset's control panels currently use a legacy `controlSetRows` structure that relies on nested arrays to define layout. This approach has several limitations:
|
||||
|
||||
1. **Rigid Layout**: The nested array structure makes it difficult to create responsive or complex layouts
|
||||
2. **Poor Type Safety**: Arrays of arrays don't provide good TypeScript support
|
||||
3. **Mixed Paradigms**: String references, configuration objects, and React components are mixed together
|
||||
4. **Limited Reusability**: Layout logic is embedded in the structure rather than using composable components
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Component Modernization ✅ COMPLETED
|
||||
- Replaced string-based control references with React components
|
||||
- Updated individual control components to use Ant Design
|
||||
- Modernized the `ControlRow` component to use Ant Design's Grid
|
||||
|
||||
### Phase 2: Layout Utilities ✅ COMPLETED
|
||||
- Created `ControlPanelLayout.tsx` with reusable layout components
|
||||
- Implemented `ControlSection`, `SingleControlRow`, `TwoColumnRow`, `ThreeColumnRow`
|
||||
- Updated control group components to use Ant Design Row/Col
|
||||
|
||||
### Phase 3: React-Based Control Panels 🚧 IN PROGRESS
|
||||
- Create `ReactControlPanel` component for rendering modern panels
|
||||
- Support both legacy and modern formats during transition
|
||||
- Provide migration helpers and examples
|
||||
|
||||
### Phase 4: Gradual Migration 📋 TODO
|
||||
- Migrate chart control panels one by one
|
||||
- Start with simpler charts (Pie, Bar) before complex ones
|
||||
- Maintain backward compatibility throughout
|
||||
|
||||
## Modern Control Panel Structure
|
||||
|
||||
### Legacy Structure (controlSetRows)
|
||||
```typescript
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[GroupByControl()],
|
||||
[MetricControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[RowLimitControl()],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Modern Structure (React Components)
|
||||
```typescript
|
||||
const modernConfig: ReactControlPanelConfig = {
|
||||
sections: [
|
||||
{
|
||||
key: 'query',
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
render: ({ values, onChange }) => (
|
||||
<>
|
||||
<SingleControlRow>
|
||||
<GroupByControl value={values.groupby} onChange={onChange} />
|
||||
</SingleControlRow>
|
||||
<SingleControlRow>
|
||||
<MetricControl value={values.metrics} onChange={onChange} />
|
||||
</SingleControlRow>
|
||||
<TwoColumnRow
|
||||
left={<AdhocFiltersControl value={values.adhoc_filters} onChange={onChange} />}
|
||||
right={<RowLimitControl value={values.row_limit} onChange={onChange} />}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Benefits of Modernization
|
||||
|
||||
1. **Better Type Safety**: Full TypeScript support with proper interfaces
|
||||
2. **Flexible Layouts**: Use Ant Design's Grid system for responsive layouts
|
||||
3. **Cleaner Code**: React components instead of nested arrays
|
||||
4. **Improved DX**: Better IDE support and autocomplete
|
||||
5. **Easier Testing**: Component-based architecture is easier to test
|
||||
6. **Consistent Styling**: Leverage Ant Design's theme system
|
||||
|
||||
## Migration Example
|
||||
|
||||
To migrate a control panel:
|
||||
|
||||
1. **Create a modern version** alongside the existing one:
|
||||
```typescript
|
||||
// controlPanelModern.tsx
|
||||
export const modernConfig: ReactControlPanelConfig = {
|
||||
sections: [/* ... */]
|
||||
};
|
||||
```
|
||||
|
||||
2. **Use the compatibility wrapper** for backward compatibility:
|
||||
```typescript
|
||||
export default createReactControlPanel(modernConfig);
|
||||
```
|
||||
|
||||
3. **Update the chart plugin** to use the new control panel:
|
||||
```typescript
|
||||
import controlPanel from './controlPanelModern';
|
||||
```
|
||||
|
||||
## Layout Components Available
|
||||
|
||||
- `ControlSection`: Collapsible section container
|
||||
- `SingleControlRow`: Full-width single control
|
||||
- `TwoColumnRow`: Two controls side by side (50/50)
|
||||
- `ThreeColumnRow`: Three controls in a row (33/33/33)
|
||||
- `Row` and `Col` from Ant Design for custom layouts
|
||||
|
||||
## Files Created
|
||||
|
||||
1. `packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx`
|
||||
- Layout utility components
|
||||
|
||||
2. `packages/superset-ui-chart-controls/src/shared-controls/components/ModernControlPanelExample.tsx`
|
||||
- Example of modern control panel structure
|
||||
|
||||
3. `plugins/plugin-chart-echarts/src/Pie/controlPanelModern.tsx`
|
||||
- Modern version of Pie chart control panel
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Complete the ReactControlPanel integration** with ControlPanelsContainer
|
||||
2. **Create migration tooling** to help convert existing panels
|
||||
3. **Document best practices** for control panel design
|
||||
4. **Update chart plugin template** to use modern structure
|
||||
5. **Gradually migrate all 90+ control panels** in the codebase
|
||||
|
||||
## Technical Debt Addressed
|
||||
|
||||
- Eliminates nested array layout structure
|
||||
- Removes string-based control references
|
||||
- Reduces coupling between layout and configuration
|
||||
- Improves maintainability and testability
|
||||
- Enables better code splitting and lazy loading
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The migration maintains full backward compatibility:
|
||||
- Existing control panels continue to work
|
||||
- Both formats can coexist during migration
|
||||
- No breaking changes to the public API
|
||||
- Charts can be migrated incrementally
|
||||
74
LLMS.md
74
LLMS.md
@@ -189,77 +189,3 @@ pre-commit run eslint # Frontend linting
|
||||
---
|
||||
|
||||
**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
|
||||
|
||||
## Control Panel Migration Guide
|
||||
|
||||
### New React-Based Control Panel Architecture
|
||||
|
||||
We've successfully migrated from the complex string-referenced control panel system to a simpler React-based approach. Here's how to migrate existing control panels:
|
||||
|
||||
#### Key Changes
|
||||
1. **No more string references** - Use React component functions instead of strings
|
||||
2. **Redux integration** - Controls connect directly to Redux via useFormData hook
|
||||
3. **Type safety** - Full TypeScript support with proper types
|
||||
4. **Simplified architecture** - Direct React components, no JSON intermediary
|
||||
|
||||
#### Migration Steps
|
||||
|
||||
##### 1. Update imports
|
||||
Replace string references with component imports:
|
||||
```typescript
|
||||
// OLD
|
||||
controlSetRows: [
|
||||
['series'],
|
||||
['metric'],
|
||||
]
|
||||
|
||||
// NEW
|
||||
import { SeriesControl, MetricControl } from '@superset-ui/chart-controls';
|
||||
controlSetRows: [
|
||||
[SeriesControl()],
|
||||
[MetricControl()],
|
||||
]
|
||||
```
|
||||
|
||||
##### 2. Custom controls
|
||||
Use InlineTextControl or InlineSelectControl for custom controls:
|
||||
```typescript
|
||||
// OLD
|
||||
{
|
||||
name: 'size_from',
|
||||
config: { type: 'TextControl', ... }
|
||||
}
|
||||
|
||||
// NEW
|
||||
InlineTextControl('sizeFrom', {
|
||||
label: t('Minimum Font Size'),
|
||||
default: 10,
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
##### 3. Control naming
|
||||
Use camelCase for new controls, but handle both in transformProps:
|
||||
```typescript
|
||||
// In transformProps.ts
|
||||
const finalSizeFrom = sizeFrom ?? size_from ?? 10;
|
||||
```
|
||||
|
||||
#### Available Control Components
|
||||
All these return CustomControlItem and can be used in controlSetRows:
|
||||
- `SeriesControl()` - Column selection
|
||||
- `MetricControl()` - Metric selection
|
||||
- `AdhocFiltersControl()` - Filter configuration
|
||||
- `RowLimitControl()` - Row limit
|
||||
- `ColorSchemeControl()` - Color scheme picker
|
||||
- `InlineTextControl(name, config)` - Custom text input
|
||||
- `InlineSelectControl(name, config)` - Custom select
|
||||
|
||||
#### Example Migration
|
||||
See `plugins/plugin-chart-word-cloud/src/plugin/controlPanelFixed.ts` for a complete example.
|
||||
|
||||
# important-instruction-reminders
|
||||
Do what has been asked; nothing more, nothing less.
|
||||
NEVER create files unless they're absolutely necessary for achieving your goal.
|
||||
ALWAYS prefer editing an existing file to creating a new one.
|
||||
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
# Pie Chart Control Panel Migration - Phased Approach
|
||||
|
||||
## Phase 1: Parallel Implementation ✅ COMPLETED
|
||||
|
||||
We've created a modern control panel alongside the legacy one:
|
||||
|
||||
### Files Created:
|
||||
1. **`controlPanelModern.tsx`** - Modern React-based control panel
|
||||
2. **`ModernControlPanelRenderer.tsx`** - Bridge component for compatibility
|
||||
3. **Updated `ControlPanelsContainer.tsx`** - Support for modern panels
|
||||
|
||||
### Key Features:
|
||||
- Full React component structure (no `controlSetRows`)
|
||||
- Uses Ant Design Grid directly
|
||||
- Type-safe with TypeScript interfaces
|
||||
- Conditional rendering based on form values
|
||||
- Organized into logical sections
|
||||
|
||||
## Phase 2: Integration Testing 🚧 NEXT STEP
|
||||
|
||||
### 2.1 Update the Pie Chart Plugin
|
||||
|
||||
```typescript
|
||||
// In plugins/plugin-chart-echarts/src/Pie/index.ts
|
||||
import controlPanel from './controlPanel'; // Legacy
|
||||
import controlPanelModern from './controlPanelModern'; // Modern
|
||||
|
||||
// Feature flag to toggle between old and new
|
||||
const useModernPanel = window.featureFlags?.MODERN_CONTROL_PANELS;
|
||||
|
||||
export default class EchartsPieChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
// ... other config
|
||||
controlPanel: useModernPanel ? controlPanelModern : controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Test the Modern Panel
|
||||
|
||||
Create test file to verify both panels produce same output:
|
||||
|
||||
```typescript
|
||||
// controlPanel.test.tsx
|
||||
describe('Pie Control Panel Migration', () => {
|
||||
it('modern panel handles all legacy controls', () => {
|
||||
// Test that all controls from legacy panel exist in modern
|
||||
});
|
||||
|
||||
it('produces same form_data structure', () => {
|
||||
// Verify form_data compatibility
|
||||
});
|
||||
|
||||
it('visibility conditions work correctly', () => {
|
||||
// Test conditional rendering
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Phase 3: Feature Flag Rollout
|
||||
|
||||
### 3.1 Add Feature Flag
|
||||
|
||||
```python
|
||||
# In superset/config.py
|
||||
FEATURE_FLAGS = {
|
||||
"MODERN_CONTROL_PANELS": False, # Start disabled
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Gradual Rollout
|
||||
|
||||
1. **Internal Testing**: Enable for development environment
|
||||
2. **Beta Users**: Enable for select users (5%)
|
||||
3. **Wider Rollout**: Increase to 50%
|
||||
4. **Full Migration**: Enable for all users
|
||||
5. **Cleanup**: Remove legacy code
|
||||
|
||||
## Phase 4: Migration Utilities
|
||||
|
||||
### 4.1 Control Panel Converter
|
||||
|
||||
```typescript
|
||||
// convertLegacyPanel.ts
|
||||
export function convertControlSetRows(rows: ControlSetRow[]): ReactElement {
|
||||
return rows.map(row => {
|
||||
if (row.length === 1) {
|
||||
return <SingleControlRow>{convertControl(row[0])}</SingleControlRow>;
|
||||
}
|
||||
if (row.length === 2) {
|
||||
return (
|
||||
<TwoColumnRow
|
||||
left={convertControl(row[0])}
|
||||
right={convertControl(row[1])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// ... handle other cases
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Common Patterns Library
|
||||
|
||||
```typescript
|
||||
// commonPanelPatterns.tsx
|
||||
export const QuerySection = ({ values, onChange }) => (
|
||||
<>
|
||||
<GroupByControl />
|
||||
<MetricControl />
|
||||
<AdhocFiltersControl />
|
||||
<RowLimitControl />
|
||||
</>
|
||||
);
|
||||
|
||||
export const AppearanceSection = ({ values, onChange }) => (
|
||||
<>
|
||||
<ColorSchemeControl />
|
||||
<OpacityControl />
|
||||
<LegendControls />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
## Phase 5: Migrate Other Charts
|
||||
|
||||
### Priority Order (Simple to Complex):
|
||||
|
||||
1. **Simple Charts** (1-2 weeks each)
|
||||
- Bar Chart
|
||||
- Line Chart
|
||||
- Area Chart
|
||||
- Scatter Plot
|
||||
|
||||
2. **Medium Complexity** (2-3 weeks each)
|
||||
- Table
|
||||
- Pivot Table
|
||||
- Heatmap
|
||||
- Treemap
|
||||
|
||||
3. **Complex Charts** (3-4 weeks each)
|
||||
- Mixed Time Series
|
||||
- Box Plot
|
||||
- Sankey
|
||||
- Graph/Network
|
||||
|
||||
### Migration Checklist per Chart:
|
||||
|
||||
- [ ] Create `controlPanelModern.tsx`
|
||||
- [ ] Update plugin index to support both
|
||||
- [ ] Write migration tests
|
||||
- [ ] Test with feature flag
|
||||
- [ ] Document any chart-specific patterns
|
||||
- [ ] Update TypeScript types if needed
|
||||
|
||||
## Phase 6: System-Wide Updates
|
||||
|
||||
### 6.1 Update Control Panel Registry
|
||||
|
||||
```typescript
|
||||
// getChartControlPanelRegistry.ts
|
||||
export interface ModernControlPanelRegistry {
|
||||
get(key: string): ControlPanelConfig | ReactControlPanelConfig;
|
||||
registerModern(key: string, config: ReactControlPanelConfig): void;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Update Explore Components
|
||||
|
||||
- `ControlPanelsContainer` - Full support for modern panels ✅
|
||||
- `Control` - Ensure all control types work
|
||||
- `ControlRow` - Already modernized ✅
|
||||
- `getSectionsToRender` - Update to handle React components
|
||||
|
||||
### 6.3 Update Types
|
||||
|
||||
```typescript
|
||||
// types.ts
|
||||
export type ControlPanelConfig = LegacyControlPanelConfig | ModernControlPanelConfig;
|
||||
|
||||
export interface ModernControlPanelConfig {
|
||||
type: 'modern';
|
||||
sections: ReactControlPanelSection[];
|
||||
controlOverrides?: ControlOverrides;
|
||||
formDataOverrides?: FormDataOverrides;
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits Tracking
|
||||
|
||||
### Metrics to Monitor:
|
||||
1. **Developer Velocity**: Time to add new controls
|
||||
2. **Bug Rate**: Control panel-related issues
|
||||
3. **Performance**: Rendering time for control panels
|
||||
4. **Type Safety**: TypeScript coverage percentage
|
||||
5. **Code Maintainability**: Lines of code, complexity metrics
|
||||
|
||||
### Expected Improvements:
|
||||
- 50% reduction in control panel code
|
||||
- 80% reduction in control panel bugs
|
||||
- 100% TypeScript coverage
|
||||
- 30% faster control panel rendering
|
||||
- Easier onboarding for new developers
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
|
||||
1. **Feature Flag**: Immediately disable `MODERN_CONTROL_PANELS`
|
||||
2. **Hotfix**: Revert to legacy panel for affected charts
|
||||
3. **Investigation**: Debug issues in staging environment
|
||||
4. **Fix Forward**: Address issues and re-enable gradually
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Phase 1**: ✅ Completed
|
||||
- **Phase 2**: 1 week (testing and integration)
|
||||
- **Phase 3**: 2 weeks (feature flag and rollout)
|
||||
- **Phase 4**: 1 week (utilities and patterns)
|
||||
- **Phase 5**: 3-6 months (all charts migration)
|
||||
- **Phase 6**: 2 weeks (system updates)
|
||||
- **Cleanup**: 1 week (remove legacy code)
|
||||
|
||||
**Total: 4-7 months for complete migration**
|
||||
|
||||
## Next Immediate Steps
|
||||
|
||||
1. Test the modern Pie control panel in development
|
||||
2. Fix any issues with value binding and onChange handlers
|
||||
3. Create feature flag in Python backend
|
||||
4. Write comprehensive tests
|
||||
5. Get team buy-in on approach
|
||||
6. Start incremental migration
|
||||
|
||||
## Code Snippets for Testing
|
||||
|
||||
```bash
|
||||
# Test the modern panel
|
||||
cd superset-frontend
|
||||
npm run dev
|
||||
|
||||
# In browser console
|
||||
window.featureFlags = { MODERN_CONTROL_PANELS: true };
|
||||
|
||||
# Create a new Pie chart and verify controls work
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All control panels migrated to modern format
|
||||
- [ ] No regression in functionality
|
||||
- [ ] Improved developer experience
|
||||
- [ ] Better performance metrics
|
||||
- [ ] Reduced maintenance burden
|
||||
- [ ] Full TypeScript coverage
|
||||
@@ -28,7 +28,6 @@ These features are considered **unfinished** and should only be used on developm
|
||||
[//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY"
|
||||
|
||||
- ALERT_REPORT_TABS
|
||||
- DATE_RANGE_TIMESHIFTS_ENABLED
|
||||
- ENABLE_ADVANCED_DATA_TYPES
|
||||
- PRESTO_EXPAND_DATA
|
||||
- SHARE_QUERIES_VIA_KV_STORE
|
||||
|
||||
@@ -23,6 +23,8 @@ This file documents any backwards-incompatible changes in Superset and
|
||||
assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
- [34346](https://github.com/apache/superset/pull/34346) The examples system has been migrated from Python-based scripts to YAML configuration files. The CLI command `superset load-examples` has been deprecated in favor of `superset examples load`. The old command still works but will show a deprecation warning. Additional example management commands are available under `superset examples` including `clear-old` and `reload`. If you have old Python-based examples loaded, the new YAML-based examples will not load automatically to preserve your existing data. To migrate to the new examples, run `superset examples clear-old --confirm` followed by `superset examples load`.
|
||||
**Note**: This change affects Cypress tests that rely on specific chart names from the old examples (e.g., "Num Births Trend", "Daily Totals"). These charts may not exist in the new YAML examples, causing test failures. Consider updating your Cypress tests or creating test-specific fixtures.
|
||||
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
|
||||
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
|
||||
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
|
||||
@@ -32,7 +34,6 @@ assists people when migrating to a new version.
|
||||
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
|
||||
- [31590](https://github.com/apache/superset/pull/31590) Marks the begining of intricate work around supporting dynamic Theming, and breaks support for [THEME_OVERRIDES](https://github.com/apache/superset/blob/732de4ac7fae88e29b7f123b6cbb2d7cd411b0e4/superset/config.py#L671) in favor of a new theming system based on AntD V5. Likely this will be in disrepair until settling over the 5.x lifecycle.
|
||||
- [32432](https://github.com/apache/superset/pull/31260) Moves the List Roles FAB view to the frontend and requires `FAB_ADD_SECURITY_API` to be enabled in the configuration and `superset init` to be executed.
|
||||
- [34319](https://github.com/apache/superset/pull/34319) Drill to Detail and Drill By is now supported in Embedded mode, and also with the `DASHBOARD_RBAC` FF. If you don't want to expose these features in Embedded / `DASHBOARD_RBAC`, make sure the roles used for Embedded / `DASHBOARD_RBAC`don't have the required permissions to perform D2D actions.
|
||||
|
||||
## 5.0.0
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
# If you choose to use this type of deployment make sure to
|
||||
# create you own docker environment file (docker/.env) with your own
|
||||
# unique random secure passwords and SECRET_KEY.
|
||||
#
|
||||
# For verbose logging during development:
|
||||
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
|
||||
# -----------------------------------------------------------------------
|
||||
x-superset-image: &superset-image apachesuperset.docker.scarf.sh/apache/superset:${TAG:-latest-dev}
|
||||
x-superset-volumes:
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
# If you choose to use this type of deployment make sure to
|
||||
# create you own docker environment file (docker/.env) with your own
|
||||
# unique random secure passwords and SECRET_KEY.
|
||||
#
|
||||
# For verbose logging during development:
|
||||
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
|
||||
# -----------------------------------------------------------------------
|
||||
x-superset-volumes:
|
||||
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
# If you choose to use this type of deployment make sure to
|
||||
# create you own docker environment file (docker/.env) with your own
|
||||
# unique random secure passwords and SECRET_KEY.
|
||||
#
|
||||
# For verbose logging during development:
|
||||
# - Set SUPERSET_LOG_LEVEL=debug in docker/.env-local for detailed Superset logs
|
||||
# -----------------------------------------------------------------------
|
||||
x-superset-user: &superset-user root
|
||||
x-superset-volumes: &superset-volumes
|
||||
|
||||
@@ -53,12 +53,7 @@ PYTHONPATH=/app/pythonpath:/app/docker/pythonpath_dev
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Development and logging configuration
|
||||
# FLASK_DEBUG: Enables Flask dev features (auto-reload, better error pages) - keep 'true' for development
|
||||
FLASK_DEBUG=true
|
||||
# SUPERSET_LOG_LEVEL: Controls Superset application logging verbosity (debug, info, warning, error, critical)
|
||||
SUPERSET_LOG_LEVEL=info
|
||||
|
||||
SUPERSET_APP_ROOT="/"
|
||||
SUPERSET_ENV=development
|
||||
SUPERSET_LOAD_EXAMPLES=yes
|
||||
@@ -71,3 +66,4 @@ SUPERSET_SECRET_KEY=TEST_NON_DEV_SECRET
|
||||
ENABLE_PLAYWRIGHT=false
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true
|
||||
SUPERSET_LOG_LEVEL=info
|
||||
|
||||
@@ -23,57 +23,25 @@ MIN_MEM_FREE_GB=3
|
||||
MIN_MEM_FREE_KB=$(($MIN_MEM_FREE_GB*1000000))
|
||||
|
||||
echo_mem_warn() {
|
||||
# Check if running in Codespaces first
|
||||
if [[ -n "${CODESPACES}" ]]; then
|
||||
echo "Memory available: Codespaces managed"
|
||||
return
|
||||
fi
|
||||
MEM_FREE_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_FREE_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
|
||||
# Check platform and get memory accordingly
|
||||
if [[ -f /proc/meminfo ]]; then
|
||||
# Linux
|
||||
if grep -q MemAvailable /proc/meminfo; then
|
||||
MEM_AVAIL_KB=$(awk '/MemAvailable/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_AVAIL_GB=$(awk '/MemAvailable/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
else
|
||||
MEM_AVAIL_KB=$(awk '/MemFree/ { printf "%s \n", $2 }' /proc/meminfo)
|
||||
MEM_AVAIL_GB=$(awk '/MemFree/ { printf "%s \n", $2/1024/1024 }' /proc/meminfo)
|
||||
fi
|
||||
elif [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS - use vm_stat to get free memory
|
||||
# vm_stat reports in pages, typically 4096 bytes per page
|
||||
PAGE_SIZE=$(pagesize)
|
||||
FREE_PAGES=$(vm_stat | awk '/Pages free:/ {print $3}' | tr -d '.')
|
||||
INACTIVE_PAGES=$(vm_stat | awk '/Pages inactive:/ {print $3}' | tr -d '.')
|
||||
# Free + inactive pages give us available memory (similar to MemAvailable on Linux)
|
||||
AVAIL_PAGES=$((FREE_PAGES + INACTIVE_PAGES))
|
||||
MEM_AVAIL_KB=$((AVAIL_PAGES * PAGE_SIZE / 1024))
|
||||
MEM_AVAIL_GB=$(echo "scale=2; $MEM_AVAIL_KB / 1024 / 1024" | bc)
|
||||
else
|
||||
# Other platforms
|
||||
echo "Memory available: Unable to determine"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${MEM_AVAIL_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
|
||||
if [[ "${MEM_FREE_KB}" -lt "${MIN_MEM_FREE_KB}" ]]; then
|
||||
cat <<EOF
|
||||
===============================================
|
||||
======== Memory Insufficient Warning =========
|
||||
===============================================
|
||||
|
||||
It looks like you only have ${MEM_AVAIL_GB}GB of
|
||||
memory ${MEM_TYPE}. Please increase your Docker
|
||||
It looks like you only have ${MEM_FREE_GB}GB of
|
||||
memory free. Please increase your Docker
|
||||
resources to at least ${MIN_MEM_FREE_GB}GB
|
||||
|
||||
Note: During builds, available memory may be
|
||||
temporarily low due to caching and compilation.
|
||||
|
||||
===============================================
|
||||
======== Memory Insufficient Warning =========
|
||||
===============================================
|
||||
EOF
|
||||
else
|
||||
echo "Memory available: ${MEM_AVAIL_GB} GB"
|
||||
echo "Memory check Ok [${MEM_FREE_GB}GB free]"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
1
docker/pythonpath_dev/.gitignore
vendored
1
docker/pythonpath_dev/.gitignore
vendored
@@ -20,5 +20,4 @@
|
||||
# DON'T ignore the .gitignore
|
||||
!.gitignore
|
||||
!superset_config.py
|
||||
!superset_config_docker_light.py
|
||||
!superset_config_local.example
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# mypy: disable-error-code="assignment,misc"
|
||||
#
|
||||
# This file is included in the final Docker image and SHOULD be overridden when
|
||||
# deploying the image to prod. Settings configured here are intended for use in local
|
||||
|
||||
@@ -87,66 +87,8 @@ Restart Superset to apply changes.
|
||||
3. **Apply**: Assign themes to specific dashboards or configure instance-wide
|
||||
4. **Iterate**: Modify theme JSON directly in the CRUD interface or re-import from the theme editor
|
||||
|
||||
## Custom Fonts
|
||||
|
||||
Superset supports custom fonts through runtime configuration, allowing you to use branded or custom typefaces without rebuilding the application.
|
||||
|
||||
### Configuring Custom Fonts
|
||||
|
||||
Add font URLs to your `superset_config.py`:
|
||||
|
||||
```python
|
||||
# Load fonts from Google Fonts, Adobe Fonts, or self-hosted sources
|
||||
CUSTOM_FONT_URLS = [
|
||||
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
|
||||
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap",
|
||||
]
|
||||
|
||||
# Update CSP to allow font sources
|
||||
TALISMAN_CONFIG = {
|
||||
"content_security_policy": {
|
||||
"font-src": ["'self'", "https://fonts.googleapis.com", "https://fonts.gstatic.com"],
|
||||
"style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Custom Fonts in Themes
|
||||
|
||||
Once configured, reference the fonts in your theme configuration:
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
"fontFamilyCode": "JetBrains Mono, Monaco, monospace",
|
||||
# ... other theme tokens
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or in the CRUD interface theme JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
"fontFamilyCode": "JetBrains Mono, Monaco, monospace"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Font Sources
|
||||
|
||||
- **Google Fonts**: Free, CDN-hosted fonts with wide variety
|
||||
- **Adobe Fonts**: Premium fonts (requires subscription and kit ID)
|
||||
- **Self-hosted**: Place font files in `/static/assets/fonts/` and reference via CSS
|
||||
|
||||
This feature works with the stock Docker image - no custom build required!
|
||||
|
||||
## Advanced Features
|
||||
|
||||
- **System Themes**: Superset includes built-in light and dark themes
|
||||
- **Per-Dashboard Theming**: Each dashboard can have its own visual identity
|
||||
- **JSON Editor**: Edit theme configurations directly within Superset's interface
|
||||
- **Custom Fonts**: Load external fonts via configuration without rebuilding
|
||||
|
||||
@@ -137,7 +137,7 @@ contributing to Apache Superset more accessible to developers worldwide.
|
||||
|
||||
1. **Create a Codespace**: Use this pre-configured link that sets up everything you need:
|
||||
|
||||
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=master&devcontainer_path=.devcontainer%2Fdevcontainer.json&geo=UsWest)
|
||||
[**Launch Superset Codespace →**](https://github.com/codespaces/new?skip_quickstart=true&machine=standardLinux32gb&repo=39464018&ref=codespaces&geo=UsWest&devcontainer_path=.devcontainer%2Fdevcontainer.json)
|
||||
|
||||
:::caution
|
||||
**Important**: You must select at least the **4 CPU / 16GB RAM** machine type (pre-selected in the link above).
|
||||
@@ -348,7 +348,7 @@ superset init
|
||||
|
||||
# Load some data to play with.
|
||||
# Note: you MUST have previously created an admin user with the username `admin` for this command to work.
|
||||
superset load-examples
|
||||
superset examples load
|
||||
|
||||
# Start the Flask dev web server from inside your virtualenv.
|
||||
# Note that your page may not have CSS at this point.
|
||||
@@ -421,6 +421,14 @@ Then make sure you run your WSGI server using the right worker type:
|
||||
gunicorn "superset.app:create_app()" -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -b 127.0.0.1:8088 --reload
|
||||
```
|
||||
|
||||
You can log anything to the browser console, including objects:
|
||||
|
||||
```python
|
||||
from superset import app
|
||||
app.logger.error('An exception occurred!')
|
||||
app.logger.info(form_data)
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in order to properly display the web UI. The `superset-frontend` directory contains all NPM-managed frontend assets. Note that for some legacy pages there are additional frontend assets bundled with Flask-Appbuilder (e.g. jQuery and bootstrap). These are not managed by NPM and may be phased out in the future.
|
||||
|
||||
@@ -26,14 +26,11 @@ Superset locally is using Docker Compose on a Linux or Mac OSX
|
||||
computer. Superset does not have official support for Windows. It's also the easiest
|
||||
way to launch a fully functioning **development environment** quickly.
|
||||
|
||||
Note that there are 4 major ways we support to run `docker compose`:
|
||||
Note that there are 3 major ways we support to run `docker compose`:
|
||||
|
||||
1. **docker-compose.yml:** for interactive development, where we mount your local folder with the
|
||||
frontend/backend files that you can edit and experience the changes you
|
||||
make in the app in real time
|
||||
1. **docker-compose-light.yml:** a lightweight configuration with minimal services (database,
|
||||
Superset app, and frontend dev server) for development. Uses in-memory caching instead of Redis
|
||||
and is designed for running multiple instances simultaneously
|
||||
1. **docker-compose-non-dev.yml** where we just build a more immutable image based on the
|
||||
local branch and get all the required images running. Changes in the local branch
|
||||
at the time you fire this up will be reflected, but changes to the code
|
||||
@@ -47,7 +44,7 @@ Note that there are 4 major ways we support to run `docker compose`:
|
||||
The `dev` builds include the `psycopg2-binary` required to connect
|
||||
to the Postgres database launched as part of the `docker compose` builds.
|
||||
|
||||
More on these approaches after setting up the requirements for either.
|
||||
More on these two approaches after setting up the requirements for either.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -106,36 +103,13 @@ and help you start fresh. In the context of `docker compose` setting
|
||||
from within docker. This will slow down the startup, but will fix various npm-related issues.
|
||||
:::
|
||||
|
||||
### Option #2 - lightweight development with multiple instances
|
||||
|
||||
For a lighter development setup that uses fewer resources and supports running multiple instances:
|
||||
|
||||
```bash
|
||||
# Single lightweight instance (default port 9001)
|
||||
docker compose -f docker-compose-light.yml up
|
||||
|
||||
# Multiple instances with different ports
|
||||
NODE_PORT=9001 docker compose -p superset-1 -f docker-compose-light.yml up
|
||||
NODE_PORT=9002 docker compose -p superset-2 -f docker-compose-light.yml up
|
||||
NODE_PORT=9003 docker compose -p superset-3 -f docker-compose-light.yml up
|
||||
```
|
||||
|
||||
This configuration includes:
|
||||
- PostgreSQL database (internal network only)
|
||||
- Superset application server
|
||||
- Frontend development server with webpack hot reloading
|
||||
- In-memory caching (no Redis)
|
||||
- Isolated volumes and networks per instance
|
||||
|
||||
Access each instance at `http://localhost:{NODE_PORT}` (e.g., `http://localhost:9001`).
|
||||
|
||||
### Option #3 - build a set of immutable images from the local branch
|
||||
### Option #2 - build a set of immutable images from the local branch
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-non-dev.yml up
|
||||
```
|
||||
|
||||
### Option #4 - boot up an official release
|
||||
### Option #3 - boot up an official release
|
||||
|
||||
```bash
|
||||
# Set the version you want to run
|
||||
|
||||
@@ -151,7 +151,7 @@ Finish installing by running through the following commands:
|
||||
superset fab create-admin
|
||||
|
||||
# Load some data to play with
|
||||
superset load_examples
|
||||
superset examples load
|
||||
|
||||
# Create default roles and permissions
|
||||
superset init
|
||||
|
||||
@@ -28,9 +28,6 @@ const globals = require('globals');
|
||||
const { defineConfig, globalIgnores } = require('eslint/config');
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
@@ -39,7 +36,7 @@ module.exports = defineConfig([
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
@@ -71,5 +68,5 @@ module.exports = defineConfig([
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
])
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc",
|
||||
"eslint": "eslint ."
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
@@ -26,33 +26,33 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.26.7",
|
||||
"antd": "^5.26.3",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.4.0",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"swagger-ui-react": "^5.27.1"
|
||||
"swagger-ui-react": "^5.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"webpack": "^5.101.0"
|
||||
"webpack": "^5.99.9"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
133
docs/yarn.lock
133
docs/yarn.lock
@@ -2205,16 +2205,11 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.31.0":
|
||||
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
|
||||
version "9.31.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
|
||||
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
|
||||
|
||||
"@eslint/js@^9.32.0":
|
||||
version "9.32.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.32.0.tgz#a02916f58bd587ea276876cb051b579a3d75d091"
|
||||
integrity sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
|
||||
@@ -2517,10 +2512,10 @@
|
||||
classnames "^2.3.2"
|
||||
rc-util "^5.24.4"
|
||||
|
||||
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.3.0.tgz#9499ada078daca9dd99d01f0f0743ee1ab9e398b"
|
||||
integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
|
||||
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
|
||||
version "2.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
|
||||
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/portal" "^1.1.0"
|
||||
@@ -3427,10 +3422,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
|
||||
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
|
||||
|
||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
|
||||
version "5.0.6"
|
||||
@@ -3971,11 +3966,6 @@ accepts@~1.3.4, accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-import-phases@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
|
||||
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -3988,7 +3978,12 @@ acorn-walk@^8.0.0:
|
||||
dependencies:
|
||||
acorn "^8.11.0"
|
||||
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.2:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
|
||||
acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
@@ -4112,10 +4107,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.26.7:
|
||||
version "5.26.7"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.7.tgz#e2f7e37330b27eec0de7a7789767975373f61602"
|
||||
integrity sha512-iCyXN6+i2CUVEOSzzJKfbKeg115qoJhGvSkCh5uzAf9hANwHUOJQhsMn+KtN+Lx/2NQ6wfM7nGZ+7NPNO5Pn1w==
|
||||
antd@^5.26.3:
|
||||
version "5.26.3"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
|
||||
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4128,7 +4123,7 @@ antd@^5.26.7:
|
||||
"@rc-component/mutate-observer" "^1.1.0"
|
||||
"@rc-component/qrcode" "~1.0.0"
|
||||
"@rc-component/tour" "~1.15.1"
|
||||
"@rc-component/trigger" "^2.3.0"
|
||||
"@rc-component/trigger" "^2.2.7"
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
@@ -4158,7 +4153,7 @@ antd@^5.26.7:
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.51.1"
|
||||
rc-tabs "~15.6.1"
|
||||
rc-textarea "~1.10.1"
|
||||
rc-textarea "~1.10.0"
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
@@ -4513,7 +4508,17 @@ braces@^3.0.3, braces@~3.0.2:
|
||||
dependencies:
|
||||
fill-range "^7.1.1"
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0:
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4:
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b"
|
||||
integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001688"
|
||||
electron-to-chromium "^1.5.73"
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.1"
|
||||
|
||||
browserslist@^4.25.0:
|
||||
version "4.25.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.0.tgz#986aa9c6d87916885da2b50d8eb577ac8d133b2c"
|
||||
integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==
|
||||
@@ -4615,7 +4620,7 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702:
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702:
|
||||
version "1.0.30001714"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz#cfd27ff07e6fa20a0f45c7a10d28a0ffeaba2122"
|
||||
integrity sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==
|
||||
@@ -5898,6 +5903,11 @@ electron-to-chromium@^1.5.160:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz#9f6697de4339e24da8b234e4492a9ecb91f5989c"
|
||||
integrity sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==
|
||||
|
||||
electron-to-chromium@^1.5.73:
|
||||
version "1.5.138"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.138.tgz#319e775179bd0889ed96a04d4390d355fb315a44"
|
||||
integrity sha512-FWlQc52z1dXqm+9cCJ2uyFgJkESd+16j6dBEjsgDNuHjBpuIzL8/lRc0uvh1k8RNI6waGo6tcy2DvwkTBJOLDg==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
@@ -5942,10 +5952,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.17.2:
|
||||
version "5.18.2"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz#7903c5b32ffd4b2143eeb4b92472bd68effd5464"
|
||||
integrity sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==
|
||||
enhanced-resolve@^5.17.1:
|
||||
version "5.18.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf"
|
||||
integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
@@ -6151,10 +6161,10 @@ escape-string-regexp@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
eslint-config-prettier@^10.1.8:
|
||||
version "10.1.8"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97"
|
||||
integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==
|
||||
eslint-config-prettier@^10.1.5:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||
|
||||
eslint-plugin-prettier@^5.5.1:
|
||||
version "5.5.1"
|
||||
@@ -8053,10 +8063,10 @@ less-loader@^12.3.0:
|
||||
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
|
||||
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
|
||||
|
||||
less@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.4.0.tgz#deaf881f4880ee80691beae925b8fac699d3a76d"
|
||||
integrity sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==
|
||||
less@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less/-/less-4.3.0.tgz#ef0cfc260a9ca8079ed8d0e3512bda8a12c82f2a"
|
||||
integrity sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==
|
||||
dependencies:
|
||||
copy-anything "^2.0.1"
|
||||
parse-node-version "^1.0.1"
|
||||
@@ -10704,10 +10714,10 @@ rc-tabs@~15.6.1:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.34.1"
|
||||
|
||||
rc-textarea@~1.10.0, rc-textarea@~1.10.1:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.2.tgz#459e3574a95c32939c6793045a1e4db04cb514cc"
|
||||
integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
|
||||
rc-textarea@~1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-1.10.0.tgz#f8f962ef83be0b8e35db97cf03dbfb86ddd9c46c"
|
||||
integrity sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
@@ -12090,10 +12100,10 @@ swagger-client@^3.35.5:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
swagger-ui-react@^5.27.1:
|
||||
version "5.27.1"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
|
||||
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
|
||||
swagger-ui-react@^5.26.0:
|
||||
version "5.26.0"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
|
||||
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
@@ -12515,7 +12525,7 @@ unraw@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz"
|
||||
integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==
|
||||
|
||||
update-browserslist-db@^1.1.3:
|
||||
update-browserslist-db@^1.1.1, update-browserslist-db@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
|
||||
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
|
||||
@@ -12784,27 +12794,26 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
|
||||
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
|
||||
webpack-sources@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.101.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.0.tgz#4b81407ffad9857f81ff03f872e3369b9198cc9d"
|
||||
integrity sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.9:
|
||||
version "5.99.9"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.9.tgz#d7de799ec17d0cce3c83b70744b4aedb537d8247"
|
||||
integrity sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/estree" "^1.0.6"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.15.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
acorn "^8.14.0"
|
||||
browserslist "^4.24.0"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.17.2"
|
||||
enhanced-resolve "^5.17.1"
|
||||
es-module-lexer "^1.2.1"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -12818,7 +12827,7 @@ webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.3.3"
|
||||
webpack-sources "^3.2.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
|
||||
486
package-lock.json
generated
486
package-lock.json
generated
@@ -1,486 +0,0 @@
|
||||
{
|
||||
"name": "move-controls",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3"
|
||||
}
|
||||
}
|
||||
@@ -94,12 +94,67 @@ describe('Charts list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('list mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('list');
|
||||
});
|
||||
|
||||
it('should load rows in list mode', () => {
|
||||
cy.getBySel('listview-table').should('be.visible');
|
||||
cy.getBySel('sort-header').eq(1).contains('Name');
|
||||
cy.getBySel('sort-header').eq(2).contains('Type');
|
||||
cy.getBySel('sort-header').eq(3).contains('Dataset');
|
||||
cy.getBySel('sort-header').eq(4).contains('On dashboards');
|
||||
cy.getBySel('sort-header').eq(5).contains('Owners');
|
||||
cy.getBySel('sort-header').eq(6).contains('Last modified');
|
||||
cy.getBySel('sort-header').eq(7).contains('Actions');
|
||||
});
|
||||
|
||||
it('should bulk select in list mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.get('[aria-label="Select all"]').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 26);
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.get('input[type="checkbox"]:checked').should('have.length', 0);
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('card mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('card');
|
||||
});
|
||||
|
||||
it('should load rows in card mode', () => {
|
||||
cy.getBySel('listview-table').should('not.exist');
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
});
|
||||
|
||||
it('should bulk select in card mode', () => {
|
||||
toggleBulkSelect();
|
||||
cy.getBySel('styled-card').click({ multiple: true });
|
||||
cy.getBySel('bulk-select-copy').contains('25 Selected');
|
||||
cy.getBySel('bulk-select-action')
|
||||
.should('have.length', 2)
|
||||
.then($btns => {
|
||||
expect($btns).to.contain('Delete');
|
||||
expect($btns).to.contain('Export');
|
||||
});
|
||||
cy.getBySel('bulk-select-deselect-all').click();
|
||||
cy.getBySel('bulk-select-copy').contains('0 Selected');
|
||||
cy.getBySel('bulk-select-action').should('not.exist');
|
||||
});
|
||||
|
||||
it('should preserve other filters when sorting', () => {
|
||||
cy.getBySel('styled-card').should('have.length', 25);
|
||||
setFilter('Type', 'Big Number');
|
||||
|
||||
@@ -31,52 +31,6 @@ import {
|
||||
interceptFormDataKey,
|
||||
} from '../explore/utils';
|
||||
|
||||
const interceptDrillInfo = () => {
|
||||
cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
result: {
|
||||
id: 1,
|
||||
changed_on_humanized: '2 days ago',
|
||||
created_on_humanized: 'a week ago',
|
||||
table_name: 'birth_names',
|
||||
changed_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
created_by: {
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
owners: [
|
||||
{
|
||||
first_name: 'Admin',
|
||||
last_name: 'User',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
column_name: 'gender',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'state',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'name',
|
||||
verbose_name: null,
|
||||
},
|
||||
{
|
||||
column_name: 'ds',
|
||||
verbose_name: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}).as('drillInfo');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-test="close-drill-by-modal"]').length) {
|
||||
@@ -108,20 +62,14 @@ const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.should('be.visible')
|
||||
.find('[role="menuitem"]')
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.click();
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
|
||||
|
||||
cy.get(
|
||||
'.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
|
||||
).should('not.exist');
|
||||
.then($el => {
|
||||
cy.wrap($el)
|
||||
.contains(new RegExp(`^${targetDrillByColumn}$`))
|
||||
.trigger('keydown', { keyCode: 13, which: 13, force: true });
|
||||
});
|
||||
|
||||
if (isLegacy) {
|
||||
return cy.wait('@legacyData');
|
||||
@@ -282,19 +230,17 @@ describe('Drill by modal', () => {
|
||||
closeModal();
|
||||
});
|
||||
before(() => {
|
||||
interceptDrillInfo();
|
||||
cy.visit(SUPPORTED_CHARTS_DASHBOARD);
|
||||
});
|
||||
|
||||
describe('Modal actions + Table', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
it.only('opens the modal from the context menu', () => {
|
||||
it('opens the modal from the context menu', () => {
|
||||
openTableContextMenu('boy');
|
||||
drillBy('state').then(intercepted => {
|
||||
verifyExpectedFormData(intercepted, {
|
||||
@@ -438,7 +384,6 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 1 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 1');
|
||||
SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
@@ -602,7 +547,6 @@ describe('Drill by modal', () => {
|
||||
describe('Tier 2 charts', () => {
|
||||
before(() => {
|
||||
closeModal();
|
||||
interceptDrillInfo();
|
||||
openTopLevelTab('Tier 2');
|
||||
SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
dataTestChartName,
|
||||
} from 'cypress/support/directories';
|
||||
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import {
|
||||
addParentFilterWithValue,
|
||||
applyNativeFilterValueWithIndex,
|
||||
@@ -161,74 +160,6 @@ describe('Native filters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Dependent filter selects first item based on parent filter selection', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region', datasetId: 2 },
|
||||
{ name: 'country_name', column: 'country_name', datasetId: 2 },
|
||||
]);
|
||||
|
||||
enterNativeFilterEditModal();
|
||||
|
||||
selectFilter(0);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
selectFilter(1);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Values are dependent on other filters')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Can select multiple values ')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
addParentFilterWithValue(0, testItems.topTenChart.filterColumnRegion);
|
||||
cy.get(nativeFilters.filterConfigurationSections.displayedSection).within(
|
||||
() => {
|
||||
cy.contains('Select first filter value by default')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
},
|
||||
);
|
||||
|
||||
// cannot use saveNativeFilterSettings because there is a bug which
|
||||
// sometimes does not allow charts to load when enabling the 'Select first filter value by default'
|
||||
// to be saved when using dependent filters so,
|
||||
// you reload the window.
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click({ force: true });
|
||||
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
cy.reload();
|
||||
|
||||
applyNativeFilterValueWithIndex(0, 'North America');
|
||||
|
||||
// Check that dependent filter auto-selects the first item
|
||||
cy.get(nativeFilters.filterFromDashboardView.filterContent)
|
||||
.eq(1)
|
||||
.should('contain.text', 'Bermuda');
|
||||
});
|
||||
|
||||
it('User can create filter depend on 2 other filters', () => {
|
||||
prepareDashboardFilters([
|
||||
{ name: 'region', column: 'region', datasetId: 2 },
|
||||
|
||||
@@ -68,13 +68,11 @@ function verifyDashboardSearch() {
|
||||
function verifyDashboardLink() {
|
||||
interceptDashboardGet();
|
||||
openDashboardsAddedTo();
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover', {
|
||||
force: true,
|
||||
});
|
||||
cy.get('.ant-dropdown-menu-submenu-popup').trigger('mouseover');
|
||||
cy.get('.ant-dropdown-menu-submenu-popup a')
|
||||
.first()
|
||||
.invoke('removeAttr', 'target')
|
||||
.click({ force: true });
|
||||
.click();
|
||||
cy.wait('@get');
|
||||
}
|
||||
|
||||
|
||||
1654
superset-frontend/package-lock.json
generated
1654
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-scale": "^2.1.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.6.0",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
@@ -208,7 +208,7 @@
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/compat-data": "^7.26.8",
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/node": "^7.22.6",
|
||||
@@ -243,7 +243,7 @@
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/classnames": "^2.3.4",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
@@ -331,7 +331,7 @@
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.20.3",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.99.9",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.4",
|
||||
"jest": "^30.0.2",
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
import { Collapse } from '@superset-ui/core/components';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const StyledCollapse = styled(Collapse)`
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: 1px solid ${({ theme }: any) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }: any) => theme.borderRadius}px;
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 2}px;
|
||||
|
||||
.ant-collapse-header {
|
||||
font-weight: ${({ theme }: any) => theme.typography.weights.bold};
|
||||
background: ${({ theme }: any) => theme.colors.grayscale.light5};
|
||||
border-radius: ${({ theme }: any) => theme.borderRadius}px
|
||||
${({ theme }: any) => theme.borderRadius}px 0 0;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
background: white;
|
||||
padding: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ControlPanelSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export const ControlPanelSection: React.FC<ControlPanelSectionProps> = ({
|
||||
title,
|
||||
children,
|
||||
expanded = false,
|
||||
}) => {
|
||||
const [activeKey, setActiveKey] = useState(expanded ? ['1'] : []);
|
||||
|
||||
return (
|
||||
<StyledCollapse
|
||||
activeKey={activeKey}
|
||||
onChange={keys => setActiveKey(keys as string[])}
|
||||
>
|
||||
<Panel header={title} key="1">
|
||||
{children}
|
||||
</Panel>
|
||||
</StyledCollapse>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { styled } from '@superset-ui/core';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
margin-bottom: ${({ theme }: any) => theme.gridUnit * 3}px;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.control-wrapper {
|
||||
min-width: 0; // Allow flex items to shrink
|
||||
}
|
||||
`;
|
||||
|
||||
export interface ControlRowProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ControlRow: React.FC<ControlRowProps> = ({ children }) => (
|
||||
<StyledRow>{children}</StyledRow>
|
||||
);
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import AdhocFilterControlOriginal from 'src/explore/components/controls/FilterControl/AdhocFilterControl/index';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface AdhocFilterControlProps {
|
||||
name: string;
|
||||
value?: any[];
|
||||
onChange: (value: any[]) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing AdhocFilterControl that simplifies its API
|
||||
*/
|
||||
export const AdhocFilterControl: React.FC<AdhocFilterControlProps> = ({
|
||||
name,
|
||||
value = [],
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
renderTrigger,
|
||||
}) => {
|
||||
// Get datasource from Redux state
|
||||
const datasource = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource,
|
||||
) as any;
|
||||
|
||||
const columns = datasource?.columns || [];
|
||||
const savedMetrics = datasource?.metrics || [];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<AdhocFilterControlOriginal
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
columns={columns}
|
||||
savedMetrics={savedMetrics}
|
||||
datasource={datasource}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import CheckboxControlComponent from '../../../../../../../src/explore/components/controls/CheckboxControl';
|
||||
|
||||
export interface CheckboxControlProps {
|
||||
value?: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
hovered?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox control component
|
||||
*/
|
||||
export const CheckboxControl: React.FC<CheckboxControlProps> = (
|
||||
props,
|
||||
): ReactElement => <CheckboxControlComponent {...props} />;
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { getCategoricalSchemeRegistry } from '@superset-ui/core';
|
||||
import { SelectControl } from './SimpleSelectControl';
|
||||
|
||||
export interface ColorSchemeControlProps {
|
||||
name: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color scheme selector control
|
||||
*/
|
||||
export const ColorSchemeControl: React.FC<ColorSchemeControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
required,
|
||||
renderTrigger,
|
||||
clearable = false,
|
||||
}) => {
|
||||
// Get available color schemes from the categorical scheme registry
|
||||
const colorSchemes = getCategoricalSchemeRegistry().keys();
|
||||
const choices: [string, string][] = colorSchemes.map((scheme: string) => [
|
||||
scheme,
|
||||
scheme,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange as (value: string | string[]) => void}
|
||||
label={label}
|
||||
description={description}
|
||||
required={required}
|
||||
renderTrigger={renderTrigger}
|
||||
choices={choices}
|
||||
clearable={clearable}
|
||||
multiple={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import ControlComponent from '../../../../../../../src/explore/components/Control';
|
||||
|
||||
export interface ControlProps {
|
||||
type?: string;
|
||||
name: string;
|
||||
value?: any;
|
||||
actions?: any;
|
||||
formData?: any;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic control wrapper component
|
||||
*/
|
||||
export const Control: React.FC<ControlProps> = (props): ReactElement => (
|
||||
<ControlComponent {...props} />
|
||||
);
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import { DndColumnSelect as DndColumnSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface DndColumnSelectProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
isTemporal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndColumnSelect that simplifies its API
|
||||
*/
|
||||
export const DndColumnSelect: React.FC<DndColumnSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
multi = false,
|
||||
required,
|
||||
renderTrigger,
|
||||
canDelete = true,
|
||||
ghostButtonText,
|
||||
isTemporal,
|
||||
}) => {
|
||||
// Get columns from Redux state
|
||||
const columns = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource?.columns || [],
|
||||
) as any[];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<DndColumnSelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={columns}
|
||||
multi={multi}
|
||||
canDelete={canDelete}
|
||||
ghostButtonText={
|
||||
ghostButtonText || (multi ? 'Drop columns here' : 'Drop column here')
|
||||
}
|
||||
isTemporal={isTemporal}
|
||||
type="DndColumnSelect"
|
||||
actions={
|
||||
{
|
||||
setControlValue: (controlName: string, value: any) => ({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
validationErrors: undefined,
|
||||
}),
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import { DndFilterSelect as DndFilterSelectControl } from '../../../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
|
||||
import { Datasource } from '../../types';
|
||||
|
||||
export interface DndFilterSelectProps {
|
||||
value?: any[];
|
||||
onChange: (value: any[]) => void;
|
||||
datasource?: Datasource;
|
||||
columns?: any[];
|
||||
formData?: any;
|
||||
savedMetrics?: any[];
|
||||
selectedMetrics?: any[];
|
||||
name?: string;
|
||||
actions?: any;
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndFilterSelect that simplifies its API
|
||||
*/
|
||||
export const DndFilterSelect: React.FC<DndFilterSelectProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
datasource,
|
||||
columns = [],
|
||||
formData = {},
|
||||
savedMetrics = [],
|
||||
selectedMetrics = [],
|
||||
name = 'adhoc_filters',
|
||||
actions,
|
||||
type = 'DndFilterSelect',
|
||||
...restProps
|
||||
}): ReactElement => {
|
||||
// Handle the case where onChange needs to be wrapped for actions.setControlValue
|
||||
const handleChange = (val: any) => {
|
||||
if (actions?.setControlValue) {
|
||||
actions.setControlValue(name, val);
|
||||
} else if (onChange) {
|
||||
onChange(val);
|
||||
}
|
||||
};
|
||||
|
||||
// For compatibility with the original component
|
||||
const componentProps = {
|
||||
value,
|
||||
onChange: handleChange,
|
||||
datasource,
|
||||
columns,
|
||||
formData,
|
||||
name,
|
||||
savedMetrics,
|
||||
selectedMetrics,
|
||||
type,
|
||||
actions,
|
||||
...restProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter-select-wrapper">
|
||||
<DndFilterSelectControl {...componentProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import { DndMetricSelect as DndMetricSelectControl } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface DndMetricSelectProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
multi?: boolean;
|
||||
required?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
canDelete?: boolean;
|
||||
ghostButtonText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the existing DndMetricSelect that simplifies its API
|
||||
*/
|
||||
export const DndMetricSelect: React.FC<DndMetricSelectProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
multi = false,
|
||||
required,
|
||||
renderTrigger,
|
||||
canDelete = true,
|
||||
ghostButtonText,
|
||||
}) => {
|
||||
// Get datasource from Redux state
|
||||
const datasource = useSelector<ExplorePageState>(
|
||||
state => state.explore.datasource,
|
||||
) as any;
|
||||
|
||||
const columns = datasource?.columns || [];
|
||||
const savedMetrics = datasource?.metrics || [];
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<DndMetricSelectControl
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
columns={columns}
|
||||
savedMetrics={savedMetrics}
|
||||
multi={multi}
|
||||
canDelete={canDelete}
|
||||
ghostButtonText={
|
||||
ghostButtonText || (multi ? 'Drop metrics here' : 'Drop metric here')
|
||||
}
|
||||
type="DndMetricSelect"
|
||||
actions={
|
||||
{
|
||||
setControlValue: (controlName: string, value: any) => ({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
validationErrors: undefined,
|
||||
}),
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import SelectControlComponent from '../../../../../../../src/explore/components/controls/SelectControl';
|
||||
|
||||
export interface SelectControlProps {
|
||||
value?: any;
|
||||
onChange: (value: any) => void;
|
||||
choices?: Array<[string | number, string]>;
|
||||
clearable?: boolean;
|
||||
multi?: boolean;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
hovered?: boolean;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select control component
|
||||
*/
|
||||
export const SelectControl: React.FC<SelectControlProps> = (
|
||||
props,
|
||||
): ReactElement => <SelectControlComponent {...props} />;
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Select } from '@superset-ui/core/components';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface SelectControlProps {
|
||||
name: string;
|
||||
value?: string | string[];
|
||||
onChange: (value: string | string[]) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
choices: Array<[string, string]>;
|
||||
clearable?: boolean;
|
||||
multiple?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const SelectControl: React.FC<SelectControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
disabled,
|
||||
choices,
|
||||
clearable = true,
|
||||
multiple = false,
|
||||
renderTrigger,
|
||||
required,
|
||||
}) => {
|
||||
const options = choices.map(([val, label]) => ({
|
||||
value: val,
|
||||
label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<Select
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
options={options}
|
||||
allowClear={clearable}
|
||||
mode={multiple ? 'multiple' : undefined}
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { Input } from '@superset-ui/core/components';
|
||||
import { ControlHeader } from '../ControlHeader';
|
||||
|
||||
export interface TextControlProps {
|
||||
name: string;
|
||||
value?: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
renderTrigger?: boolean;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const TextControl: React.FC<TextControlProps> = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
description,
|
||||
placeholder,
|
||||
disabled,
|
||||
isInt,
|
||||
isFloat,
|
||||
min,
|
||||
max,
|
||||
renderTrigger,
|
||||
required,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newValue: string | number = e.target.value;
|
||||
|
||||
if (isInt) {
|
||||
newValue = parseInt(newValue, 10) || 0;
|
||||
} else if (isFloat) {
|
||||
newValue = parseFloat(newValue) || 0;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange, isInt, isFloat],
|
||||
);
|
||||
|
||||
const inputType = isInt || isFloat ? 'number' : 'text';
|
||||
|
||||
return (
|
||||
<div className="control-wrapper">
|
||||
{label && (
|
||||
<ControlHeader
|
||||
label={label}
|
||||
description={description}
|
||||
renderTrigger={renderTrigger}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
name={name}
|
||||
type={inputType}
|
||||
value={value ?? ''}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
min={min}
|
||||
max={max}
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import SliderControlComponent from '../../../../../../../src/explore/components/controls/SliderControl';
|
||||
|
||||
export interface SliderControlProps {
|
||||
value?: number;
|
||||
onChange: (value: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider control component
|
||||
*/
|
||||
export const SliderControl: React.FC<SliderControlProps> = (
|
||||
props,
|
||||
): ReactElement => <SliderControlComponent {...props} />;
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
// @ts-ignore
|
||||
import TextControlComponent from '../../../../../../../src/explore/components/controls/TextControl';
|
||||
|
||||
export interface TextControlProps {
|
||||
value?: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
placeholder?: string;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
disabled?: boolean;
|
||||
controlId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text input control component
|
||||
*/
|
||||
export const TextControl: React.FC<TextControlProps> = (
|
||||
props,
|
||||
): ReactElement => <TextControlComponent {...props} />;
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Layout components
|
||||
export { ControlPanelSection } from './ControlPanelSection';
|
||||
export { ControlRow } from './ControlRow';
|
||||
export { ControlHeader } from './ControlHeader';
|
||||
|
||||
// Control components
|
||||
export { TextControl as SimpleTextControl } from './controls/SimpleTextControl';
|
||||
export { SelectControl as SimpleSelectControl } from './controls/SimpleSelectControl';
|
||||
|
||||
// Wrapper controls for new simplified API
|
||||
export { DndColumnSelect } from './controls/DndColumnSelectWrapper';
|
||||
export { DndMetricSelect } from './controls/DndMetricSelectWrapper';
|
||||
export { DndFilterSelect } from './controls/DndFilterSelectWrapper';
|
||||
export { AdhocFilterControl } from './controls/AdhocFilterControlWrapper';
|
||||
export { ColorSchemeControl as SimpleColorSchemeControl } from './controls/ColorSchemeControlWrapper';
|
||||
export { TextControl } from './controls/TextControlWrapper';
|
||||
export { CheckboxControl } from './controls/CheckboxControlWrapper';
|
||||
export { SelectControl } from './controls/SelectControlWrapper';
|
||||
export { SliderControl } from './controls/SliderControlWrapper';
|
||||
export { Control } from './controls/ControlWrapper';
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
|
||||
// Define minimal types to avoid circular dependencies
|
||||
interface ExploreState {
|
||||
form_data: QueryFormData;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RootState {
|
||||
explore: ExploreState;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access and update form data from Redux store
|
||||
* Provides a simple interface for control components
|
||||
*
|
||||
* NOTE: This hook assumes the Redux store structure used by Superset's explore view.
|
||||
* The actual setControlValue action should be provided by the app via context or props.
|
||||
*/
|
||||
export function useFormData() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get form data from Redux
|
||||
const formData = useSelector<RootState, QueryFormData>(state => {
|
||||
console.log('useFormData - state.explore:', state.explore);
|
||||
return state.explore?.form_data || {};
|
||||
});
|
||||
|
||||
// Update a single control value
|
||||
// This is a placeholder - the actual action should be injected
|
||||
const updateControl = useCallback(
|
||||
(controlName: string, value: any) => {
|
||||
console.log('updateControl:', controlName, value);
|
||||
// In production, this would dispatch the actual setControlValue action
|
||||
// For now, we'll dispatch a generic action
|
||||
dispatch({
|
||||
type: 'SET_CONTROL_VALUE',
|
||||
controlName,
|
||||
value,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// Update multiple controls at once
|
||||
const updateControls = useCallback(
|
||||
(updates: Partial<QueryFormData>) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
updateControl(key, value);
|
||||
});
|
||||
},
|
||||
[updateControl],
|
||||
);
|
||||
|
||||
return {
|
||||
formData,
|
||||
updateControl,
|
||||
updateControls,
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import * as sectionsModule from './sections';
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
export * from './operators';
|
||||
export * from './hooks/useFormData';
|
||||
|
||||
// can't do `export * as sections from './sections'`, babel-transformer will fail
|
||||
export const sections = sectionsModule;
|
||||
@@ -33,26 +32,7 @@ export * from './components/Dropdown';
|
||||
export * from './components/Menu';
|
||||
export * from './components/MetricOption';
|
||||
export * from './components/ControlHeader';
|
||||
export * from './components';
|
||||
// Export individual control components for easier access
|
||||
export {
|
||||
DndColumnSelect,
|
||||
DndMetricSelect,
|
||||
DndFilterSelect,
|
||||
TextControl,
|
||||
CheckboxControl,
|
||||
SelectControl,
|
||||
SliderControl,
|
||||
Control,
|
||||
} from './components';
|
||||
|
||||
export * from './shared-controls';
|
||||
export {
|
||||
GranularityControl,
|
||||
RadioButtonControl,
|
||||
ReactControlPanel,
|
||||
} from './shared-controls/components';
|
||||
// Export all from shared-controls/components which includes inline control functions
|
||||
export * from './shared-controls/components';
|
||||
export * from './types';
|
||||
export * from './fixtures';
|
||||
|
||||
@@ -18,20 +18,6 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { ControlPanelSectionConfig, ControlSetRow } from '../types';
|
||||
import {
|
||||
AdhocFiltersControl,
|
||||
GroupByControl,
|
||||
GroupOthersWhenLimitReachedControl,
|
||||
LimitControl,
|
||||
MetricsControl,
|
||||
OrderDescControl,
|
||||
RowLimitControl,
|
||||
ShowEmptyColumnsControl,
|
||||
TimeGrainSqlaControl,
|
||||
TimeLimitMetricControl,
|
||||
TruncateMetricControl,
|
||||
XAxisControl,
|
||||
} from '../shared-controls/components/SharedControlComponents';
|
||||
import {
|
||||
contributionModeControl,
|
||||
xAxisForceCategoricalControl,
|
||||
@@ -40,34 +26,30 @@ import {
|
||||
} from '../shared-controls';
|
||||
|
||||
const controlsWithoutXAxis: ControlSetRow[] = [
|
||||
[MetricsControl()],
|
||||
[GroupByControl()],
|
||||
['metrics'],
|
||||
['groupby'],
|
||||
[contributionModeControl],
|
||||
[AdhocFiltersControl()],
|
||||
[LimitControl(), GroupOthersWhenLimitReachedControl()],
|
||||
[TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
[RowLimitControl()],
|
||||
[TruncateMetricControl()],
|
||||
[ShowEmptyColumnsControl()],
|
||||
['adhoc_filters'],
|
||||
['limit', 'group_others_when_limit_reached'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
['row_limit'],
|
||||
['truncate_metric'],
|
||||
['show_empty_columns'],
|
||||
];
|
||||
|
||||
export const echartsTimeSeriesQuery: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
...controlsWithoutXAxis,
|
||||
],
|
||||
controlSetRows: [['x_axis'], ['time_grain_sqla'], ...controlsWithoutXAxis],
|
||||
};
|
||||
|
||||
export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[XAxisControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
['x_axis'],
|
||||
['time_grain_sqla'],
|
||||
[xAxisForceCategoricalControl],
|
||||
[xAxisSortControl],
|
||||
[xAxisSortAscControl],
|
||||
|
||||
@@ -18,15 +18,6 @@
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { ControlPanelSectionConfig } from '../types';
|
||||
import {
|
||||
GranularityControl,
|
||||
GranularitySqlaControl,
|
||||
TimeGrainSqlaControl,
|
||||
TimeRangeControl,
|
||||
DatasourceControl,
|
||||
VizTypeControl,
|
||||
ColorSchemeControl,
|
||||
} from '../shared-controls/components/SharedControlComponents';
|
||||
|
||||
// A few standard controls sections that are used internally.
|
||||
// Not recommended for use in third-party plugins.
|
||||
@@ -40,10 +31,10 @@ const baseTimeSection = {
|
||||
export const legacyTimeseriesTime: ControlPanelSectionConfig = {
|
||||
...baseTimeSection,
|
||||
controlSetRows: [
|
||||
[GranularityControl()],
|
||||
[GranularitySqlaControl()],
|
||||
[TimeGrainSqlaControl()],
|
||||
[TimeRangeControl()],
|
||||
['granularity'],
|
||||
['granularity_sqla'],
|
||||
['time_grain_sqla'],
|
||||
['time_range'],
|
||||
],
|
||||
};
|
||||
|
||||
@@ -51,8 +42,8 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
label: t('Datasource & Chart Type'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[DatasourceControl()],
|
||||
[VizTypeControl()],
|
||||
['datasource'],
|
||||
['viz_type'],
|
||||
[
|
||||
{
|
||||
name: 'slice_id',
|
||||
@@ -100,7 +91,7 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
|
||||
export const colorScheme: ControlPanelSectionConfig = {
|
||||
label: t('Color Scheme'),
|
||||
controlSetRows: [[ColorSchemeControl()]],
|
||||
controlSetRows: [['color_scheme']],
|
||||
};
|
||||
|
||||
export const annotations: ControlPanelSectionConfig = {
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Input, Select, Switch, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface AxisControlSectionProps {
|
||||
axis: 'x' | 'y';
|
||||
showTitle?: boolean;
|
||||
showFormat?: boolean;
|
||||
showRotation?: boolean;
|
||||
showBounds?: boolean;
|
||||
showLogarithmic?: boolean;
|
||||
showMinorTicks?: boolean;
|
||||
showTruncate?: boolean;
|
||||
timeFormat?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const D3_FORMAT_OPTIONS = [
|
||||
['SMART_NUMBER', t('Adaptive formatting')],
|
||||
['~g', t('Original value')],
|
||||
['d', t('Signed integer')],
|
||||
['.1f', t('1 decimal place')],
|
||||
['.2f', t('2 decimal places')],
|
||||
['.3f', t('3 decimal places')],
|
||||
['+,', t('Positive integer')],
|
||||
['$,.2f', t('Currency (2 decimals)')],
|
||||
[',.0%', t('Percentage')],
|
||||
['.1%', t('Percentage (1 decimal)')],
|
||||
];
|
||||
|
||||
const D3_TIME_FORMAT_OPTIONS = [
|
||||
['smart_date', t('Adaptive formatting')],
|
||||
['%Y-%m-%d', t('2023-01-01')],
|
||||
['%Y-%m-%d %H:%M', t('2023-01-01 10:30')],
|
||||
['%m/%d/%Y', t('01/01/2023')],
|
||||
['%d/%m/%Y', t('01/01/2023')],
|
||||
['%Y', t('2023')],
|
||||
['%B %Y', t('January 2023')],
|
||||
['%b %Y', t('Jan 2023')],
|
||||
['%B %-d, %Y', t('January 1, 2023')],
|
||||
];
|
||||
|
||||
const ROTATION_OPTIONS = [
|
||||
[0, '0°'],
|
||||
[45, '45°'],
|
||||
[90, '90°'],
|
||||
[-45, '-45°'],
|
||||
[-90, '-90°'],
|
||||
];
|
||||
|
||||
export const AxisControlSection: FC<AxisControlSectionProps> = ({
|
||||
axis,
|
||||
showTitle = true,
|
||||
showFormat = true,
|
||||
showRotation = false,
|
||||
showBounds = false,
|
||||
showLogarithmic = false,
|
||||
showMinorTicks = false,
|
||||
showTruncate = false,
|
||||
timeFormat = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isXAxis = axis === 'x';
|
||||
const axisUpper = axis.toUpperCase();
|
||||
const titleKey = `${axis}_axis_title`;
|
||||
const formatKey = timeFormat
|
||||
? `${axis}_axis_time_format`
|
||||
: `${axis}_axis_format`;
|
||||
const rotationKey = `${axis}_axis_label_rotation`;
|
||||
const boundsMinKey = `${axis}_axis_bounds_min`;
|
||||
const boundsMaxKey = `${axis}_axis_bounds_max`;
|
||||
const logScaleKey = `log_scale`;
|
||||
const minorTicksKey = `${axis}_axis_minor_ticks`;
|
||||
const truncateKey = `truncate_${axis}axis`;
|
||||
const truncateLabelsKey = `${axis}_axis_truncate_labels`;
|
||||
|
||||
return (
|
||||
<div className="axis-control-section">
|
||||
{showTitle && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Title`)}</label>
|
||||
<Input
|
||||
value={values[titleKey] || ''}
|
||||
onChange={e => onChange(titleKey, e.target.value)}
|
||||
placeholder={t(`Enter ${axis} axis title`)}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'Overrides the axis title derived from the metric or column name',
|
||||
)}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showFormat && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Format`)}</label>
|
||||
<Select
|
||||
value={
|
||||
values[formatKey] ||
|
||||
(timeFormat ? 'smart_date' : 'SMART_NUMBER')
|
||||
}
|
||||
onChange={value => onChange(formatKey, value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a format')}
|
||||
options={(timeFormat
|
||||
? D3_TIME_FORMAT_OPTIONS
|
||||
: D3_FORMAT_OPTIONS
|
||||
).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{timeFormat
|
||||
? t('D3 time format for x axis')
|
||||
: t('D3 format for axis values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showRotation && isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Rotation')}</label>
|
||||
<Select
|
||||
value={values[rotationKey] || 0}
|
||||
onChange={value => onChange(rotationKey, value)}
|
||||
style={{ width: '100%' }}
|
||||
options={ROTATION_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Rotation angle for axis labels')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showBounds && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t(`${axisUpper} Axis Bounds`)}</label>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<InputNumber
|
||||
value={values[boundsMinKey]}
|
||||
onChange={value => onChange(boundsMinKey, value)}
|
||||
placeholder={t('Min')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<InputNumber
|
||||
value={values[boundsMaxKey]}
|
||||
onChange={value => onChange(boundsMaxKey, value)}
|
||||
placeholder={t('Max')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
<small className="text-muted">
|
||||
{t('Bounds for axis values. Leave empty for automatic scaling.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showLogarithmic && !isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values[logScaleKey] || false}
|
||||
onChange={checked => onChange(logScaleKey, checked)}
|
||||
/>
|
||||
{t('Logarithmic Scale')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Use a logarithmic scale for the Y-axis')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showMinorTicks && !isXAxis && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values[minorTicksKey] || false}
|
||||
onChange={checked => onChange(minorTicksKey, checked)}
|
||||
/>
|
||||
{t('Show Minor Ticks')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show minor grid lines on the axis')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showTruncate && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={
|
||||
values[truncateKey] || values[truncateLabelsKey] || false
|
||||
}
|
||||
onChange={checked => {
|
||||
onChange(truncateKey, checked);
|
||||
onChange(truncateLabelsKey, checked);
|
||||
}}
|
||||
/>
|
||||
{t(`Truncate ${axisUpper} Axis Labels`)}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Truncate long axis labels to prevent overlap')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AxisControlSection;
|
||||
@@ -1,634 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement } from 'react';
|
||||
import type { CustomControlItem, ControlValueValidator } from '../../types';
|
||||
|
||||
// Base control props that all controls share
|
||||
interface BaseControlProps {
|
||||
name: string;
|
||||
label?: ReactElement | string;
|
||||
description?: string;
|
||||
default?: any;
|
||||
renderTrigger?: boolean;
|
||||
validators?: ControlValueValidator[];
|
||||
warning?: string;
|
||||
error?: string;
|
||||
mapStateToProps?: (state: any, control: any) => any;
|
||||
visibility?: (props: any) => boolean;
|
||||
value?: any;
|
||||
onChange?: (value: any) => void;
|
||||
}
|
||||
|
||||
// Use the existing CustomControlItem type instead of creating a duplicate
|
||||
// This ensures type compatibility with the rest of the codebase
|
||||
export type ControlComponentConfig = CustomControlItem;
|
||||
|
||||
// CheckboxControl Component
|
||||
interface CheckboxControlProps extends BaseControlProps {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export const CheckboxControl = (
|
||||
props: CheckboxControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? false,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SelectControl Component
|
||||
interface SelectControlProps extends BaseControlProps {
|
||||
choices?: Array<[string, string]> | (() => Array<[string, string]>);
|
||||
clearable?: boolean;
|
||||
freeForm?: boolean;
|
||||
multi?: boolean;
|
||||
placeholder?: string;
|
||||
optionRenderer?: (option: any) => ReactElement;
|
||||
valueRenderer?: (value: any) => ReactElement;
|
||||
valueKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
|
||||
export const SelectControl = (
|
||||
props: SelectControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
choices: props.choices ?? [],
|
||||
clearable: props.clearable ?? true,
|
||||
freeForm: props.freeForm ?? false,
|
||||
multi: props.multi ?? false,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
placeholder: props.placeholder,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
optionRenderer: props.optionRenderer,
|
||||
valueRenderer: props.valueRenderer,
|
||||
valueKey: props.valueKey,
|
||||
labelKey: props.labelKey,
|
||||
},
|
||||
});
|
||||
|
||||
// TextControl Component
|
||||
interface TextControlProps extends BaseControlProps {
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
}
|
||||
|
||||
export const TextControl = (
|
||||
props: TextControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
default: props.default ?? '',
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
disabled: props.disabled ?? false,
|
||||
isInt: props.isInt ?? false,
|
||||
isFloat: props.isFloat ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// TextAreaControl Component
|
||||
interface TextAreaControlProps extends BaseControlProps {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
language?: 'json' | 'html' | 'sql' | 'markdown' | 'javascript';
|
||||
offerEditInModal?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TextAreaControl = (
|
||||
props: TextAreaControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextAreaControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
rows: props.rows ?? 3,
|
||||
language: props.language,
|
||||
offerEditInModal: props.offerEditInModal ?? true,
|
||||
default: props.default ?? '',
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
disabled: props.disabled ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SliderControl Component
|
||||
interface SliderControlProps extends BaseControlProps {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
default?: number;
|
||||
}
|
||||
|
||||
export const SliderControl = (
|
||||
props: SliderControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SliderControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
min: props.min ?? 0,
|
||||
max: props.max ?? 100,
|
||||
step: props.step ?? 1,
|
||||
default: props.default ?? 0,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// RadioButtonControl Component
|
||||
interface RadioButtonControlProps extends BaseControlProps {
|
||||
options?: Array<[string, string | ReactElement]>;
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export const RadioButtonControl = (
|
||||
props: RadioButtonControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'RadioButtonControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
options: props.options ?? [],
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// NumberControl Component
|
||||
interface NumberControlProps extends BaseControlProps {
|
||||
min?: number;
|
||||
max?: number;
|
||||
default?: number;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const NumberControl = (
|
||||
props: NumberControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
placeholder: props.placeholder,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
isFloat: true,
|
||||
controlHeader: {
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
},
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
},
|
||||
});
|
||||
|
||||
// ColorPickerControl Component
|
||||
interface ColorPickerControlProps extends BaseControlProps {
|
||||
default?: { r: number; g: number; b: number; a?: number };
|
||||
}
|
||||
|
||||
export const ColorPickerControl = (
|
||||
props: ColorPickerControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColorPickerControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? { r: 0, g: 122, b: 135, a: 1 },
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// DateFilterControl Component
|
||||
interface DateFilterControlProps extends BaseControlProps {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export const DateFilterControl = (
|
||||
props: DateFilterControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'DateFilterControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
renderTrigger: props.renderTrigger,
|
||||
},
|
||||
});
|
||||
|
||||
// BoundsControl Component
|
||||
interface BoundsControlProps extends BaseControlProps {
|
||||
default?: [number | null, number | null];
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export const BoundsControl = (
|
||||
props: BoundsControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'BoundsControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? [null, null],
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// SwitchControl Component
|
||||
interface SwitchControlProps extends BaseControlProps {
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
export const SwitchControl = (
|
||||
props: SwitchControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default ?? false,
|
||||
renderTrigger: props.renderTrigger ?? false,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// HiddenControl Component (for hidden fields)
|
||||
interface HiddenControlProps {
|
||||
name: string;
|
||||
value?: any;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
export const HiddenControl = (
|
||||
props: HiddenControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'HiddenControl',
|
||||
default: props.default,
|
||||
value: props.value,
|
||||
renderTrigger: false,
|
||||
visible: false,
|
||||
},
|
||||
});
|
||||
|
||||
// MetricsControl Component
|
||||
interface MetricsControlProps extends BaseControlProps {
|
||||
multi?: boolean;
|
||||
clearable?: boolean;
|
||||
savedMetrics?: any[];
|
||||
columns?: any[];
|
||||
datasourceType?: string;
|
||||
}
|
||||
|
||||
export const MetricsControl = (
|
||||
props: MetricsControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'MetricsControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
multi: props.multi ?? true,
|
||||
clearable: props.clearable ?? true,
|
||||
validators: props.validators ?? [],
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
columns: state.datasource?.columns || [],
|
||||
savedMetrics: state.datasource?.metrics || [],
|
||||
datasourceType: state.datasource?.type,
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
savedMetrics: props.savedMetrics,
|
||||
columns: props.columns,
|
||||
datasourceType: props.datasourceType,
|
||||
},
|
||||
});
|
||||
|
||||
// GroupByControl Component
|
||||
interface GroupByControlProps extends BaseControlProps {
|
||||
multi?: boolean;
|
||||
clearable?: boolean;
|
||||
columns?: any[];
|
||||
}
|
||||
|
||||
export const GroupByControl = (
|
||||
props: GroupByControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
multi: props.multi ?? true,
|
||||
clearable: props.clearable ?? true,
|
||||
validators: props.validators ?? [],
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
choices: state.datasource?.columns || [],
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
columns: props.columns,
|
||||
},
|
||||
});
|
||||
|
||||
// AdhocFilterControl Component
|
||||
interface AdhocFilterControlProps extends BaseControlProps {
|
||||
columns?: any[];
|
||||
savedMetrics?: any[];
|
||||
datasourceType?: string;
|
||||
}
|
||||
|
||||
export const AdhocFilterControl = (
|
||||
props: AdhocFilterControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'AdhocFilterControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
columns: state.datasource?.columns || [],
|
||||
savedMetrics: state.datasource?.metrics || [],
|
||||
datasourceType: state.datasource?.type,
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
columns: props.columns,
|
||||
savedMetrics: props.savedMetrics,
|
||||
datasourceType: props.datasourceType,
|
||||
},
|
||||
});
|
||||
|
||||
// SpatialControl Component
|
||||
interface SpatialControlProps extends BaseControlProps {
|
||||
choices?: any[];
|
||||
}
|
||||
|
||||
export const SpatialControl = (
|
||||
props: SpatialControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SpatialControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
validators: props.validators,
|
||||
mapStateToProps:
|
||||
props.mapStateToProps ||
|
||||
((state: any) => ({
|
||||
choices: state.datasource?.columns || [],
|
||||
})),
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// ColorSchemeControl Component
|
||||
interface ColorSchemeControlProps extends BaseControlProps {
|
||||
choices?: (() => Array<[string, string]>) | Array<[string, string]>;
|
||||
schemes?: () => any;
|
||||
isLinear?: boolean;
|
||||
}
|
||||
|
||||
export const ColorSchemeControl = (
|
||||
props: ColorSchemeControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColorSchemeControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
choices: props.choices,
|
||||
schemes: props.schemes,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
isLinear: props.isLinear,
|
||||
},
|
||||
});
|
||||
|
||||
// SelectAsyncControl Component
|
||||
interface SelectAsyncControlProps extends BaseControlProps {
|
||||
dataEndpoint?: string;
|
||||
multi?: boolean;
|
||||
mutator?: (data: any) => any;
|
||||
placeholder?: string;
|
||||
onAsyncErrorMessage?: string;
|
||||
cacheOptions?: boolean;
|
||||
}
|
||||
|
||||
export const SelectAsyncControl = (
|
||||
props: SelectAsyncControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'SelectAsyncControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
dataEndpoint: props.dataEndpoint,
|
||||
multi: props.multi ?? false,
|
||||
mutator: props.mutator,
|
||||
placeholder: props.placeholder,
|
||||
onAsyncErrorMessage: props.onAsyncErrorMessage,
|
||||
cacheOptions: props.cacheOptions ?? true,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
renderTrigger: props.renderTrigger,
|
||||
},
|
||||
});
|
||||
|
||||
// ContourControl Component
|
||||
interface ContourControlProps extends BaseControlProps {
|
||||
renderTrigger?: boolean;
|
||||
choices?: Array<[string, string]>;
|
||||
}
|
||||
|
||||
export const ContourControl = (
|
||||
props: ContourControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ContourControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
choices: props.choices,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// ColumnConfigControl Component
|
||||
interface ColumnConfigControlProps extends BaseControlProps {
|
||||
renderTrigger?: boolean;
|
||||
}
|
||||
|
||||
export const ColumnConfigControl = (
|
||||
props: ColumnConfigControlProps,
|
||||
): ControlComponentConfig => ({
|
||||
name: props.name,
|
||||
config: {
|
||||
type: 'ColumnConfigControl',
|
||||
label: props.label,
|
||||
description: props.description,
|
||||
default: props.default,
|
||||
renderTrigger: props.renderTrigger ?? true,
|
||||
validators: props.validators,
|
||||
warning: props.warning,
|
||||
error: props.error,
|
||||
mapStateToProps: props.mapStateToProps,
|
||||
visibility: props.visibility,
|
||||
},
|
||||
});
|
||||
|
||||
// Export all components
|
||||
export default {
|
||||
CheckboxControl,
|
||||
SelectControl,
|
||||
TextControl,
|
||||
TextAreaControl,
|
||||
SliderControl,
|
||||
RadioButtonControl,
|
||||
NumberControl,
|
||||
ColorPickerControl,
|
||||
DateFilterControl,
|
||||
BoundsControl,
|
||||
SwitchControl,
|
||||
HiddenControl,
|
||||
MetricsControl,
|
||||
GroupByControl,
|
||||
AdhocFilterControl,
|
||||
SpatialControl,
|
||||
ColorSchemeControl,
|
||||
SelectAsyncControl,
|
||||
ContourControl,
|
||||
ColumnConfigControl,
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, FC } from 'react';
|
||||
import { Row, Col, Collapse } from '@superset-ui/core/components';
|
||||
|
||||
/**
|
||||
* Props for control panel sections
|
||||
*/
|
||||
export interface ControlSectionProps {
|
||||
label?: ReactNode;
|
||||
description?: ReactNode;
|
||||
expanded?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A collapsible section in the control panel
|
||||
*/
|
||||
export const ControlSection: FC<ControlSectionProps> = ({
|
||||
label,
|
||||
description,
|
||||
expanded = true,
|
||||
children,
|
||||
}) => {
|
||||
if (!label) {
|
||||
// No label means no collapsible wrapper
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse defaultActiveKey={expanded ? ['1'] : []} ghost>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<span>
|
||||
{label}
|
||||
{description && (
|
||||
<span style={{ marginLeft: 8, fontSize: '0.85em', opacity: 0.7 }}>
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
{children}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for control row - uses Ant Design grid
|
||||
*/
|
||||
export interface ControlRowProps {
|
||||
children: ReactNode;
|
||||
gutter?: number | [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* A row of controls using Ant Design's grid system
|
||||
* Automatically distributes controls evenly across columns
|
||||
*/
|
||||
export const ControlPanelRow: FC<ControlRowProps> = ({
|
||||
children,
|
||||
gutter = [16, 16],
|
||||
}) => {
|
||||
const childArray = Array.isArray(children) ? children : [children];
|
||||
const validChildren = childArray.filter(
|
||||
child => child !== null && child !== undefined,
|
||||
);
|
||||
const colSpan =
|
||||
validChildren.length > 0 ? Math.floor(24 / validChildren.length) : 24;
|
||||
|
||||
return (
|
||||
<Row gutter={gutter} style={{ marginBottom: 16 }}>
|
||||
{validChildren.map((child, index) => (
|
||||
<Col key={index} span={colSpan}>
|
||||
{child}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the main control panel layout
|
||||
*/
|
||||
export interface ControlPanelLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main control panel layout container
|
||||
*/
|
||||
export const ControlPanelLayout: FC<ControlPanelLayoutProps> = ({
|
||||
children,
|
||||
}) => (
|
||||
<div className="control-panel-layout" style={{ padding: '16px 0' }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a full-width single control row
|
||||
*/
|
||||
export const SingleControlRow: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>{children}</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a two-column control row
|
||||
*/
|
||||
export const TwoColumnRow: FC<{ left: ReactNode; right: ReactNode }> = ({
|
||||
left,
|
||||
right,
|
||||
}) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={12}>{left}</Col>
|
||||
<Col span={12}>{right}</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to create a three-column control row
|
||||
*/
|
||||
export const ThreeColumnRow: FC<{
|
||||
left: ReactNode;
|
||||
center: ReactNode;
|
||||
right: ReactNode;
|
||||
}> = ({ left, center, right }) => (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={8}>{left}</Col>
|
||||
<Col span={8}>{center}</Col>
|
||||
<Col span={8}>{right}</Col>
|
||||
</Row>
|
||||
);
|
||||
@@ -1,360 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Select, Input, Slider } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface DeckGLControlsSectionProps {
|
||||
layerType?:
|
||||
| 'scatter'
|
||||
| 'polygon'
|
||||
| 'path'
|
||||
| 'heatmap'
|
||||
| 'hex'
|
||||
| 'grid'
|
||||
| 'screengrid'
|
||||
| 'contour'
|
||||
| 'geojson'
|
||||
| 'arc';
|
||||
showViewport?: boolean;
|
||||
showMapStyle?: boolean;
|
||||
showColorScheme?: boolean;
|
||||
showLegend?: boolean;
|
||||
showTooltip?: boolean;
|
||||
showFilters?: boolean;
|
||||
showAnimation?: boolean;
|
||||
show3D?: boolean;
|
||||
showMultiplier?: boolean;
|
||||
showPointRadius?: boolean;
|
||||
showLineWidth?: boolean;
|
||||
showFillColor?: boolean;
|
||||
showStrokeColor?: boolean;
|
||||
showOpacity?: boolean;
|
||||
showCoverage?: boolean;
|
||||
showElevation?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const DeckGLControlsSection: FC<DeckGLControlsSectionProps> = ({
|
||||
layerType = 'scatter',
|
||||
showViewport = true,
|
||||
showMapStyle = true,
|
||||
showColorScheme = true,
|
||||
showLegend = true,
|
||||
showTooltip = true,
|
||||
showFilters = true,
|
||||
showAnimation = false,
|
||||
show3D = false,
|
||||
showMultiplier = false,
|
||||
showPointRadius = false,
|
||||
showLineWidth = false,
|
||||
showFillColor = false,
|
||||
showStrokeColor = false,
|
||||
showOpacity = true,
|
||||
showCoverage = false,
|
||||
showElevation = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => (
|
||||
<div className="deckgl-controls-section">
|
||||
{/* Map Style */}
|
||||
{showMapStyle && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Map Style')}</label>
|
||||
<Select
|
||||
value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'}
|
||||
onChange={value => onChange('mapbox_style', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/streets-v11',
|
||||
label: t('Streets'),
|
||||
},
|
||||
{ value: 'mapbox://styles/mapbox/light-v9', label: t('Light') },
|
||||
{ value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') },
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/satellite-v9',
|
||||
label: t('Satellite'),
|
||||
},
|
||||
{
|
||||
value: 'mapbox://styles/mapbox/outdoors-v11',
|
||||
label: t('Outdoors'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Base map style for the visualization')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Viewport */}
|
||||
{showViewport && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Zoom')}</label>
|
||||
<Slider
|
||||
value={values.zoom || 11}
|
||||
onChange={value => onChange('zoom', value)}
|
||||
min={0}
|
||||
max={22}
|
||||
step={0.1}
|
||||
marks={{ 0: '0', 11: '11', 22: '22' }}
|
||||
/>
|
||||
<small className="text-muted">{t('Map zoom level')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.autozoom || true}
|
||||
onChange={checked => onChange('autozoom', checked)}
|
||||
/>
|
||||
{t('Auto Zoom')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Automatically zoom to fit data bounds')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Point/Shape Size Controls */}
|
||||
{showPointRadius && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Point Radius')}</label>
|
||||
<Slider
|
||||
value={values.point_radius_fixed?.value || 1000}
|
||||
onChange={value =>
|
||||
onChange('point_radius_fixed', { type: 'fix', value })
|
||||
}
|
||||
min={1}
|
||||
max={10000}
|
||||
step={10}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Fixed radius for points in meters')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showLineWidth && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Line Width')}</label>
|
||||
<Slider
|
||||
value={values.line_width || 1}
|
||||
onChange={value => onChange('line_width', value)}
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
/>
|
||||
<small className="text-muted">{t('Width of lines in pixels')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* 3D Controls */}
|
||||
{show3D && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.extruded || false}
|
||||
onChange={checked => onChange('extruded', checked)}
|
||||
/>
|
||||
{t('3D')}
|
||||
</label>
|
||||
<small className="text-muted">{t('Show data in 3D')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
{values.extruded && showElevation && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Elevation')}</label>
|
||||
<Slider
|
||||
value={values.elevation || 0.1}
|
||||
onChange={value => onChange('elevation', value)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Elevation multiplier for 3D rendering')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Opacity */}
|
||||
{showOpacity && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Opacity')}</label>
|
||||
<Slider
|
||||
value={values.opacity || 80}
|
||||
onChange={value => onChange('opacity', value)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
marks={{ 0: '0%', 50: '50%', 100: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">{t('Layer opacity')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Coverage (for hex, grid) */}
|
||||
{showCoverage && (layerType === 'hex' || layerType === 'grid') && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Coverage')}</label>
|
||||
<Slider
|
||||
value={values.coverage || 1}
|
||||
onChange={value => onChange('coverage', value)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">{t('Cell coverage radius')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
{showLegend && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Legend Position')}</label>
|
||||
<Select
|
||||
value={values.legend_position || 'top_right'}
|
||||
onChange={value => onChange('legend_position', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'top_left', label: t('Top left') },
|
||||
{ value: 'top_right', label: t('Top right') },
|
||||
{ value: 'bottom_left', label: t('Bottom left') },
|
||||
{ value: 'bottom_right', label: t('Bottom right') },
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Legend Format')}</label>
|
||||
<Input
|
||||
value={values.legend_format || ''}
|
||||
onChange={e => onChange('legend_format', e.target.value)}
|
||||
placeholder=".3s"
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 number format for legend')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
{showFilters && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.filter_nulls || true}
|
||||
onChange={checked => onChange('filter_nulls', checked)}
|
||||
/>
|
||||
{t('Filter Nulls')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Filter out null values from data')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Tooltip */}
|
||||
{showTooltip && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Tooltip')}</label>
|
||||
<Input.TextArea
|
||||
value={values.js_tooltip || ''}
|
||||
onChange={e => onChange('js_tooltip', e.target.value)}
|
||||
placeholder={t('JavaScript tooltip generator')}
|
||||
rows={3}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('JavaScript code for custom tooltip')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Animation */}
|
||||
{showAnimation && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.animation || false}
|
||||
onChange={checked => onChange('animation', checked)}
|
||||
/>
|
||||
{t('Animate')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Animate visualization over time')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Multiplier for some visualizations */}
|
||||
{showMultiplier && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Multiplier')}</label>
|
||||
<Slider
|
||||
value={values.multiplier || 1}
|
||||
onChange={value => onChange('multiplier', value)}
|
||||
min={0.01}
|
||||
max={10}
|
||||
step={0.01}
|
||||
/>
|
||||
<small className="text-muted">{t('Value multiplier')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DeckGLControlsSection;
|
||||
@@ -1,336 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Select, Input, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface FilterControlsSectionProps {
|
||||
filterType: 'select' | 'range' | 'time' | 'time_column' | 'time_grain';
|
||||
showMultiple?: boolean;
|
||||
showSearch?: boolean;
|
||||
showParentFilter?: boolean;
|
||||
showDefaultValue?: boolean;
|
||||
showInverseSelection?: boolean;
|
||||
showDateFilter?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const FilterControlsSection: FC<FilterControlsSectionProps> = ({
|
||||
filterType,
|
||||
showMultiple = true,
|
||||
showSearch = true,
|
||||
showParentFilter = true,
|
||||
showDefaultValue = true,
|
||||
showInverseSelection = false,
|
||||
showDateFilter = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isSelect = filterType === 'select';
|
||||
const isRange = filterType === 'range';
|
||||
const isTime = filterType === 'time';
|
||||
const isTimeColumn = filterType === 'time_column';
|
||||
const isTimeGrain = filterType === 'time_grain';
|
||||
|
||||
return (
|
||||
<div className="filter-controls-section">
|
||||
{/* Multiple Selection */}
|
||||
{showMultiple && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.multiSelect || false}
|
||||
onChange={checked => onChange('multiSelect', checked)}
|
||||
/>
|
||||
{t('Multiple Select')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Allow selecting multiple values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
{showSearch && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.enableEmptyFilter || false}
|
||||
onChange={checked => onChange('enableEmptyFilter', checked)}
|
||||
/>
|
||||
{t('Enable Empty Filter')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Allow empty filter values')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Inverse Selection */}
|
||||
{showInverseSelection && isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.inverseSelection || false}
|
||||
onChange={checked => onChange('inverseSelection', checked)}
|
||||
/>
|
||||
{t('Inverse Selection')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Exclude selected values instead of including them')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Parent Filter */}
|
||||
{showParentFilter && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.parentFilter || false}
|
||||
onChange={checked => onChange('parentFilter', checked)}
|
||||
/>
|
||||
{t('Parent Filter')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Filter is dependent on another filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Default Value */}
|
||||
{showDefaultValue && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Default Value')}</label>
|
||||
{isSelect ? (
|
||||
<Input
|
||||
value={values.defaultValue || ''}
|
||||
onChange={e => onChange('defaultValue', e.target.value)}
|
||||
placeholder={t('Enter default value')}
|
||||
/>
|
||||
) : isRange ? (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<InputNumber
|
||||
value={values.defaultValueMin}
|
||||
onChange={value => onChange('defaultValueMin', value)}
|
||||
placeholder={t('Min')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<InputNumber
|
||||
value={values.defaultValueMax}
|
||||
onChange={value => onChange('defaultValueMax', value)}
|
||||
placeholder={t('Max')}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
value={values.defaultValue || ''}
|
||||
onChange={e => onChange('defaultValue', e.target.value)}
|
||||
placeholder={t('Enter default value')}
|
||||
/>
|
||||
)}
|
||||
<small className="text-muted">
|
||||
{t('Default value to use when filter is first loaded')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Sort Options for Select */}
|
||||
{isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Sort Filter Values')}</label>
|
||||
<Select
|
||||
value={values.sortFilter || false}
|
||||
onChange={value => onChange('sortFilter', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: false, label: t('No Sort') },
|
||||
{ value: true, label: t('Sort Ascending') },
|
||||
{ value: 'desc', label: t('Sort Descending') },
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Sort filter values alphabetically')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Search for Select Filter */}
|
||||
{isSelect && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.searchAllOptions || false}
|
||||
onChange={checked => onChange('searchAllOptions', checked)}
|
||||
/>
|
||||
{t('Search All Options')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Search all filter options, not just displayed ones')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Range Options */}
|
||||
{isRange && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Min Value')}</label>
|
||||
<InputNumber
|
||||
value={values.rangeMin}
|
||||
onChange={value => onChange('rangeMin', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Minimum value for the range')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Max Value')}</label>
|
||||
<InputNumber
|
||||
value={values.rangeMax}
|
||||
onChange={value => onChange('rangeMax', value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Maximum value for the range')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Time Options */}
|
||||
{(isTime || isTimeColumn) && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.defaultToFirstValue || false}
|
||||
onChange={checked => onChange('defaultToFirstValue', checked)}
|
||||
/>
|
||||
{t('Default to First Value')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Default to the first available time value')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Time Grain Options */}
|
||||
{isTimeGrain && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Default Time Grain')}</label>
|
||||
<Select
|
||||
value={values.defaultTimeGrain || 'day'}
|
||||
onChange={value => onChange('defaultTimeGrain', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'minute', label: t('Minute') },
|
||||
{ value: 'hour', label: t('Hour') },
|
||||
{ value: 'day', label: t('Day') },
|
||||
{ value: 'week', label: t('Week') },
|
||||
{ value: 'month', label: t('Month') },
|
||||
{ value: 'quarter', label: t('Quarter') },
|
||||
{ value: 'year', label: t('Year') },
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Default time granularity')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* UI Configuration */}
|
||||
<h4 style={{ marginTop: 24, marginBottom: 16 }}>
|
||||
{t('UI Configuration')}
|
||||
</h4>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.instant_filtering || true}
|
||||
onChange={checked => onChange('instant_filtering', checked)}
|
||||
/>
|
||||
{t('Instant Filtering')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Apply filters instantly as they change')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_apply || false}
|
||||
onChange={checked => onChange('show_apply', checked)}
|
||||
/>
|
||||
{t('Show Apply Button')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show an apply button for the filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_clear || true}
|
||||
onChange={checked => onChange('show_clear', checked)}
|
||||
/>
|
||||
{t('Show Clear Button')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show a clear button for the filter')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterControlsSection;
|
||||
@@ -1,241 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface FormatControlGroupProps {
|
||||
showNumber?: boolean;
|
||||
showCurrency?: boolean;
|
||||
showDate?: boolean;
|
||||
showPercentage?: boolean;
|
||||
numberFormatLabel?: string;
|
||||
currencyFormatLabel?: string;
|
||||
dateFormatLabel?: string;
|
||||
percentageFormatLabel?: string;
|
||||
customFormatOptions?: Array<[string, string]>;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
export const D3_FORMAT_OPTIONS = [
|
||||
['SMART_NUMBER', t('Adaptive formatting')],
|
||||
['~g', t('Original value')],
|
||||
['d', t('Signed integer')],
|
||||
['.0f', t('Integer')],
|
||||
['.1f', t('1 decimal place')],
|
||||
['.2f', t('2 decimal places')],
|
||||
['.3f', t('3 decimal places')],
|
||||
['.4f', t('4 decimal places')],
|
||||
['.5f', t('5 decimal places')],
|
||||
['+,', t('Positive integer')],
|
||||
['+,.0f', t('Positive number')],
|
||||
['+,.1f', t('Positive (1 decimal)')],
|
||||
['+,.2f', t('Positive (2 decimals)')],
|
||||
[',.0f', t('Number (no decimals)')],
|
||||
[',.1f', t('Number (1 decimal)')],
|
||||
[',.2f', t('Number (2 decimals)')],
|
||||
[',.3f', t('Number (3 decimals)')],
|
||||
['.0%', t('Percentage')],
|
||||
['.1%', t('Percentage (1 decimal)')],
|
||||
['.2%', t('Percentage (2 decimals)')],
|
||||
['.3%', t('Percentage (3 decimals)')],
|
||||
[',.0%', t('Percentage with thousands')],
|
||||
['.1s', t('SI notation')],
|
||||
['.2s', t('SI notation (2 decimals)')],
|
||||
['.3s', t('SI notation (3 decimals)')],
|
||||
['$,.0f', t('Currency (no decimals)')],
|
||||
['$,.1f', t('Currency (1 decimal)')],
|
||||
['$,.2f', t('Currency (2 decimals)')],
|
||||
['$,.3f', t('Currency (3 decimals)')],
|
||||
];
|
||||
|
||||
export const D3_TIME_FORMAT_OPTIONS = [
|
||||
['smart_date', t('Adaptive formatting')],
|
||||
['%Y-%m-%d', t('YYYY-MM-DD')],
|
||||
['%Y-%m-%d %H:%M', t('YYYY-MM-DD HH:MM')],
|
||||
['%Y-%m-%d %H:%M:%S', t('YYYY-MM-DD HH:MM:SS')],
|
||||
['%Y/%m/%d', t('YYYY/MM/DD')],
|
||||
['%m/%d/%Y', t('MM/DD/YYYY')],
|
||||
['%d/%m/%Y', t('DD/MM/YYYY')],
|
||||
['%d.%m.%Y', t('DD.MM.YYYY')],
|
||||
['%Y', t('Year (YYYY)')],
|
||||
['%B %Y', t('Month Year (January 2023)')],
|
||||
['%b %Y', t('Month Year (Jan 2023)')],
|
||||
['%B', t('Month (January)')],
|
||||
['%b', t('Month (Jan)')],
|
||||
['%B %-d, %Y', t('Month Day, Year')],
|
||||
['%b %-d, %Y', t('Mon Day, Year')],
|
||||
['%a', t('Day of week (short)')],
|
||||
['%A', t('Day of week (full)')],
|
||||
['%H:%M', t('Time (24-hour)')],
|
||||
['%I:%M %p', t('Time (12-hour)')],
|
||||
['%H:%M:%S', t('Time with seconds')],
|
||||
];
|
||||
|
||||
const CURRENCY_OPTIONS = [
|
||||
{ value: 'USD', label: 'USD ($)' },
|
||||
{ value: 'EUR', label: 'EUR (€)' },
|
||||
{ value: 'GBP', label: 'GBP (£)' },
|
||||
{ value: 'JPY', label: 'JPY (¥)' },
|
||||
{ value: 'CNY', label: 'CNY (¥)' },
|
||||
{ value: 'INR', label: 'INR (₹)' },
|
||||
{ value: 'CAD', label: 'CAD ($)' },
|
||||
{ value: 'AUD', label: 'AUD ($)' },
|
||||
{ value: 'CHF', label: 'CHF (Fr)' },
|
||||
{ value: 'SEK', label: 'SEK (kr)' },
|
||||
{ value: 'NOK', label: 'NOK (kr)' },
|
||||
{ value: 'DKK', label: 'DKK (kr)' },
|
||||
{ value: 'KRW', label: 'KRW (₩)' },
|
||||
{ value: 'BRL', label: 'BRL (R$)' },
|
||||
{ value: 'MXN', label: 'MXN ($)' },
|
||||
{ value: 'RUB', label: 'RUB (₽)' },
|
||||
];
|
||||
|
||||
const FormatControlGroup: FC<FormatControlGroupProps> = ({
|
||||
showNumber = true,
|
||||
showCurrency = false,
|
||||
showDate = false,
|
||||
showPercentage = false,
|
||||
numberFormatLabel = t('Number format'),
|
||||
currencyFormatLabel = t('Currency'),
|
||||
dateFormatLabel = t('Date format'),
|
||||
percentageFormatLabel = t('Percentage format'),
|
||||
customFormatOptions = [],
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const formatOptions =
|
||||
customFormatOptions.length > 0 ? customFormatOptions : D3_FORMAT_OPTIONS;
|
||||
|
||||
return (
|
||||
<div className="format-control-group">
|
||||
{showNumber && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{numberFormatLabel}</label>
|
||||
<Select
|
||||
value={values.number_format || 'SMART_NUMBER'}
|
||||
onChange={value => onChange('number_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={formatOptions.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 format string for numbers. See ')}
|
||||
<a
|
||||
href="https://github.com/d3/d3-format/blob/main/README.md#format"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('D3 format docs')}
|
||||
</a>
|
||||
{t(' for details.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showCurrency && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{currencyFormatLabel}</label>
|
||||
<Select
|
||||
value={values.currency_format || 'USD'}
|
||||
onChange={value => onChange('currency_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select currency')}
|
||||
options={CURRENCY_OPTIONS}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Currency to use for formatting')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showDate && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{dateFormatLabel}</label>
|
||||
<Select
|
||||
value={values.date_format || 'smart_date'}
|
||||
onChange={value => onChange('date_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 time format string. See ')}
|
||||
<a
|
||||
href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('D3 time format docs')}
|
||||
</a>
|
||||
{t(' for details.')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{showPercentage && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{percentageFormatLabel}</label>
|
||||
<Select
|
||||
value={values.percentage_format || '.0%'}
|
||||
onChange={value => onChange('percentage_format', value)}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
placeholder={t('Select or type a custom format')}
|
||||
options={[
|
||||
['.0%', t('0%')],
|
||||
['.1%', t('0.1%')],
|
||||
['.2%', t('0.12%')],
|
||||
['.3%', t('0.123%')],
|
||||
[',.0%', t('1,234%')],
|
||||
[',.1%', t('1,234.5%')],
|
||||
].map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 format for percentages')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormatControlGroup;
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, useMemo } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select } from '@superset-ui/core/components';
|
||||
import { ControlComponentProps, ColumnMeta } from '../../types';
|
||||
import { ControlHeader } from '../../components/ControlHeader';
|
||||
|
||||
export interface GranularityControlValue {
|
||||
column_name: string;
|
||||
type?: string;
|
||||
is_dttm?: boolean;
|
||||
}
|
||||
|
||||
export interface GranularityControlProps
|
||||
extends ControlComponentProps<GranularityControlValue | string> {
|
||||
columns?: ColumnMeta[];
|
||||
datasource?: {
|
||||
columns?: ColumnMeta[];
|
||||
verbose_map?: Record<string, string>;
|
||||
};
|
||||
clearable?: boolean;
|
||||
temporalColumnsOnly?: boolean;
|
||||
}
|
||||
|
||||
const GranularityControl: FC<GranularityControlProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
columns = [],
|
||||
datasource,
|
||||
clearable = false,
|
||||
temporalColumnsOnly = true,
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
validationErrors,
|
||||
renderTrigger,
|
||||
...props
|
||||
}) => {
|
||||
const allColumns = useMemo(() => {
|
||||
const cols = columns.length > 0 ? columns : datasource?.columns || [];
|
||||
if (temporalColumnsOnly) {
|
||||
return cols.filter(col => col.is_dttm);
|
||||
}
|
||||
return cols;
|
||||
}, [columns, datasource?.columns, temporalColumnsOnly]);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
allColumns.map(col => ({
|
||||
value: col.column_name,
|
||||
label:
|
||||
datasource?.verbose_map?.[col.column_name] ||
|
||||
col.verbose_name ||
|
||||
col.column_name,
|
||||
})),
|
||||
[allColumns, datasource?.verbose_map],
|
||||
);
|
||||
|
||||
const currentValue = useMemo(() => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return value?.column_name;
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (newValue: string | undefined) => {
|
||||
if (onChange) {
|
||||
if (!newValue && clearable) {
|
||||
onChange(null as any);
|
||||
} else if (newValue) {
|
||||
const column = allColumns.find(col => col.column_name === newValue);
|
||||
if (column) {
|
||||
onChange({
|
||||
column_name: column.column_name,
|
||||
type: column.type,
|
||||
is_dttm: column.is_dttm,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ControlHeader
|
||||
name={name}
|
||||
label={label || t('Time Column')}
|
||||
description={
|
||||
description ||
|
||||
t(
|
||||
'The time column for the visualization. Note that you ' +
|
||||
'can define arbitrary expression that return a DATETIME ' +
|
||||
'column in the table. Also note that the ' +
|
||||
'filter below is applied against this column or ' +
|
||||
'expression',
|
||||
)
|
||||
}
|
||||
validationErrors={validationErrors}
|
||||
renderTrigger={renderTrigger}
|
||||
/>
|
||||
<Select
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
placeholder={t('Select a temporal column')}
|
||||
allowClear={clearable}
|
||||
showSearch
|
||||
css={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GranularityControl;
|
||||
@@ -1,268 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { CustomControlItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Helper function to create a SelectControl configuration
|
||||
*/
|
||||
export const SelectControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
choices?: any[][] | (() => any[][]) | any[];
|
||||
description?: string;
|
||||
freeForm?: boolean;
|
||||
clearable?: boolean;
|
||||
multiple?: boolean;
|
||||
validators?: any[];
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a TextControl configuration
|
||||
*/
|
||||
export const TextControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
description?: string;
|
||||
isInt?: boolean;
|
||||
isFloat?: boolean;
|
||||
validators?: any[];
|
||||
renderTrigger?: boolean;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a CheckboxControl configuration
|
||||
*/
|
||||
export const CheckboxControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SliderControl configuration
|
||||
*/
|
||||
export const SliderControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SliderControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a RadioButtonControl configuration
|
||||
*/
|
||||
export const RadioButtonControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: any;
|
||||
options?: any[][] | any[];
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'RadioButtonControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a BoundsControl configuration
|
||||
*/
|
||||
export const BoundsControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: [number | null, number | null];
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'BoundsControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a ColorPickerControl configuration
|
||||
*/
|
||||
export const ColorPickerControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: { r: number; g: number; b: number; a: number };
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'ColorPickerControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a DateFilterControl configuration
|
||||
*/
|
||||
export const DateFilterControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: string;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'DateFilterControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SwitchControl configuration
|
||||
*/
|
||||
export const SwitchControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SwitchControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a HiddenControl configuration
|
||||
*/
|
||||
export const HiddenControl = (config: {
|
||||
name: string;
|
||||
default?: any;
|
||||
initialValue?: any;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'HiddenControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a SpatialControl configuration
|
||||
*/
|
||||
export const SpatialControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
validators?: any[];
|
||||
mapStateToProps?: (state: any) => any;
|
||||
renderTrigger?: boolean;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'SpatialControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a ContourControl configuration
|
||||
*/
|
||||
export const ContourControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
mapStateToProps?: (state: any) => any;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'ContourControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to create a TextAreaControl configuration
|
||||
*/
|
||||
export const TextAreaControl = (config: {
|
||||
name: string;
|
||||
label: string;
|
||||
default?: string;
|
||||
description?: string;
|
||||
renderTrigger?: boolean;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
[key: string]: any;
|
||||
}): CustomControlItem => ({
|
||||
name: config.name,
|
||||
config: {
|
||||
type: 'TextAreaControl',
|
||||
...config,
|
||||
},
|
||||
});
|
||||
@@ -1,241 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select, Switch, InputNumber, Input } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface LabelControlGroupProps {
|
||||
chartType?: 'pie' | 'sunburst' | 'treemap' | 'funnel' | 'gauge';
|
||||
showLabelType?: boolean;
|
||||
showTemplate?: boolean;
|
||||
showThreshold?: boolean;
|
||||
showOutside?: boolean;
|
||||
showLabelLine?: boolean;
|
||||
showRotation?: boolean;
|
||||
showUpperLabels?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const LABEL_TYPE_OPTIONS = [
|
||||
['key', t('Category Name')],
|
||||
['value', t('Value')],
|
||||
['percent', t('Percentage')],
|
||||
['key_value', t('Category and Value')],
|
||||
['key_percent', t('Category and Percentage')],
|
||||
['key_value_percent', t('Category, Value and Percentage')],
|
||||
['value_percent', t('Value and Percentage')],
|
||||
['template', t('Template')],
|
||||
];
|
||||
|
||||
const LABEL_ROTATION_OPTIONS = [
|
||||
['0', t('Horizontal')],
|
||||
['45', t('45°')],
|
||||
['90', t('Vertical')],
|
||||
['-45', t('-45°')],
|
||||
];
|
||||
|
||||
const LabelControlGroup: FC<LabelControlGroupProps> = ({
|
||||
chartType = 'pie',
|
||||
showLabelType = true,
|
||||
showTemplate = true,
|
||||
showThreshold = true,
|
||||
showOutside = false,
|
||||
showLabelLine = false,
|
||||
showRotation = false,
|
||||
showUpperLabels = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const showLabels = values.show_labels ?? true;
|
||||
const labelType = values.label_type || 'key';
|
||||
|
||||
return (
|
||||
<div className="label-control-group">
|
||||
{/* Show Labels Toggle */}
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={showLabels}
|
||||
onChange={checked => onChange('show_labels', checked)}
|
||||
/>
|
||||
{t('Show Labels')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Whether to display the labels')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{showLabels && (
|
||||
<>
|
||||
{/* Label Type */}
|
||||
{showLabelType && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Type')}</label>
|
||||
<Select
|
||||
value={labelType}
|
||||
onChange={value => onChange('label_type', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={LABEL_TYPE_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('What should be shown on the label?')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Label Template */}
|
||||
{showTemplate && labelType === 'template' && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Template')}</label>
|
||||
<Input.TextArea
|
||||
value={values.label_template || ''}
|
||||
onChange={e => onChange('label_template', e.target.value)}
|
||||
placeholder="{name}: {value} ({percent}%)"
|
||||
rows={3}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'Format data labels. Use variables: {name}, {value}, {percent}. \\n represents a new line.',
|
||||
)}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Label Threshold */}
|
||||
{showThreshold && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Threshold')}</label>
|
||||
<InputNumber
|
||||
value={values.show_labels_threshold ?? 5}
|
||||
onChange={value => onChange('show_labels_threshold', value)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.5}
|
||||
formatter={value => `${value}%`}
|
||||
parser={value => Number((value as string).replace('%', ''))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'Minimum threshold in percentage points for showing labels',
|
||||
)}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Labels Outside (Pie specific) */}
|
||||
{showOutside && chartType === 'pie' && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
|
||||
>
|
||||
<Switch
|
||||
checked={values.labels_outside || false}
|
||||
onChange={checked => onChange('labels_outside', checked)}
|
||||
/>
|
||||
{t('Put labels outside')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Put the labels outside of the pie?')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Label Line (Pie specific) */}
|
||||
{showLabelLine && chartType === 'pie' && values.labels_outside && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
|
||||
>
|
||||
<Switch
|
||||
checked={values.label_line || false}
|
||||
onChange={checked => onChange('label_line', checked)}
|
||||
/>
|
||||
{t('Label Line')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Draw a line from the label to the slice')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Label Rotation */}
|
||||
{showRotation && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Label Rotation')}</label>
|
||||
<Select
|
||||
value={values.label_rotation || '0'}
|
||||
onChange={value => onChange('label_rotation', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={LABEL_ROTATION_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Rotation angle of labels')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Show Upper Labels (Treemap specific) */}
|
||||
{showUpperLabels && chartType === 'treemap' && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8 }}
|
||||
>
|
||||
<Switch
|
||||
checked={values.show_upper_labels || false}
|
||||
onChange={checked => onChange('show_upper_labels', checked)}
|
||||
/>
|
||||
{t('Show Upper Labels')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show labels for parent nodes')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelControlGroup;
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Slider, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface MarkerControlGroupProps {
|
||||
enabledLabel?: string;
|
||||
sizeLabel?: string;
|
||||
maxSize?: number;
|
||||
minSize?: number;
|
||||
defaultSize?: number;
|
||||
values?: {
|
||||
markerEnabled?: boolean;
|
||||
markerSize?: number;
|
||||
};
|
||||
onChange?: (name: string, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MarkerControlGroup: FC<MarkerControlGroupProps> = ({
|
||||
enabledLabel = t('Show markers'),
|
||||
sizeLabel = t('Marker size'),
|
||||
maxSize = 20,
|
||||
minSize = 0,
|
||||
defaultSize = 6,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
disabled = false,
|
||||
}) => {
|
||||
const markerEnabled = values.markerEnabled ?? false;
|
||||
const markerSize = values.markerSize ?? defaultSize;
|
||||
|
||||
const handleEnabledChange = (checked: boolean) => {
|
||||
onChange('markerEnabled', checked);
|
||||
if (checked && !values.markerSize) {
|
||||
onChange('markerSize', defaultSize);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSizeChange = (value: number) => {
|
||||
onChange('markerSize', value);
|
||||
};
|
||||
|
||||
const handleInputChange = (value: number | null) => {
|
||||
if (value !== null) {
|
||||
const clampedValue = Math.max(minSize, Math.min(maxSize, value));
|
||||
onChange('markerSize', clampedValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="marker-control-group">
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={markerEnabled}
|
||||
onChange={handleEnabledChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{enabledLabel}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Draw markers on data points for better visibility')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{markerEnabled && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>
|
||||
{sizeLabel}
|
||||
</label>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
min={minSize}
|
||||
max={maxSize}
|
||||
step={1}
|
||||
value={markerSize}
|
||||
onChange={handleSizeChange}
|
||||
disabled={disabled || !markerEnabled}
|
||||
marks={{
|
||||
[minSize]: minSize.toString(),
|
||||
[Math.floor(maxSize / 2)]: Math.floor(
|
||||
maxSize / 2,
|
||||
).toString(),
|
||||
[maxSize]: maxSize.toString(),
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={minSize}
|
||||
max={maxSize}
|
||||
step={1}
|
||||
value={markerSize}
|
||||
onChange={handleInputChange}
|
||||
disabled={disabled || !markerEnabled}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<small className="text-muted">
|
||||
{t('Size of the markers in pixels')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkerControlGroup;
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Slider, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface OpacityControlProps {
|
||||
name?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
marks?: Record<number, string>;
|
||||
}
|
||||
|
||||
const OpacityControl: FC<OpacityControlProps> = ({
|
||||
name = 'opacity',
|
||||
label = t('Opacity'),
|
||||
description = t('Opacity of the elements'),
|
||||
min = 0,
|
||||
max = 1,
|
||||
step = 0.1,
|
||||
value = 0.8,
|
||||
onChange = () => {},
|
||||
disabled = false,
|
||||
marks,
|
||||
}) => {
|
||||
const defaultMarks = marks || {
|
||||
0: '0%',
|
||||
0.25: '25%',
|
||||
0.5: '50%',
|
||||
0.75: '75%',
|
||||
1: '100%',
|
||||
};
|
||||
|
||||
const handleSliderChange = (val: number) => {
|
||||
onChange(val);
|
||||
};
|
||||
|
||||
const handleInputChange = (val: number | null) => {
|
||||
if (val !== null) {
|
||||
const clampedValue = Math.max(min, Math.min(max, val));
|
||||
onChange(clampedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const percentageValue = Math.round(value * 100);
|
||||
|
||||
return (
|
||||
<div className="opacity-control" data-name={name}>
|
||||
<label style={{ display: 'block', marginBottom: 8 }}>{label}</label>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={handleSliderChange}
|
||||
marks={defaultMarks}
|
||||
disabled={disabled}
|
||||
tooltip={{
|
||||
formatter: val => `${Math.round((val as number) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={min * 100}
|
||||
max={max * 100}
|
||||
step={step * 100}
|
||||
value={percentageValue}
|
||||
onChange={val => handleInputChange(val !== null ? val / 100 : null)}
|
||||
formatter={val => `${val}%`}
|
||||
parser={val => Number((val as string).replace('%', ''))}
|
||||
disabled={disabled}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{description && (
|
||||
<small
|
||||
className="text-muted"
|
||||
style={{ display: 'block', marginTop: 4 }}
|
||||
>
|
||||
{description}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpacityControl;
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select, Switch, Slider, InputNumber } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
|
||||
export interface PieShapeControlProps {
|
||||
showDonut?: boolean;
|
||||
showRoseType?: boolean;
|
||||
showRadius?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const ROSE_TYPE_OPTIONS = [
|
||||
['area', t('Area')],
|
||||
['radius', t('Radius')],
|
||||
[null, t('None')],
|
||||
];
|
||||
|
||||
const PieShapeControl: FC<PieShapeControlProps> = ({
|
||||
showDonut = true,
|
||||
showRoseType = true,
|
||||
showRadius = true,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isDonut = values.donut || false;
|
||||
const innerRadius = values.innerRadius || 30;
|
||||
const outerRadius = values.outerRadius || 70;
|
||||
|
||||
return (
|
||||
<div className="pie-shape-control">
|
||||
{/* Donut Toggle */}
|
||||
{showDonut && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={isDonut}
|
||||
onChange={checked => onChange('donut', checked)}
|
||||
/>
|
||||
{t('Donut')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Do you want a donut or a pie?')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Inner Radius (for Donut) */}
|
||||
{showRadius && isDonut && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Inner Radius')}</label>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={innerRadius}
|
||||
onChange={value => onChange('innerRadius', value)}
|
||||
marks={{
|
||||
0: '0%',
|
||||
50: '50%',
|
||||
100: '100%',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={innerRadius}
|
||||
onChange={value => onChange('innerRadius', value)}
|
||||
formatter={value => `${value}%`}
|
||||
parser={value => Number((value as string).replace('%', ''))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<small className="text-muted">
|
||||
{t('Inner radius of donut hole')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Outer Radius */}
|
||||
{showRadius && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Outer Radius')}</label>
|
||||
<Row gutter={16} align="middle">
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={outerRadius}
|
||||
onChange={value => onChange('outerRadius', value)}
|
||||
marks={{
|
||||
0: '0%',
|
||||
50: '50%',
|
||||
100: '100%',
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={outerRadius}
|
||||
onChange={value => onChange('outerRadius', value)}
|
||||
formatter={value => `${value}%`}
|
||||
parser={value => Number((value as string).replace('%', ''))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<small className="text-muted">
|
||||
{t('Outer edge of the pie/donut')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Rose Type (Nightingale Chart) */}
|
||||
{showRoseType && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Rose Type')}</label>
|
||||
<Select
|
||||
value={values.roseType || null}
|
||||
onChange={value => onChange('roseType', value)}
|
||||
style={{ width: '100%' }}
|
||||
allowClear
|
||||
placeholder={t('None')}
|
||||
options={ROSE_TYPE_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Whether to show as Nightingale chart (polar area chart)')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PieShapeControl;
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC, ReactElement, useMemo } from 'react';
|
||||
import { JsonValue } from '@superset-ui/core';
|
||||
|
||||
export interface ReactControlPanelSection {
|
||||
key: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
expanded?: boolean;
|
||||
render: (props: ControlPanelRenderProps) => ReactElement;
|
||||
}
|
||||
|
||||
export interface ControlPanelRenderProps {
|
||||
values: Record<string, JsonValue>;
|
||||
onChange: (name: string, value: JsonValue) => void;
|
||||
datasource?: any;
|
||||
formData?: any;
|
||||
validationErrors?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface ReactControlPanelConfig {
|
||||
sections: ReactControlPanelSection[];
|
||||
onFormDataChange?: (formData: any) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component that allows rendering control panels with actual React components
|
||||
* instead of configuration objects. This provides a stepping stone toward JSON-driven forms.
|
||||
*/
|
||||
export const ReactControlPanel: FC<{
|
||||
config: ReactControlPanelConfig;
|
||||
values: Record<string, JsonValue>;
|
||||
onChange: (name: string, value: JsonValue) => void;
|
||||
datasource?: any;
|
||||
formData?: any;
|
||||
validationErrors?: Record<string, string[]>;
|
||||
}> = ({ config, values, onChange, datasource, formData, validationErrors }) => {
|
||||
const renderProps: ControlPanelRenderProps = useMemo(
|
||||
() => ({
|
||||
values,
|
||||
onChange,
|
||||
datasource,
|
||||
formData,
|
||||
validationErrors,
|
||||
}),
|
||||
[values, onChange, datasource, formData, validationErrors],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="react-control-panel">
|
||||
{config.sections.map(section => (
|
||||
<div key={section.key} className="control-panel-section">
|
||||
<h4>{section.label}</h4>
|
||||
{section.description && (
|
||||
<p className="section-description">{section.description}</p>
|
||||
)}
|
||||
{section.render(renderProps)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to create a control panel configuration that works with
|
||||
* both the old and new systems
|
||||
*/
|
||||
export function createReactControlPanel(config: ReactControlPanelConfig): any {
|
||||
return {
|
||||
controlPanelSections: config.sections.map(section => ({
|
||||
label: section.label,
|
||||
description: section.description,
|
||||
expanded: section.expanded ?? true,
|
||||
controlSetRows: [
|
||||
[
|
||||
// Return a React element that will be rendered directly
|
||||
<ReactControlPanel
|
||||
key={section.key}
|
||||
config={{ sections: [section] }}
|
||||
values={{}}
|
||||
onChange={() => {}}
|
||||
/>,
|
||||
],
|
||||
],
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { CustomControlItem } from '../../types';
|
||||
import sharedControls from '../sharedControls';
|
||||
|
||||
/**
|
||||
* React component wrappers for shared controls.
|
||||
* These replace string references like ['metrics'] with actual component calls.
|
||||
*/
|
||||
|
||||
// Metrics controls
|
||||
export const MetricsControl = (): CustomControlItem => ({
|
||||
name: 'metrics',
|
||||
config: sharedControls.metrics,
|
||||
});
|
||||
|
||||
export const MetricControl = (): CustomControlItem => ({
|
||||
name: 'metric',
|
||||
config: sharedControls.metric,
|
||||
});
|
||||
|
||||
export const SecondaryMetricControl = (): CustomControlItem => ({
|
||||
name: 'secondary_metric',
|
||||
config: sharedControls.secondary_metric,
|
||||
});
|
||||
|
||||
export const Metric2Control = (): CustomControlItem => ({
|
||||
name: 'metric_2',
|
||||
config: sharedControls.metric_2,
|
||||
});
|
||||
|
||||
export const TimeLimitMetricControl = (): CustomControlItem => ({
|
||||
name: 'timeseries_limit_metric',
|
||||
config: sharedControls.timeseries_limit_metric,
|
||||
});
|
||||
|
||||
export const OrderByControl = (): CustomControlItem => ({
|
||||
name: 'orderby',
|
||||
config: sharedControls.orderby,
|
||||
});
|
||||
|
||||
export const SeriesLimitMetricControl = (): CustomControlItem => ({
|
||||
name: 'series_limit_metric',
|
||||
config: sharedControls.series_limit_metric,
|
||||
});
|
||||
|
||||
export const SortByMetricControl = (): CustomControlItem => ({
|
||||
name: 'sort_by_metric',
|
||||
config: sharedControls.sort_by_metric,
|
||||
});
|
||||
|
||||
// Dimension controls
|
||||
export const GroupByControl = (): CustomControlItem => ({
|
||||
name: 'groupby',
|
||||
config: sharedControls.groupby,
|
||||
});
|
||||
|
||||
export const ColumnsControl = (): CustomControlItem => ({
|
||||
name: 'columns',
|
||||
config: sharedControls.columns,
|
||||
});
|
||||
|
||||
// Note: These controls are not in sharedControls, using columns as fallback
|
||||
export const AllColumnsControl = (): CustomControlItem => ({
|
||||
name: 'all_columns',
|
||||
config: sharedControls.columns || {},
|
||||
});
|
||||
|
||||
export const AllColumnsXControl = (): CustomControlItem => ({
|
||||
name: 'all_columns_x',
|
||||
config: sharedControls.columns || {},
|
||||
});
|
||||
|
||||
export const AllColumnsYControl = (): CustomControlItem => ({
|
||||
name: 'all_columns_y',
|
||||
config: sharedControls.columns || {},
|
||||
});
|
||||
|
||||
export const SeriesControl = (): CustomControlItem => ({
|
||||
name: 'series',
|
||||
config: sharedControls.series,
|
||||
});
|
||||
|
||||
export const EntityControl = (): CustomControlItem => ({
|
||||
name: 'entity',
|
||||
config: sharedControls.entity,
|
||||
});
|
||||
|
||||
export const XControl = (): CustomControlItem => ({
|
||||
name: 'x',
|
||||
config: sharedControls.x,
|
||||
});
|
||||
|
||||
export const YControl = (): CustomControlItem => ({
|
||||
name: 'y',
|
||||
config: sharedControls.y,
|
||||
});
|
||||
|
||||
// Note: sort_by is not in sharedControls, using a default config
|
||||
export const SortByControl = (): CustomControlItem => ({
|
||||
name: 'sort_by',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: 'Sort By',
|
||||
description: 'Sort by column',
|
||||
},
|
||||
});
|
||||
|
||||
export const SizeControl = (): CustomControlItem => ({
|
||||
name: 'size',
|
||||
config: sharedControls.size,
|
||||
});
|
||||
|
||||
export const XAxisControl = (): CustomControlItem => ({
|
||||
name: 'x_axis',
|
||||
config: sharedControls.x_axis,
|
||||
});
|
||||
|
||||
// Filter controls
|
||||
export const AdhocFiltersControl = (): CustomControlItem => ({
|
||||
name: 'adhoc_filters',
|
||||
config: sharedControls.adhoc_filters,
|
||||
});
|
||||
|
||||
export const TimeRangeControl = (): CustomControlItem => ({
|
||||
name: 'time_range',
|
||||
config: sharedControls.time_range,
|
||||
});
|
||||
|
||||
export const TimeGrainSqlaControl = (): CustomControlItem => ({
|
||||
name: 'time_grain_sqla',
|
||||
config: sharedControls.time_grain_sqla,
|
||||
});
|
||||
|
||||
export const GranularityControl = (): CustomControlItem => ({
|
||||
name: 'granularity',
|
||||
config: sharedControls.granularity,
|
||||
});
|
||||
|
||||
export const GranularitySqlaControl = (): CustomControlItem => ({
|
||||
name: 'granularity_sqla',
|
||||
config: sharedControls.granularity_sqla,
|
||||
});
|
||||
|
||||
// Limit controls
|
||||
export const RowLimitControl = (): CustomControlItem => ({
|
||||
name: 'row_limit',
|
||||
config: sharedControls.row_limit,
|
||||
});
|
||||
|
||||
export const LimitControl = (): CustomControlItem => ({
|
||||
name: 'limit',
|
||||
config: sharedControls.limit,
|
||||
});
|
||||
|
||||
export const GroupOthersWhenLimitReachedControl = (): CustomControlItem => ({
|
||||
name: 'group_others_when_limit_reached',
|
||||
config: sharedControls.group_others_when_limit_reached,
|
||||
});
|
||||
|
||||
export const SeriesLimitControl = (): CustomControlItem => ({
|
||||
name: 'series_limit',
|
||||
config: sharedControls.series_limit,
|
||||
});
|
||||
|
||||
// Sort controls
|
||||
export const OrderDescControl = (): CustomControlItem => ({
|
||||
name: 'order_desc',
|
||||
config: sharedControls.order_desc,
|
||||
});
|
||||
|
||||
export const OrderByColsControl = (): CustomControlItem => ({
|
||||
name: 'order_by_cols',
|
||||
config: sharedControls.order_by_cols,
|
||||
});
|
||||
|
||||
// Color controls
|
||||
export const ColorSchemeControl = (): CustomControlItem => ({
|
||||
name: 'color_scheme',
|
||||
config: sharedControls.color_scheme,
|
||||
});
|
||||
|
||||
export const LinearColorSchemeControl = (): CustomControlItem => ({
|
||||
name: 'linear_color_scheme',
|
||||
config: sharedControls.linear_color_scheme,
|
||||
});
|
||||
|
||||
export const ColorPickerControl = (): CustomControlItem => ({
|
||||
name: 'color_picker',
|
||||
config: sharedControls.color_picker,
|
||||
});
|
||||
|
||||
export const TimeShiftColorControl = (): CustomControlItem => ({
|
||||
name: 'time_shift_color',
|
||||
config: sharedControls.time_shift_color,
|
||||
});
|
||||
|
||||
export const CurrencyFormatControl = (): CustomControlItem => ({
|
||||
name: 'currency_format',
|
||||
config: sharedControls.currency_format,
|
||||
});
|
||||
|
||||
export const TruncateMetricControl = (): CustomControlItem => ({
|
||||
name: 'truncate_metric',
|
||||
config: sharedControls.truncate_metric,
|
||||
});
|
||||
|
||||
export const ShowEmptyColumnsControl = (): CustomControlItem => ({
|
||||
name: 'show_empty_columns',
|
||||
config: sharedControls.show_empty_columns,
|
||||
});
|
||||
|
||||
// Other controls
|
||||
export const ZoomableControl = (): CustomControlItem => ({
|
||||
name: 'zoomable',
|
||||
config: sharedControls.zoomable,
|
||||
});
|
||||
|
||||
export const DatasourceControl = (): CustomControlItem => ({
|
||||
name: 'datasource',
|
||||
config: sharedControls.datasource,
|
||||
});
|
||||
|
||||
export const VizTypeControl = (): CustomControlItem => ({
|
||||
name: 'viz_type',
|
||||
config: sharedControls.viz_type,
|
||||
});
|
||||
|
||||
// Tooltip controls
|
||||
export const TooltipColumnsControl = (): CustomControlItem => ({
|
||||
name: 'tooltip_columns',
|
||||
config: sharedControls.tooltip_columns,
|
||||
});
|
||||
|
||||
export const TooltipMetricsControl = (): CustomControlItem => ({
|
||||
name: 'tooltip_metrics',
|
||||
config: sharedControls.tooltip_metrics,
|
||||
});
|
||||
|
||||
// Format controls
|
||||
export const YAxisFormatControl = (): CustomControlItem => ({
|
||||
name: 'y_axis_format',
|
||||
config: sharedControls.y_axis_format,
|
||||
});
|
||||
|
||||
export const XAxisTimeFormatControl = (): CustomControlItem => ({
|
||||
name: 'x_axis_time_format',
|
||||
config: sharedControls.x_axis_time_format,
|
||||
});
|
||||
|
||||
// Hidden controls
|
||||
export const TemporalColumnsLookupControl = (): CustomControlItem => ({
|
||||
name: 'temporal_columns_lookup',
|
||||
config: sharedControls.temporal_columns_lookup,
|
||||
});
|
||||
|
||||
// Inline controls - for creating custom controls with overrides
|
||||
export const InlineTextControl = (
|
||||
name: string,
|
||||
overrides?: Partial<CustomControlItem['config']>,
|
||||
): CustomControlItem => ({
|
||||
name,
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
export const InlineSelectControl = (
|
||||
name: string,
|
||||
overrides?: Partial<CustomControlItem['config']>,
|
||||
): CustomControlItem => ({
|
||||
name,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
export const InlineCheckboxControl = (
|
||||
name: string,
|
||||
overrides?: Partial<CustomControlItem['config']>,
|
||||
): CustomControlItem => ({
|
||||
name,
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Switch, Select, Input } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
import FormatControlGroup from './FormatControlGroup';
|
||||
|
||||
export interface TableControlsSectionProps {
|
||||
variant?: 'table' | 'pivot' | 'ag-grid';
|
||||
showPagination?: boolean;
|
||||
showCellBars?: boolean;
|
||||
showTotals?: boolean;
|
||||
showConditionalFormatting?: boolean;
|
||||
showTimestampFormat?: boolean;
|
||||
showAllowHtml?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const TableControlsSection: FC<TableControlsSectionProps> = ({
|
||||
variant = 'table',
|
||||
showPagination = false,
|
||||
showCellBars = false,
|
||||
showTotals = false,
|
||||
showConditionalFormatting = true,
|
||||
showTimestampFormat = false,
|
||||
showAllowHtml = true,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const isPivot = variant === 'pivot';
|
||||
|
||||
return (
|
||||
<div className="table-controls-section">
|
||||
{/* Pagination Controls */}
|
||||
{showPagination && !isPivot && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.server_pagination || false}
|
||||
onChange={checked => onChange('server_pagination', checked)}
|
||||
/>
|
||||
{t('Server Pagination')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Enable server-side pagination for large datasets')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
{values.server_pagination && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Page Length')}</label>
|
||||
<Select
|
||||
value={values.server_page_length || 10}
|
||||
onChange={value => onChange('server_page_length', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 10, label: '10' },
|
||||
{ value: 25, label: '25' },
|
||||
{ value: 50, label: '50' },
|
||||
{ value: 100, label: '100' },
|
||||
{ value: 200, label: '200' },
|
||||
]}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Number of rows per page')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cell Bars */}
|
||||
{showCellBars && !isPivot && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_cell_bars || false}
|
||||
onChange={checked => onChange('show_cell_bars', checked)}
|
||||
/>
|
||||
{t('Show Cell Bars')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Display mini bar charts in numeric columns')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Totals */}
|
||||
{showTotals && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.show_totals || values.rowTotals || false}
|
||||
onChange={checked => {
|
||||
if (isPivot) {
|
||||
onChange('rowTotals', checked);
|
||||
onChange('colTotals', checked);
|
||||
} else {
|
||||
onChange('show_totals', checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{t('Show Totals')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{isPivot
|
||||
? t('Show row and column totals')
|
||||
: t('Show total row at bottom')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Subtotals for Pivot */}
|
||||
{isPivot && (
|
||||
<>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.rowSubTotals || false}
|
||||
onChange={checked => onChange('rowSubTotals', checked)}
|
||||
/>
|
||||
{t('Show Row Subtotals')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show subtotals for row groups')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
{values.rowSubTotals && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Subtotal Position')}</label>
|
||||
<Select
|
||||
value={values.rowSubtotalPosition || 'bottom'}
|
||||
onChange={value => onChange('rowSubtotalPosition', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'top', label: t('Top') },
|
||||
{ value: 'bottom', label: t('Bottom') },
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.colSubTotals || false}
|
||||
onChange={checked => onChange('colSubTotals', checked)}
|
||||
/>
|
||||
{t('Show Column Subtotals')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t('Show subtotals for column groups')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.transposePivot || false}
|
||||
onChange={checked => onChange('transposePivot', checked)}
|
||||
/>
|
||||
{t('Transpose Pivot')}
|
||||
</label>
|
||||
<small className="text-muted">{t('Swap rows and columns')}</small>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Conditional Formatting */}
|
||||
{showConditionalFormatting && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Conditional Formatting')}</label>
|
||||
<Input.TextArea
|
||||
value={values.conditional_formatting || ''}
|
||||
onChange={e => onChange('conditional_formatting', e.target.value)}
|
||||
placeholder={t('Enter conditional formatting rules as JSON')}
|
||||
rows={4}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Apply conditional color formatting to cells')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Timestamp Format */}
|
||||
{showTimestampFormat && !isPivot && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Timestamp Format')}</label>
|
||||
<Input
|
||||
value={values.table_timestamp_format || ''}
|
||||
onChange={e => onChange('table_timestamp_format', e.target.value)}
|
||||
placeholder="%Y-%m-%d %H:%M:%S"
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('D3 time format for timestamp columns')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Allow HTML */}
|
||||
{showAllowHtml && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Switch
|
||||
checked={values.allow_render_html || false}
|
||||
onChange={checked => onChange('allow_render_html', checked)}
|
||||
/>
|
||||
{t('Allow HTML')}
|
||||
</label>
|
||||
<small className="text-muted">
|
||||
{t(
|
||||
'Render HTML content in cells (security warning: only enable for trusted data)',
|
||||
)}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Format Controls */}
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<h4>{t('Value Formats')}</h4>
|
||||
<FormatControlGroup
|
||||
showNumber
|
||||
showCurrency
|
||||
showPercentage={isPivot}
|
||||
showDate={!isPivot}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableControlsSection;
|
||||
@@ -1,237 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FC } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { Select, Radio } from 'antd';
|
||||
import { Row, Col } from '@superset-ui/core/components';
|
||||
import AxisControlSection from './AxisControlSection';
|
||||
import FormatControlGroup from './FormatControlGroup';
|
||||
import OpacityControl from './OpacityControl';
|
||||
import MarkerControlGroup from './MarkerControlGroup';
|
||||
|
||||
export interface TimeseriesControlPanelProps {
|
||||
variant: 'area' | 'bar' | 'line' | 'scatter' | 'smooth' | 'step';
|
||||
showSeriesType?: boolean;
|
||||
showStack?: boolean;
|
||||
showArea?: boolean;
|
||||
showMarkers?: boolean;
|
||||
showOpacity?: boolean;
|
||||
showOrientation?: boolean;
|
||||
values?: Record<string, any>;
|
||||
onChange?: (name: string, value: any) => void;
|
||||
}
|
||||
|
||||
const SERIES_TYPE_OPTIONS: Record<string, Array<[string, string]>> = {
|
||||
line: [
|
||||
['line', t('Line')],
|
||||
['scatter', t('Scatter')],
|
||||
['smooth', t('Smooth')],
|
||||
],
|
||||
area: [
|
||||
['line', t('Line')],
|
||||
['smooth', t('Smooth Line')],
|
||||
['start', t('Step - start')],
|
||||
['middle', t('Step - middle')],
|
||||
['end', t('Step - end')],
|
||||
],
|
||||
step: [
|
||||
['start', t('Step - start')],
|
||||
['middle', t('Step - middle')],
|
||||
['end', t('Step - end')],
|
||||
],
|
||||
bar: [],
|
||||
scatter: [],
|
||||
smooth: [],
|
||||
};
|
||||
|
||||
const STACK_OPTIONS = [
|
||||
['stack', t('Stack')],
|
||||
['stream', t('Stream')],
|
||||
['expand', t('Expand')],
|
||||
];
|
||||
|
||||
const TimeseriesControlPanel: FC<TimeseriesControlPanelProps> = ({
|
||||
variant,
|
||||
showSeriesType = true,
|
||||
showStack = false,
|
||||
showArea = false,
|
||||
showMarkers = true,
|
||||
showOpacity = false,
|
||||
showOrientation = false,
|
||||
values = {},
|
||||
onChange = () => {},
|
||||
}) => {
|
||||
const hasAreaOptions = variant === 'area' || showArea;
|
||||
const hasBarOptions = variant === 'bar';
|
||||
const hasLineOptions = variant === 'line' || variant === 'smooth';
|
||||
|
||||
return (
|
||||
<div className="timeseries-control-panel">
|
||||
{/* Series Type Selection */}
|
||||
{showSeriesType && SERIES_TYPE_OPTIONS[variant] && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Series Style')}</label>
|
||||
<Select
|
||||
value={
|
||||
values.seriesType ||
|
||||
(SERIES_TYPE_OPTIONS[variant][0]
|
||||
? SERIES_TYPE_OPTIONS[variant][0][0]
|
||||
: 'line')
|
||||
}
|
||||
onChange={value => onChange('seriesType', value)}
|
||||
style={{ width: '100%' }}
|
||||
options={SERIES_TYPE_OPTIONS[variant].map(
|
||||
([value, label]: [string, string]) => ({
|
||||
value,
|
||||
label,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Series chart type (line, smooth, step, etc)')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Stack Options */}
|
||||
{showStack && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Stacking')}</label>
|
||||
<Select
|
||||
value={values.stack || null}
|
||||
onChange={value => onChange('stack', value)}
|
||||
style={{ width: '100%' }}
|
||||
allowClear
|
||||
placeholder={t('No stacking')}
|
||||
options={STACK_OPTIONS.map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<small className="text-muted">
|
||||
{t('Stack series on top of each other')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Bar Orientation */}
|
||||
{showOrientation && hasBarOptions && (
|
||||
<Row gutter={[16, 8]} style={{ marginBottom: 24 }}>
|
||||
<Col span={24}>
|
||||
<label>{t('Bar Orientation')}</label>
|
||||
<Radio.Group
|
||||
value={values.orientation || 'vertical'}
|
||||
onChange={e => onChange('orientation', e.target.value)}
|
||||
>
|
||||
<Radio value="vertical">{t('Vertical')}</Radio>
|
||||
<Radio value="horizontal">{t('Horizontal')}</Radio>
|
||||
</Radio.Group>
|
||||
<small
|
||||
className="text-muted"
|
||||
style={{ display: 'block', marginTop: 8 }}
|
||||
>
|
||||
{t('Orientation of bar chart')}
|
||||
</small>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* Area Chart Options */}
|
||||
{hasAreaOptions && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Area Chart')}</h4>
|
||||
<OpacityControl
|
||||
name="opacity"
|
||||
label={t('Area opacity')}
|
||||
description={t(
|
||||
'Opacity of area under the line. Set to 0 to disable area.',
|
||||
)}
|
||||
value={values.opacity || (variant === 'area' ? 0.2 : 0)}
|
||||
onChange={value => onChange('opacity', value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Line/Marker Options */}
|
||||
{showMarkers && (hasLineOptions || variant === 'area') && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Markers')}</h4>
|
||||
<MarkerControlGroup
|
||||
values={{
|
||||
markerEnabled: values.markerEnabled || false,
|
||||
markerSize: values.markerSize || 6,
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* X Axis Controls */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('X Axis')}</h4>
|
||||
<AxisControlSection
|
||||
axis="x"
|
||||
showTitle
|
||||
showFormat
|
||||
showRotation
|
||||
showBounds={hasBarOptions}
|
||||
showTruncate
|
||||
timeFormat
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Y Axis Controls */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Y Axis')}</h4>
|
||||
<AxisControlSection
|
||||
axis="y"
|
||||
showTitle
|
||||
showFormat
|
||||
showBounds
|
||||
showLogarithmic
|
||||
showMinorTicks
|
||||
showTruncate
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Value Formats */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h4>{t('Value Formats')}</h4>
|
||||
<FormatControlGroup
|
||||
showNumber
|
||||
showCurrency={hasBarOptions}
|
||||
showDate
|
||||
showPercentage={showStack}
|
||||
values={values}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeseriesControlPanel;
|
||||
@@ -16,42 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import RadioButtonControl from './RadioButtonControl';
|
||||
|
||||
export * from './RadioButtonControl';
|
||||
export { default as RadioButtonControl } from './RadioButtonControl';
|
||||
export { default as GranularityControl } from './GranularityControl';
|
||||
export * from './ReactControlPanel';
|
||||
export { default as AxisControlSection } from './AxisControlSection';
|
||||
export { default as FormatControlGroup } from './FormatControlGroup';
|
||||
export { default as OpacityControl } from './OpacityControl';
|
||||
export { default as MarkerControlGroup } from './MarkerControlGroup';
|
||||
export { default as TimeseriesControlPanel } from './TimeseriesControlPanel';
|
||||
export { default as LabelControlGroup } from './LabelControlGroup';
|
||||
export { default as PieShapeControl } from './PieShapeControl';
|
||||
export { default as TableControlsSection } from './TableControlsSection';
|
||||
export { default as FilterControlsSection } from './FilterControlsSection';
|
||||
export { default as DeckGLControlsSection } from './DeckGLControlsSection';
|
||||
// Export ControlComponents with specific names
|
||||
// ColorPickerControl from ControlComponents takes props, renamed to avoid conflict
|
||||
export {
|
||||
CheckboxControl,
|
||||
NumberControl,
|
||||
SelectControl,
|
||||
SliderControl,
|
||||
SwitchControl,
|
||||
TextAreaControl,
|
||||
TextControl,
|
||||
ColorPickerControl as ColorPickerControlWithProps,
|
||||
type ControlComponentConfig,
|
||||
} from './ControlComponents';
|
||||
|
||||
// Export all SharedControlComponents which replace string references
|
||||
// ColorPickerControl from here does NOT take props - it's for the shared 'color_picker' control
|
||||
export * from './SharedControlComponents';
|
||||
|
||||
// Export React control panel
|
||||
export { ReactControlPanel } from './ReactControlPanel';
|
||||
|
||||
// Export control panel layout components
|
||||
export * from './ControlPanelLayout';
|
||||
|
||||
// Inline control functions are exported from SharedControlComponents
|
||||
/**
|
||||
* Shared chart controls. Can be referred via string shortcuts in chart control
|
||||
* configs.
|
||||
*/
|
||||
export default {
|
||||
RadioButtonControl,
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
export { default as sharedControls } from './sharedControls';
|
||||
// sharedControlComponents is deprecated - import components directly instead
|
||||
// export { default as sharedControlComponents } from './components';
|
||||
// React control components
|
||||
export { default as sharedControlComponents } from './components';
|
||||
export { aggregationControl } from './customControls';
|
||||
export * from './components';
|
||||
export * from './customControls';
|
||||
|
||||
@@ -177,7 +177,6 @@ const granularity: SharedControlConfig<'SelectControl'> = {
|
||||
'can type and use simple natural language as in `10 seconds`, ' +
|
||||
'`1 day` or `56 weeks`',
|
||||
),
|
||||
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
|
||||
};
|
||||
|
||||
const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
|
||||
@@ -205,7 +204,6 @@ const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
|
||||
choices: (datasource as Dataset)?.time_grain_sqla || [],
|
||||
}),
|
||||
visibility: displayTimeRelatedControls,
|
||||
sortComparator: () => 0, // Disable frontend sorting to preserve backend order
|
||||
};
|
||||
|
||||
const time_range: SharedControlConfig<'DateFilterControl'> = {
|
||||
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
QueryFormMetric,
|
||||
QueryResponse,
|
||||
} from '@superset-ui/core';
|
||||
import { sharedControls } from './shared-controls';
|
||||
import { sharedControls, sharedControlComponents } from './shared-controls';
|
||||
|
||||
export type { Metric } from '@superset-ui/core';
|
||||
export type { ControlComponentProps } from './shared-controls/components/types';
|
||||
@@ -45,6 +45,8 @@ interface Action {
|
||||
interface AnyAction extends Action, AnyDict {}
|
||||
|
||||
export type SharedControls = typeof sharedControls;
|
||||
export type SharedControlAlias = keyof typeof sharedControls;
|
||||
export type SharedControlComponents = typeof sharedControlComponents;
|
||||
|
||||
/** ----------------------------------------------
|
||||
* Input data/props while rendering
|
||||
@@ -183,7 +185,7 @@ export type InternalControlType =
|
||||
| 'Select'
|
||||
| 'Slider'
|
||||
| 'Input'
|
||||
| 'RadioButtonControl';
|
||||
| keyof SharedControlComponents; // expanded in `expandControlConfig`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ControlType = InternalControlType | ComponentType<any>;
|
||||
@@ -357,7 +359,7 @@ export type SharedSectionAlias =
|
||||
| 'NVD3TimeSeries';
|
||||
|
||||
export interface OverrideSharedControlItem<
|
||||
A extends keyof SharedControls = keyof SharedControls,
|
||||
A extends SharedControlAlias = SharedControlAlias,
|
||||
> {
|
||||
name: A;
|
||||
override: Partial<SharedControls[A]>;
|
||||
@@ -380,16 +382,16 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
|
||||
// interfere with other ControlSetItem types
|
||||
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
|
||||
|
||||
// All controls must be React components or control configuration objects
|
||||
export type ControlSetItem = OverrideSharedControlItem | ExpandedControlItem;
|
||||
export type ControlSetItem =
|
||||
| SharedControlAlias
|
||||
| OverrideSharedControlItem
|
||||
| ExpandedControlItem;
|
||||
|
||||
export type ControlSetRow = ControlSetItem[];
|
||||
|
||||
// Ref:
|
||||
// - superset-frontend/src/explore/components/ControlPanelsContainer.jsx
|
||||
// - superset-frontend/src/explore/components/ControlPanelSection.jsx
|
||||
// DEPRECATED: Legacy control panel types - use JsonFormsControlPanelConfig instead
|
||||
// These are kept temporarily for backward compatibility during migration
|
||||
export interface ControlPanelSectionConfig {
|
||||
label?: ReactNode;
|
||||
description?: ReactNode;
|
||||
@@ -426,7 +428,6 @@ export const isStandardizedFormData = (
|
||||
Array.isArray(formData.standardizedFormData.controls.metrics) &&
|
||||
Array.isArray(formData.standardizedFormData.controls.columns);
|
||||
|
||||
// DEPRECATED: Use JsonFormsControlPanelConfig from './types/jsonForms' instead
|
||||
export interface ControlPanelConfig {
|
||||
controlPanelSections: (ControlPanelSectionConfig | null)[];
|
||||
controlOverrides?: ControlOverrides;
|
||||
@@ -436,7 +437,7 @@ export interface ControlPanelConfig {
|
||||
}
|
||||
|
||||
export type ControlOverrides = {
|
||||
[P in keyof SharedControls]?: Partial<SharedControls[P]>;
|
||||
[P in SharedControlAlias]?: Partial<SharedControls[P]>;
|
||||
};
|
||||
|
||||
export type SectionOverrides = {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Type exports placeholder
|
||||
@@ -17,15 +17,28 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { isValidElement, ReactElement } from 'react';
|
||||
import { sharedControls } from '../shared-controls';
|
||||
import { sharedControls, sharedControlComponents } from '../shared-controls';
|
||||
import {
|
||||
ControlType,
|
||||
ControlSetItem,
|
||||
ExpandedControlItem,
|
||||
ControlOverrides,
|
||||
} from '../types';
|
||||
|
||||
export function expandControlType(controlType: ControlType) {
|
||||
if (
|
||||
typeof controlType === 'string' &&
|
||||
controlType in sharedControlComponents
|
||||
) {
|
||||
return sharedControlComponents[
|
||||
controlType as keyof typeof sharedControlComponents
|
||||
];
|
||||
}
|
||||
return controlType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a control config item to full config in the format of
|
||||
* Expand a shorthand control config item to full config in the format of
|
||||
* {
|
||||
* name: ...,
|
||||
* config: {
|
||||
@@ -33,31 +46,26 @@ import {
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Note: String references to shared controls are no longer supported.
|
||||
* All controls must be React components or control configuration objects.
|
||||
*/
|
||||
export function expandControlConfig(
|
||||
control: ControlSetItem,
|
||||
controlOverrides: ControlOverrides = {},
|
||||
): ExpandedControlItem {
|
||||
// one of the named shared controls
|
||||
if (typeof control === 'string' && control in sharedControls) {
|
||||
const name = control;
|
||||
return {
|
||||
name,
|
||||
config: {
|
||||
...sharedControls[name],
|
||||
...controlOverrides[name],
|
||||
},
|
||||
};
|
||||
}
|
||||
// JSX/React element or NULL
|
||||
if (!control || isValidElement(control)) {
|
||||
if (!control || typeof control === 'string' || isValidElement(control)) {
|
||||
return control as ReactElement;
|
||||
}
|
||||
// Check if it's a modern panel component (function with isModernPanel flag)
|
||||
if (typeof control === 'function' && (control as any).isModernPanel) {
|
||||
console.log('expandControlConfig - Found modern panel, returning as-is');
|
||||
return control as any;
|
||||
}
|
||||
// String controls are no longer supported - they must be migrated to React components
|
||||
if (typeof control === 'string') {
|
||||
throw new Error(
|
||||
`String control reference "${control}" is not supported. ` +
|
||||
`Use the corresponding React component from @superset-ui/chart-controls instead. ` +
|
||||
`For example, replace ['metrics'] with [MetricsControl()].`,
|
||||
);
|
||||
}
|
||||
// already fully expanded control config, e.g.
|
||||
// {
|
||||
// name: 'metric',
|
||||
@@ -66,7 +74,13 @@ export function expandControlConfig(
|
||||
// }
|
||||
// }
|
||||
if ('name' in control && 'config' in control) {
|
||||
return control;
|
||||
return {
|
||||
...control,
|
||||
config: {
|
||||
...control.config,
|
||||
type: expandControlType(control.config.type as ControlType),
|
||||
},
|
||||
};
|
||||
}
|
||||
// apply overrides with shared controls
|
||||
if ('override' in control && control.name in sharedControls) {
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
MetricsControl,
|
||||
MetricControl,
|
||||
GroupByControl,
|
||||
ColumnsControl,
|
||||
AdhocFiltersControl,
|
||||
LimitControl,
|
||||
RowLimitControl,
|
||||
OrderByControl,
|
||||
OrderDescControl,
|
||||
SeriesControl,
|
||||
EntityControl,
|
||||
XControl,
|
||||
YControl,
|
||||
SizeControl,
|
||||
ColorSchemeControl,
|
||||
LinearColorSchemeControl,
|
||||
ColorPickerControl,
|
||||
TimeRangeControl,
|
||||
GranularitySqlaControl,
|
||||
TimeGrainSqlaControl,
|
||||
DatasourceControl,
|
||||
VizTypeControl,
|
||||
XAxisControl,
|
||||
YAxisFormatControl,
|
||||
XAxisTimeFormatControl,
|
||||
ZoomableControl,
|
||||
SortByMetricControl,
|
||||
CurrencyFormatControl,
|
||||
TooltipColumnsControl,
|
||||
TooltipMetricsControl,
|
||||
sharedControls,
|
||||
} from '../../src';
|
||||
|
||||
describe('SharedControlComponents', () => {
|
||||
describe('React Component Controls', () => {
|
||||
it('should return proper control items for metrics controls', () => {
|
||||
const metricsControl = MetricsControl();
|
||||
expect(metricsControl).toEqual({
|
||||
name: 'metrics',
|
||||
config: sharedControls.metrics,
|
||||
});
|
||||
|
||||
const metricControl = MetricControl();
|
||||
expect(metricControl).toEqual({
|
||||
name: 'metric',
|
||||
config: sharedControls.metric,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for dimension controls', () => {
|
||||
const groupByControl = GroupByControl();
|
||||
expect(groupByControl).toEqual({
|
||||
name: 'groupby',
|
||||
config: sharedControls.groupby,
|
||||
});
|
||||
|
||||
const columnsControl = ColumnsControl();
|
||||
expect(columnsControl).toEqual({
|
||||
name: 'columns',
|
||||
config: sharedControls.columns,
|
||||
});
|
||||
|
||||
const seriesControl = SeriesControl();
|
||||
expect(seriesControl).toEqual({
|
||||
name: 'series',
|
||||
config: sharedControls.series,
|
||||
});
|
||||
|
||||
const entityControl = EntityControl();
|
||||
expect(entityControl).toEqual({
|
||||
name: 'entity',
|
||||
config: sharedControls.entity,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for filter controls', () => {
|
||||
const adhocFiltersControl = AdhocFiltersControl();
|
||||
expect(adhocFiltersControl).toEqual({
|
||||
name: 'adhoc_filters',
|
||||
config: sharedControls.adhoc_filters,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for limit controls', () => {
|
||||
const limitControl = LimitControl();
|
||||
expect(limitControl).toEqual({
|
||||
name: 'limit',
|
||||
config: sharedControls.limit,
|
||||
});
|
||||
|
||||
const rowLimitControl = RowLimitControl();
|
||||
expect(rowLimitControl).toEqual({
|
||||
name: 'row_limit',
|
||||
config: sharedControls.row_limit,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for sort controls', () => {
|
||||
const orderByControl = OrderByControl();
|
||||
expect(orderByControl).toEqual({
|
||||
name: 'orderby',
|
||||
config: sharedControls.orderby,
|
||||
});
|
||||
|
||||
const orderDescControl = OrderDescControl();
|
||||
expect(orderDescControl).toEqual({
|
||||
name: 'order_desc',
|
||||
config: sharedControls.order_desc,
|
||||
});
|
||||
|
||||
const sortByMetricControl = SortByMetricControl();
|
||||
expect(sortByMetricControl).toEqual({
|
||||
name: 'sort_by_metric',
|
||||
config: sharedControls.sort_by_metric,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for axis controls', () => {
|
||||
const xControl = XControl();
|
||||
expect(xControl).toEqual({
|
||||
name: 'x',
|
||||
config: sharedControls.x,
|
||||
});
|
||||
|
||||
const yControl = YControl();
|
||||
expect(yControl).toEqual({
|
||||
name: 'y',
|
||||
config: sharedControls.y,
|
||||
});
|
||||
|
||||
const xAxisControl = XAxisControl();
|
||||
expect(xAxisControl).toEqual({
|
||||
name: 'x_axis',
|
||||
config: sharedControls.x_axis,
|
||||
});
|
||||
|
||||
// Note: YAxisControl doesn't exist, YControl is reused for y axis
|
||||
const yControl2 = YControl();
|
||||
expect(yControl2).toEqual({
|
||||
name: 'y',
|
||||
config: sharedControls.y,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for formatting controls', () => {
|
||||
const yAxisFormatControl = YAxisFormatControl();
|
||||
expect(yAxisFormatControl).toEqual({
|
||||
name: 'y_axis_format',
|
||||
config: sharedControls.y_axis_format,
|
||||
});
|
||||
|
||||
const xAxisTimeFormatControl = XAxisTimeFormatControl();
|
||||
expect(xAxisTimeFormatControl).toEqual({
|
||||
name: 'x_axis_time_format',
|
||||
config: sharedControls.x_axis_time_format,
|
||||
});
|
||||
|
||||
const currencyFormatControl = CurrencyFormatControl();
|
||||
expect(currencyFormatControl).toEqual({
|
||||
name: 'currency_format',
|
||||
config: sharedControls.currency_format,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for color controls', () => {
|
||||
const colorSchemeControl = ColorSchemeControl();
|
||||
expect(colorSchemeControl).toEqual({
|
||||
name: 'color_scheme',
|
||||
config: sharedControls.color_scheme,
|
||||
});
|
||||
|
||||
const linearColorSchemeControl = LinearColorSchemeControl();
|
||||
expect(linearColorSchemeControl).toEqual({
|
||||
name: 'linear_color_scheme',
|
||||
config: sharedControls.linear_color_scheme,
|
||||
});
|
||||
|
||||
const colorPickerControl = ColorPickerControl();
|
||||
expect(colorPickerControl).toEqual({
|
||||
name: 'color_picker',
|
||||
config: sharedControls.color_picker,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for time controls', () => {
|
||||
const timeRangeControl = TimeRangeControl();
|
||||
expect(timeRangeControl).toEqual({
|
||||
name: 'time_range',
|
||||
config: sharedControls.time_range,
|
||||
});
|
||||
|
||||
const granularitySqlaControl = GranularitySqlaControl();
|
||||
expect(granularitySqlaControl).toEqual({
|
||||
name: 'granularity_sqla',
|
||||
config: sharedControls.granularity_sqla,
|
||||
});
|
||||
|
||||
const timeGrainSqlaControl = TimeGrainSqlaControl();
|
||||
expect(timeGrainSqlaControl).toEqual({
|
||||
name: 'time_grain_sqla',
|
||||
config: sharedControls.time_grain_sqla,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for datasource controls', () => {
|
||||
const datasourceControl = DatasourceControl();
|
||||
expect(datasourceControl).toEqual({
|
||||
name: 'datasource',
|
||||
config: sharedControls.datasource,
|
||||
});
|
||||
|
||||
const vizTypeControl = VizTypeControl();
|
||||
expect(vizTypeControl).toEqual({
|
||||
name: 'viz_type',
|
||||
config: sharedControls.viz_type,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for tooltip controls', () => {
|
||||
const tooltipColumnsControl = TooltipColumnsControl();
|
||||
expect(tooltipColumnsControl).toEqual({
|
||||
name: 'tooltip_columns',
|
||||
config: sharedControls.tooltip_columns,
|
||||
});
|
||||
|
||||
const tooltipMetricsControl = TooltipMetricsControl();
|
||||
expect(tooltipMetricsControl).toEqual({
|
||||
name: 'tooltip_metrics',
|
||||
config: sharedControls.tooltip_metrics,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return proper control items for other controls', () => {
|
||||
const sizeControl = SizeControl();
|
||||
expect(sizeControl).toEqual({
|
||||
name: 'size',
|
||||
config: sharedControls.size,
|
||||
});
|
||||
|
||||
const zoomableControl = ZoomableControl();
|
||||
expect(zoomableControl).toEqual({
|
||||
name: 'zoomable',
|
||||
config: sharedControls.zoomable,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Control compatibility', () => {
|
||||
it('should be usable in control panel configurations', () => {
|
||||
// Simulate a control panel configuration
|
||||
const controlPanel = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: 'Query',
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[MetricsControl()],
|
||||
[GroupByControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[LimitControl(), OrderDescControl()],
|
||||
[RowLimitControl()],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Chart Options',
|
||||
expanded: true,
|
||||
controlSetRows: [[ColorSchemeControl()], [YAxisFormatControl()]],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Verify structure
|
||||
expect(controlPanel.controlPanelSections).toHaveLength(2);
|
||||
|
||||
// Verify first section
|
||||
const querySection = controlPanel.controlPanelSections[0];
|
||||
expect(querySection.controlSetRows[0][0]).toHaveProperty(
|
||||
'name',
|
||||
'metrics',
|
||||
);
|
||||
expect(querySection.controlSetRows[1][0]).toHaveProperty(
|
||||
'name',
|
||||
'groupby',
|
||||
);
|
||||
expect(querySection.controlSetRows[2][0]).toHaveProperty(
|
||||
'name',
|
||||
'adhoc_filters',
|
||||
);
|
||||
expect(querySection.controlSetRows[3][0]).toHaveProperty('name', 'limit');
|
||||
expect(querySection.controlSetRows[3][1]).toHaveProperty(
|
||||
'name',
|
||||
'order_desc',
|
||||
);
|
||||
expect(querySection.controlSetRows[4][0]).toHaveProperty(
|
||||
'name',
|
||||
'row_limit',
|
||||
);
|
||||
|
||||
// Verify second section
|
||||
const optionsSection = controlPanel.controlPanelSections[1];
|
||||
expect(optionsSection.controlSetRows[0][0]).toHaveProperty(
|
||||
'name',
|
||||
'color_scheme',
|
||||
);
|
||||
expect(optionsSection.controlSetRows[1][0]).toHaveProperty(
|
||||
'name',
|
||||
'y_axis_format',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,19 +20,15 @@ import {
|
||||
expandControlConfig,
|
||||
sharedControls,
|
||||
CustomControlItem,
|
||||
sharedControlComponents,
|
||||
} from '../../src';
|
||||
|
||||
describe('expandControlConfig()', () => {
|
||||
it('throws error when string control is passed', () => {
|
||||
expect(() => expandControlConfig('metrics' as any)).toThrow(
|
||||
'String control reference "metrics" is not supported',
|
||||
);
|
||||
expect(() => expandControlConfig('groupby' as any)).toThrow(
|
||||
'String control reference "groupby" is not supported',
|
||||
);
|
||||
expect(() => expandControlConfig('columns' as any)).toThrow(
|
||||
'String control reference "columns" is not supported',
|
||||
);
|
||||
it('expands shared control alias', () => {
|
||||
expect(expandControlConfig('metrics')).toEqual({
|
||||
name: 'metrics',
|
||||
config: sharedControls.metrics,
|
||||
});
|
||||
});
|
||||
|
||||
it('expands control with overrides', () => {
|
||||
@@ -73,7 +69,7 @@ describe('expandControlConfig()', () => {
|
||||
};
|
||||
expect(
|
||||
(expandControlConfig(input) as CustomControlItem).config.type,
|
||||
).toEqual('RadioButtonControl');
|
||||
).toEqual(sharedControlComponents.RadioButtonControl);
|
||||
});
|
||||
|
||||
it('leave NULL and ReactElement untouched', () => {
|
||||
@@ -82,6 +78,11 @@ describe('expandControlConfig()', () => {
|
||||
expect(expandControlConfig(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('leave unknown text untouched', () => {
|
||||
const input = 'superset-ui';
|
||||
expect(expandControlConfig(input as never)).toBe(input);
|
||||
});
|
||||
|
||||
it('return null for invalid configs', () => {
|
||||
expect(
|
||||
expandControlConfig({ type: 'SelectControl', label: 'Hello' } as never),
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"d3-format": "^1.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale": "^3.0.0",
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
@@ -59,7 +59,7 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^5.1.1",
|
||||
"reselect": "^4.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"@visx/responsive": "^3.12.0",
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/** Type checking is disabled for this file due to reselect only supporting
|
||||
* TS declarations for selectors with up to 12 arguments. */
|
||||
// @ts-nocheck
|
||||
import { RefObject } from 'react';
|
||||
import { createSelector, lruMemoize } from 'reselect';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
AppSection,
|
||||
Behavior,
|
||||
@@ -34,7 +37,7 @@ import {
|
||||
SetDataMaskHook,
|
||||
} from '../types/Base';
|
||||
import { QueryData, DataRecordFilters } from '..';
|
||||
import { supersetTheme, SupersetTheme } from '../../theme';
|
||||
import { SupersetTheme } from '../../theme';
|
||||
|
||||
// TODO: more specific typing for these fields of ChartProps
|
||||
type AnnotationData = PlainObject;
|
||||
@@ -106,8 +109,6 @@ export interface ChartPropsConfig {
|
||||
theme: SupersetTheme;
|
||||
/* legend index */
|
||||
legendIndex?: number;
|
||||
inContextMenu?: boolean;
|
||||
emitCrossFilters?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_WIDTH = 800;
|
||||
@@ -160,11 +161,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
||||
|
||||
theme: SupersetTheme;
|
||||
|
||||
constructor(
|
||||
config: ChartPropsConfig & { formData?: FormData } = {
|
||||
theme: supersetTheme,
|
||||
},
|
||||
) {
|
||||
constructor(config: ChartPropsConfig & { formData?: FormData } = {}) {
|
||||
const {
|
||||
annotationData = {},
|
||||
datasource = {},
|
||||
@@ -279,16 +276,5 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
|
||||
emitCrossFilters,
|
||||
theme,
|
||||
}),
|
||||
// Below config is to retain usage of 1-sized `lruMemoize` object in Reselect v4
|
||||
// Reselect v5 introduces `weakMapMemoize` which is more performant but potentially memory-leaky
|
||||
// due to infinite cache size.
|
||||
// Source: https://github.com/reduxjs/reselect/releases/tag/v5.0.1
|
||||
{
|
||||
memoize: lruMemoize,
|
||||
argsMemoize: lruMemoize,
|
||||
memoizeOptions: {
|
||||
maxSize: 10,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { DrawerProps } from './types';
|
||||
|
||||
export { Drawer } from 'antd';
|
||||
export type { DrawerProps };
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { DrawerProps } from 'antd/es/drawer';
|
||||
|
||||
export { DrawerProps };
|
||||
@@ -132,7 +132,7 @@ const VirtualTable = <RecordType extends object>(
|
||||
if (gridRef.current) {
|
||||
return gridRef.current?.state?.scrollLeft;
|
||||
}
|
||||
return 0;
|
||||
return null;
|
||||
},
|
||||
set: (scrollLeft: number) => {
|
||||
if (gridRef.current) {
|
||||
|
||||
@@ -76,7 +76,6 @@ export { CronPicker, type CronError } from './CronPicker';
|
||||
export * from './DatePicker';
|
||||
export { DeleteModal, type DeleteModalProps } from './DeleteModal';
|
||||
export { Divider, type DividerProps } from './Divider';
|
||||
export { Drawer, type DrawerProps } from './Drawer';
|
||||
export {
|
||||
Dropdown,
|
||||
MenuDotsDropdown,
|
||||
|
||||
@@ -49,21 +49,6 @@ describe('isProbablyHTML', () => {
|
||||
const trickyText = 'a <= 10 and b > 10';
|
||||
expect(isProbablyHTML(trickyText)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for strings with angle brackets that are not HTML', () => {
|
||||
// Test case from issue #25561
|
||||
expect(isProbablyHTML('<abcdef:12345>')).toBe(false);
|
||||
|
||||
// Other similar cases
|
||||
expect(isProbablyHTML('<foo:bar>')).toBe(false);
|
||||
expect(isProbablyHTML('<123>')).toBe(false);
|
||||
expect(isProbablyHTML('<test@example.com>')).toBe(false);
|
||||
expect(isProbablyHTML('<custom-element>')).toBe(false);
|
||||
|
||||
// Mathematical expressions
|
||||
expect(isProbablyHTML('if x < 5 and y > 10')).toBe(false);
|
||||
expect(isProbablyHTML('price < $100')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeHtmlIfNeeded', () => {
|
||||
|
||||
@@ -68,87 +68,9 @@ export function isProbablyHTML(text: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the string contains common HTML patterns
|
||||
if (!hasHtmlTagPattern(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(cleanedStr, 'text/html');
|
||||
|
||||
// Check if parsing created actual HTML elements (not just text nodes)
|
||||
const elements = Array.from(doc.body.childNodes).filter(
|
||||
node => node.nodeType === 1,
|
||||
) as Element[];
|
||||
|
||||
// If no elements were created, it's not HTML
|
||||
if (elements.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the elements are known HTML tags (not custom/unknown tags)
|
||||
// This prevents strings like "<abcdef:12345>" from being treated as HTML
|
||||
return elements.some(element => {
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
// List of common HTML tags we want to recognize
|
||||
const knownHtmlTags = [
|
||||
'div',
|
||||
'span',
|
||||
'p',
|
||||
'a',
|
||||
'b',
|
||||
'i',
|
||||
'u',
|
||||
'em',
|
||||
'strong',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'th',
|
||||
'tbody',
|
||||
'thead',
|
||||
'tfoot',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'img',
|
||||
'br',
|
||||
'hr',
|
||||
'pre',
|
||||
'code',
|
||||
'blockquote',
|
||||
'section',
|
||||
'article',
|
||||
'nav',
|
||||
'header',
|
||||
'footer',
|
||||
'form',
|
||||
'input',
|
||||
'button',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'label',
|
||||
'fieldset',
|
||||
'legend',
|
||||
'video',
|
||||
'audio',
|
||||
'canvas',
|
||||
'iframe',
|
||||
'script',
|
||||
'style',
|
||||
'link',
|
||||
'meta',
|
||||
'title',
|
||||
];
|
||||
return knownHtmlTags.includes(tagName);
|
||||
});
|
||||
return Array.from(doc.body.childNodes).some(({ nodeType }) => nodeType === 1);
|
||||
}
|
||||
|
||||
export function sanitizeHtmlIfNeeded(htmlString: string) {
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('ChartProps', () => {
|
||||
});
|
||||
expect(props1).not.toBe(props2);
|
||||
});
|
||||
it('selector returns a new chartProps if some input fields change and returns memoized chart props', () => {
|
||||
it('selector returns a new chartProps if some input fields change', () => {
|
||||
const props1 = selector({
|
||||
width: 800,
|
||||
height: 600,
|
||||
@@ -145,7 +145,7 @@ describe('ChartProps', () => {
|
||||
theme: supersetTheme,
|
||||
});
|
||||
expect(props1).not.toBe(props2);
|
||||
expect(props1).toBe(props3);
|
||||
expect(props1).not.toBe(props3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"@storybook/types": "8.4.7",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"core-js": "3.40.0",
|
||||
"gh-pages": "^6.3.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"memoize-one": "^5.2.1",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -22,15 +22,6 @@ import {
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
AdhocFiltersControl,
|
||||
GranularitySqlaControl,
|
||||
LinearColorSchemeControl,
|
||||
MetricsControl,
|
||||
TimeRangeControl,
|
||||
YAxisFormatControl,
|
||||
InlineSelectControl,
|
||||
InlineTextControl,
|
||||
InlineCheckboxControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -39,43 +30,51 @@ const config: ControlPanelConfig = {
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [[GranularitySqlaControl()], [TimeRangeControl()]],
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
InlineSelectControl('domain_granularity', {
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
}),
|
||||
InlineSelectControl('subdomain_granularity', {
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'domain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Domain'),
|
||||
default: 'month',
|
||||
choices: [
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
['year', t('year')],
|
||||
],
|
||||
description: t('The time unit used for the grouping of blocks'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'subdomain_granularity',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Subdomain'),
|
||||
default: 'day',
|
||||
choices: [
|
||||
['min', t('min')],
|
||||
['hour', t('hour')],
|
||||
['day', t('day')],
|
||||
['week', t('week')],
|
||||
['month', t('month')],
|
||||
],
|
||||
description: t(
|
||||
'The time unit for each block. Should be a smaller unit than ' +
|
||||
'domain_granularity. Should be larger or equal to Time Grain',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[MetricsControl()],
|
||||
[AdhocFiltersControl()],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -83,77 +82,109 @@ const config: ControlPanelConfig = {
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[LinearColorSchemeControl()],
|
||||
['linear_color_scheme'],
|
||||
[
|
||||
InlineTextControl('cell_size', {
|
||||
label: t('Cell Size'),
|
||||
default: 10,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
}),
|
||||
InlineTextControl('cell_padding', {
|
||||
label: t('Cell Padding'),
|
||||
default: 2,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The distance between cells, in pixels'),
|
||||
}),
|
||||
{
|
||||
name: 'cell_size',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
default: 10,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
label: t('Cell Size'),
|
||||
description: t('The size of the square cell, in pixels'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cell_padding',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
label: t('Cell Padding'),
|
||||
description: t('The distance between cells, in pixels'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
InlineTextControl('cell_radius', {
|
||||
label: t('Cell Radius'),
|
||||
default: 0,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The pixel radius'),
|
||||
}),
|
||||
InlineTextControl('steps', {
|
||||
label: t('Color Steps'),
|
||||
default: 10,
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
description: t('The number color "steps"'),
|
||||
}),
|
||||
{
|
||||
name: 'cell_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 0,
|
||||
label: t('Cell Radius'),
|
||||
description: t('The pixel radius'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'steps',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
isInt: true,
|
||||
validators: [legacyValidateInteger],
|
||||
renderTrigger: true,
|
||||
default: 10,
|
||||
label: t('Color Steps'),
|
||||
description: t('The number color "steps"'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
YAxisFormatControl(),
|
||||
InlineSelectControl('x_axis_time_format', {
|
||||
label: t('Time Format'),
|
||||
default: 'smart_date',
|
||||
freeForm: true,
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
}),
|
||||
'y_axis_format',
|
||||
{
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
InlineCheckboxControl('show_legend', {
|
||||
label: t('Legend'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
}),
|
||||
InlineCheckboxControl('show_values', {
|
||||
label: t('Show Values'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'show_legend',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Legend'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the legend (toggles)'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'show_values',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Values'),
|
||||
renderTrigger: true,
|
||||
default: false,
|
||||
description: t(
|
||||
'Whether to display the numerical values within the cells',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
InlineCheckboxControl('show_metric_name', {
|
||||
label: t('Show Metric Names'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
}),
|
||||
{
|
||||
name: 'show_metric_name',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Metric Names'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Whether to display the metric name as a title'),
|
||||
},
|
||||
},
|
||||
null,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -20,14 +20,6 @@ import { ensureIsArray, t, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
GroupByControl,
|
||||
ColumnsControl,
|
||||
MetricControl,
|
||||
AdhocFiltersControl,
|
||||
RowLimitControl,
|
||||
SortByMetricControl,
|
||||
YAxisFormatControl,
|
||||
ColorSchemeControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -36,19 +28,18 @@ const config: ControlPanelConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[GroupByControl()],
|
||||
[ColumnsControl()],
|
||||
[MetricControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[RowLimitControl()],
|
||||
[SortByMetricControl()],
|
||||
['groupby'],
|
||||
['columns'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
['row_limit'],
|
||||
['sort_by_metric'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [[YAxisFormatControl()], [ColorSchemeControl()]],
|
||||
controlSetRows: [['y_axis_format', null], ['color_scheme']],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
|
||||
@@ -22,11 +22,6 @@ import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
getStandardizedControls,
|
||||
AdhocFiltersControl,
|
||||
EntityControl,
|
||||
LinearColorSchemeControl,
|
||||
MetricControl,
|
||||
InlineSelectControl as SelectControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { countryOptions } from './countries';
|
||||
|
||||
@@ -37,17 +32,21 @@ const config: ControlPanelConfig = {
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
SelectControl('select_country', {
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
}),
|
||||
{
|
||||
name: 'select_country',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Country'),
|
||||
default: null,
|
||||
choices: countryOptions,
|
||||
description: t('Which country to plot the map for?'),
|
||||
validators: [validateNonEmpty],
|
||||
},
|
||||
},
|
||||
],
|
||||
[EntityControl()],
|
||||
[MetricControl()],
|
||||
[AdhocFiltersControl()],
|
||||
['entity'],
|
||||
['metric'],
|
||||
['adhoc_filters'],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -56,16 +55,20 @@ const config: ControlPanelConfig = {
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[
|
||||
SelectControl('number_format', {
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
}),
|
||||
{
|
||||
name: 'number_format',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Number format'),
|
||||
renderTrigger: true,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
],
|
||||
[LinearColorSchemeControl()],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,17 +20,6 @@ import { t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
AdhocFiltersControl,
|
||||
GranularitySqlaControl,
|
||||
GroupByControl,
|
||||
LimitControl,
|
||||
MetricsControl,
|
||||
OrderDescControl,
|
||||
RowLimitControl,
|
||||
TimeLimitMetricControl,
|
||||
TimeRangeControl,
|
||||
InlineCheckboxControl as CheckboxControl,
|
||||
InlineSelectControl as SelectControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -39,25 +28,29 @@ const config: ControlPanelConfig = {
|
||||
label: t('Time'),
|
||||
expanded: true,
|
||||
description: t('Time related form attributes'),
|
||||
controlSetRows: [[GranularitySqlaControl()], [TimeRangeControl()]],
|
||||
controlSetRows: [['granularity_sqla'], ['time_range']],
|
||||
},
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[MetricsControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[GroupByControl()],
|
||||
[LimitControl(), TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
['groupby'],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
CheckboxControl('contribution', {
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
}),
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[RowLimitControl(), null],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -65,36 +58,44 @@ const config: ControlPanelConfig = {
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
SelectControl('series_height', {
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
}),
|
||||
SelectControl('horizon_color_scale', {
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'series_height',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
freeForm: true,
|
||||
label: t('Series Height'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'10',
|
||||
'25',
|
||||
'40',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
]),
|
||||
description: t('Pixel height of each series'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'horizon_color_scale',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
renderTrigger: true,
|
||||
label: t('Value Domain'),
|
||||
choices: [
|
||||
['series', t('series')],
|
||||
['overall', t('overall')],
|
||||
['change', t('change')],
|
||||
],
|
||||
default: 'series',
|
||||
description: t(
|
||||
'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -23,9 +23,6 @@ import {
|
||||
formatSelectOptions,
|
||||
sharedControls,
|
||||
getStandardizedControls,
|
||||
AdhocFiltersControl,
|
||||
GroupByControl,
|
||||
RowLimitControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const columnsConfig = sharedControls.entity;
|
||||
@@ -92,9 +89,9 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[RowLimitControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[GroupByControl()],
|
||||
['row_limit'],
|
||||
['adhoc_filters'],
|
||||
['groupby'],
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,17 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
AdhocFiltersControl,
|
||||
LimitControl,
|
||||
MetricsControl,
|
||||
OrderDescControl,
|
||||
RowLimitControl,
|
||||
TimeLimitMetricControl,
|
||||
InlineCheckboxControl as CheckboxControl,
|
||||
InlineTextControl as TextControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -35,8 +25,8 @@ const config: ControlPanelConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[MetricsControl()],
|
||||
[AdhocFiltersControl()],
|
||||
['metrics'],
|
||||
['adhoc_filters'],
|
||||
[
|
||||
{
|
||||
name: 'groupby',
|
||||
@@ -45,16 +35,20 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[LimitControl(), TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
['limit', 'timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
[
|
||||
CheckboxControl('contribution', {
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
}),
|
||||
{
|
||||
name: 'contribution',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Contribution'),
|
||||
default: false,
|
||||
description: t('Compute the contribution to the total'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[RowLimitControl(), null],
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -62,31 +56,43 @@ const config: ControlPanelConfig = {
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
[
|
||||
TextControl('significance_level', {
|
||||
label: t('Significance Level'),
|
||||
default: 0.05,
|
||||
description: t(
|
||||
'Threshold alpha level for determining significance',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'significance_level',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Significance Level'),
|
||||
default: 0.05,
|
||||
description: t(
|
||||
'Threshold alpha level for determining significance',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
TextControl('pvalue_precision', {
|
||||
label: t('p-value precision'),
|
||||
default: 6,
|
||||
description: t(
|
||||
'Number of decimal places with which to display p-values',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'pvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('p-value precision'),
|
||||
default: 6,
|
||||
description: t(
|
||||
'Number of decimal places with which to display p-values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
TextControl('liftvalue_precision', {
|
||||
label: t('Lift percent precision'),
|
||||
default: 4,
|
||||
description: t(
|
||||
'Number of decimal places with which to display lift values',
|
||||
),
|
||||
}),
|
||||
{
|
||||
name: 'liftvalue_precision',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Lift percent precision'),
|
||||
default: 4,
|
||||
description: t(
|
||||
'Number of decimal places with which to display lift values',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
@@ -17,18 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
AdhocFiltersControl,
|
||||
LimitControl,
|
||||
LinearColorSchemeControl,
|
||||
MetricsControl,
|
||||
OrderDescControl,
|
||||
RowLimitControl,
|
||||
SecondaryMetricControl,
|
||||
SeriesControl,
|
||||
TimeLimitMetricControl,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
@@ -36,13 +25,13 @@ const config: ControlPanelConfig = {
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[SeriesControl()],
|
||||
[MetricsControl()],
|
||||
[SecondaryMetricControl()],
|
||||
[AdhocFiltersControl()],
|
||||
[LimitControl(), RowLimitControl()],
|
||||
[TimeLimitMetricControl()],
|
||||
[OrderDescControl()],
|
||||
['series'],
|
||||
['metrics'],
|
||||
['secondary_metric'],
|
||||
['adhoc_filters'],
|
||||
['limit', 'row_limit'],
|
||||
['timeseries_limit_metric'],
|
||||
['order_desc'],
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -71,7 +60,7 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[LinearColorSchemeControl()],
|
||||
['linear_color_scheme'],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user