docs: bifurcate documentation into user and admin sections (#38196)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-02-26 16:29:08 -05:00
committed by GitHub
parent 8a053bbe07
commit 6589ee48f9
171 changed files with 10899 additions and 2866 deletions

View File

@@ -0,0 +1,318 @@
---
title: Overview
sidebar_position: 1
---
<!--
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.
-->
# Backend Style Guidelines
This is a list of statements that describe how we do backend development in Superset. While they might not be 100% true for all files in the repo, they represent the gold standard we strive towards for backend quality and style.
- We use a monolithic Python/Flask/Flask-AppBuilder backend, with small single-responsibility satellite services where necessary.
- Files are generally organized by feature or object type. Within each domain, we can have api controllers, models, schemas, commands, and data access objects (DAO).
- See: [Proposal for Improving Superset's Python Code Organization](https://github.com/apache/superset/issues/9077)
- API controllers use Marshmallow Schemas to serialize/deserialize data.
- Authentication and authorization are controlled by the [security manager](https://github.com/apache/superset/blob/master/superset/security/manager).
- We use Pytest for unit and integration tests. These live in the `tests` directory.
- We add tests for every new piece of functionality added to the backend.
- We use pytest fixtures to share setup between tests.
- We use SQLAlchemy to access both Superset's application database, and users' analytics databases.
- We make changes backwards compatible whenever possible.
- If a change cannot be made backwards compatible, it goes into a major release.
- See: [Proposal For Semantic Versioning](https://github.com/apache/superset/issues/12566)
- We use Swagger for API documentation, with docs written inline on the API endpoint code.
- We prefer thin ORM models, putting shared functionality in other utilities.
- Several linters/checkers are used to maintain consistent code style and type safety: pylint, mypy, black, isort.
- `__init__.py` files are kept empty to avoid implicit dependencies.
## Code Organization
### Domain-Driven Structure
Organize code by business domain rather than technical layers:
```
superset/
├── dashboards/
│ ├── api.py # API endpoints
│ ├── commands/ # Business logic
│ ├── dao.py # Data access layer
│ ├── models.py # Database models
│ └── schemas.py # Serialization schemas
├── charts/
│ ├── api.py
│ ├── commands/
│ ├── dao.py
│ ├── models.py
│ └── schemas.py
```
### API Controllers
Use Flask-AppBuilder with Marshmallow schemas:
```python
from flask_appbuilder.api import BaseApi
from marshmallow import Schema, fields
class DashboardSchema(Schema):
id = fields.Integer()
dashboard_title = fields.String(required=True)
created_on = fields.DateTime(dump_only=True)
class DashboardApi(BaseApi):
resource_name = "dashboard"
openapi_spec_tag = "Dashboards"
@expose("/", methods=["GET"])
@protect()
@safe
def get_list(self) -> Response:
"""Get a list of dashboards"""
# Implementation
```
### Commands Pattern
Use commands for business logic:
```python
from typing import Optional
from superset.commands.base import BaseCommand
from superset.dashboards.dao import DashboardDAO
class CreateDashboardCommand(BaseCommand):
def __init__(self, properties: Dict[str, Any]):
self._properties = properties
def run(self) -> Dashboard:
self.validate()
return DashboardDAO.create(self._properties)
def validate(self) -> None:
if not self._properties.get("dashboard_title"):
raise ValidationError("Title is required")
```
### Data Access Objects (DAOs)
See: [DAO Style Guidelines and Best Practices](./backend/dao-style-guidelines)
## Testing
### Unit Tests
```python
import pytest
from unittest.mock import patch
from superset.dashboards.commands.create import CreateDashboardCommand
def test_create_dashboard_success():
properties = {
"dashboard_title": "Test Dashboard",
"owners": [1]
}
command = CreateDashboardCommand(properties)
dashboard = command.run()
assert dashboard.dashboard_title == "Test Dashboard"
def test_create_dashboard_validation_error():
properties = {} # Missing required title
command = CreateDashboardCommand(properties)
with pytest.raises(ValidationError):
command.run()
```
### Integration Tests
```python
import pytest
from superset.app import create_app
from superset.extensions import db
@pytest.fixture
def app():
app = create_app()
app.config["TESTING"] = True
with app.app_context():
db.create_all()
yield app
db.drop_all()
def test_dashboard_api_create(app, auth_headers):
with app.test_client() as client:
response = client.post(
"/api/v1/dashboard/",
json={"dashboard_title": "Test Dashboard"},
headers=auth_headers
)
assert response.status_code == 201
```
## Security
### Authorization Decorators
```python
from flask_appbuilder.security.decorators import has_access
class DashboardApi(BaseApi):
@expose("/", methods=["POST"])
@protect()
@has_access("can_write", "Dashboard")
def post(self) -> Response:
"""Create a new dashboard"""
# Implementation
```
### Input Validation
```python
from marshmallow import Schema, fields, validate
class DashboardSchema(Schema):
dashboard_title = fields.String(
required=True,
validate=validate.Length(min=1, max=500)
)
slug = fields.String(
validate=validate.Regexp(r'^[a-z0-9-]+$')
)
```
## Database Operations
### Use SQLAlchemy ORM
```python
from sqlalchemy import Column, Integer, String, Text
from superset.models.helpers import AuditMixinNullable
class Dashboard(Model, AuditMixinNullable):
__tablename__ = "dashboards"
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
position_json = Column(Text)
def __repr__(self) -> str:
return f"<Dashboard {self.dashboard_title}>"
```
### Database Migrations
```python
# migration file
def upgrade():
op.add_column(
"dashboards",
sa.Column("new_column", sa.String(255), nullable=True)
)
def downgrade():
op.drop_column("dashboards", "new_column")
```
## Error Handling
### Custom Exceptions
```python
class SupersetException(Exception):
"""Base exception for Superset"""
pass
class ValidationError(SupersetException):
"""Raised when validation fails"""
pass
class SecurityException(SupersetException):
"""Raised when security check fails"""
pass
```
### Error Responses
```python
from flask import jsonify
from werkzeug.exceptions import BadRequest
@app.errorhandler(ValidationError)
def handle_validation_error(error):
return jsonify({
"message": str(error),
"error_type": "VALIDATION_ERROR"
}), 400
```
## Type Hints and Documentation
### Use Type Hints
```python
from typing import List, Optional, Dict, Any
from superset.models.dashboard import Dashboard
def get_dashboards_by_owner(owner_id: int) -> List[Dashboard]:
"""Get all dashboards owned by a specific user"""
return db.session.query(Dashboard).filter_by(owner_id=owner_id).all()
def create_dashboard(properties: Dict[str, Any]) -> Optional[Dashboard]:
"""Create a new dashboard with the given properties"""
# Implementation
```
### API Documentation
```python
from flask_appbuilder.api import BaseApi
from flask_appbuilder.api.schemas import get_list_schema
class DashboardApi(BaseApi):
@expose("/", methods=["GET"])
@protect()
@safe
def get_list(self) -> Response:
"""Get a list of dashboards
---
get:
description: >-
Get a list of dashboards
parameters:
- in: query
schema:
type: integer
name: page_size
description: Number of results per page
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardListResponse'
"""
# Implementation
```

View File

@@ -0,0 +1,375 @@
---
title: DAO Style Guidelines and Best Practices
sidebar_position: 2
---
<!--
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.
-->
# DAO Style Guidelines and Best Practices
A Data Access Object (DAO) is a pattern that provides an abstract interface to the SQLAlchemy Object Relational Mapper (ORM). The DAOs are critical as they form the building block of the application which are wrapped by the associated commands and RESTful API endpoints.
There are numerous inconsistencies and violations of the DRY principal within the codebase as it relates to DAOs and ORMs—unnecessary commits, non-ACID transactions, etc.—which makes the code unnecessarily complex and convoluted. Addressing the underlying issues with the DAOs _should_ help simplify the downstream operations and improve the developer experience.
To ensure consistency the following rules should be adhered to:
## Core Rules
1. **All database operations (including testing) should be defined within a DAO**, i.e., there should not be any explicit `db.session.add`, `db.session.merge`, etc. calls outside of a DAO.
2. **A DAO should use `create`, `update`, `delete`, `upsert` terms**—typical database operations which ensure consistency with commands—rather than action based terms like `save`, `saveas`, `override`, etc.
3. **Sessions should be managed via a [context manager](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#begin-once)** which auto-commits on success and rolls back on failure, i.e., there should be no explicit `db.session.commit` or `db.session.rollback` calls within the DAO.
4. **There should be a single atomic transaction representing the entirety of the operation**, i.e., when creating a dataset with associated columns and metrics either all the changes succeed when the transaction is committed, or all the changes are undone when the transaction is rolled back. SQLAlchemy supports [nested transactions](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#nested-transaction) via the `begin_nested` method which can be nested—inline with how DAOs are invoked.
5. **The database layer should adopt a "shift left" mentality** i.e., uniqueness/foreign key constraints, relationships, cascades, etc. should all be defined in the database layer rather than being enforced in the application layer.
6. **Exception-based validation**: Ask for forgiveness rather than permission. Try to perform the operation and rely on database constraints to verify that the model is acceptable, rather than pre-validating conditions.
7. **Bulk operations**: Provide bulk `create`, `update`, and `delete` methods where applicable for performance optimization.
8. **Sparse updates**: Updates should only modify explicitly defined attributes.
9. **Test transactions**: Tests should leverage nested transactions which should be rolled back on teardown, rather than deleting objects.
## DAO Implementation Examples
### Basic DAO Structure
```python
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from superset.extensions import db
from superset.models.dashboard import Dashboard
class DashboardDAO:
"""Data Access Object for Dashboard operations"""
@classmethod
def find_by_id(cls, dashboard_id: int) -> Optional[Dashboard]:
"""Find dashboard by ID"""
return db.session.query(Dashboard).filter_by(id=dashboard_id).first()
@classmethod
def find_by_ids(cls, dashboard_ids: List[int]) -> List[Dashboard]:
"""Find dashboards by list of IDs"""
return db.session.query(Dashboard).filter(
Dashboard.id.in_(dashboard_ids)
).all()
@classmethod
def create(cls, properties: Dict[str, Any]) -> Dashboard:
"""Create a new dashboard"""
with db.session.begin():
dashboard = Dashboard(**properties)
db.session.add(dashboard)
db.session.flush() # Get the ID
return dashboard
@classmethod
def update(cls, dashboard: Dashboard, properties: Dict[str, Any]) -> Dashboard:
"""Update an existing dashboard"""
with db.session.begin():
for key, value in properties.items():
setattr(dashboard, key, value)
return dashboard
@classmethod
def delete(cls, dashboard: Dashboard) -> None:
"""Delete a dashboard"""
with db.session.begin():
db.session.delete(dashboard)
```
### Complex DAO Operations
```python
class DatasetDAO:
"""Data Access Object for Dataset operations"""
@classmethod
def create_with_columns_and_metrics(
cls,
dataset_properties: Dict[str, Any],
columns: List[Dict[str, Any]],
metrics: List[Dict[str, Any]]
) -> Dataset:
"""Create dataset with associated columns and metrics atomically"""
with db.session.begin():
# Create the dataset
dataset = Dataset(**dataset_properties)
db.session.add(dataset)
db.session.flush() # Get the dataset ID
# Create columns
for column_props in columns:
column_props['dataset_id'] = dataset.id
column = TableColumn(**column_props)
db.session.add(column)
# Create metrics
for metric_props in metrics:
metric_props['dataset_id'] = dataset.id
metric = SqlMetric(**metric_props)
db.session.add(metric)
return dataset
@classmethod
def bulk_delete(cls, dataset_ids: List[int]) -> int:
"""Delete multiple datasets and return count"""
with db.session.begin():
count = db.session.query(Dataset).filter(
Dataset.id.in_(dataset_ids)
).delete(synchronize_session=False)
return count
```
### Query Methods
```python
class DashboardDAO:
@classmethod
def find_by_slug(cls, slug: str) -> Optional[Dashboard]:
"""Find dashboard by slug"""
return db.session.query(Dashboard).filter_by(slug=slug).first()
@classmethod
def find_by_owner(cls, owner_id: int) -> List[Dashboard]:
"""Find all dashboards owned by a user"""
return db.session.query(Dashboard).filter_by(
created_by_fk=owner_id
).all()
@classmethod
def search(
cls,
query: str,
page: int = 0,
page_size: int = 25
) -> Tuple[List[Dashboard], int]:
"""Search dashboards with pagination"""
base_query = db.session.query(Dashboard).filter(
Dashboard.dashboard_title.ilike(f"%{query}%")
)
total_count = base_query.count()
dashboards = base_query.offset(page * page_size).limit(page_size).all()
return dashboards, total_count
```
### Error Handling in DAOs
```python
from sqlalchemy.exc import IntegrityError
from superset.exceptions import DAOCreateFailedError, DAODeleteFailedError
class DashboardDAO:
@classmethod
def create(cls, properties: Dict[str, Any]) -> Dashboard:
"""Create a new dashboard with error handling"""
try:
with db.session.begin():
dashboard = Dashboard(**properties)
db.session.add(dashboard)
db.session.flush()
return dashboard
except IntegrityError as ex:
raise DAOCreateFailedError(str(ex)) from ex
@classmethod
def delete(cls, dashboard: Dashboard) -> None:
"""Delete a dashboard with error handling"""
try:
with db.session.begin():
db.session.delete(dashboard)
except IntegrityError as ex:
raise DAODeleteFailedError(
f"Cannot delete dashboard {dashboard.id}: {str(ex)}"
) from ex
```
## Best Practices
### 1. Use Class Methods
All DAO methods should be class methods (`@classmethod`) rather than instance methods:
```python
# ✅ Good
class DashboardDAO:
@classmethod
def find_by_id(cls, dashboard_id: int) -> Optional[Dashboard]:
return db.session.query(Dashboard).filter_by(id=dashboard_id).first()
# ❌ Avoid
class DashboardDAO:
def find_by_id(self, dashboard_id: int) -> Optional[Dashboard]:
return db.session.query(Dashboard).filter_by(id=dashboard_id).first()
```
### 2. Use Context Managers for Transactions
Always use context managers to ensure proper transaction handling:
```python
# ✅ Good - automatic commit/rollback
@classmethod
def create(cls, properties: Dict[str, Any]) -> Dashboard:
with db.session.begin():
dashboard = Dashboard(**properties)
db.session.add(dashboard)
return dashboard
# ❌ Avoid - manual commit/rollback
@classmethod
def create(cls, properties: Dict[str, Any]) -> Dashboard:
try:
dashboard = Dashboard(**properties)
db.session.add(dashboard)
db.session.commit()
return dashboard
except Exception:
db.session.rollback()
raise
```
### 3. Use Descriptive Method Names
Method names should clearly indicate the operation:
```python
# ✅ Good - clear CRUD operations
create()
update()
delete()
find_by_id()
find_by_slug()
# ❌ Avoid - ambiguous names
save()
remove()
get()
```
### 4. Type Hints
Always include type hints for parameters and return values:
```python
@classmethod
def find_by_ids(cls, dashboard_ids: List[int]) -> List[Dashboard]:
"""Find dashboards by list of IDs"""
return db.session.query(Dashboard).filter(
Dashboard.id.in_(dashboard_ids)
).all()
```
### 5. Batch Operations
Provide efficient batch operations when needed:
```python
@classmethod
def bulk_update_published_status(
cls,
dashboard_ids: List[int],
published: bool
) -> int:
"""Update published status for multiple dashboards"""
with db.session.begin():
count = db.session.query(Dashboard).filter(
Dashboard.id.in_(dashboard_ids)
).update(
{Dashboard.published: published},
synchronize_session=False
)
return count
```
## Testing DAOs
### Unit Tests for DAOs
```python
import pytest
from superset.dashboards.dao import DashboardDAO
from superset.models.dashboard import Dashboard
def test_dashboard_create(session):
"""Test creating a dashboard"""
properties = {
"dashboard_title": "Test Dashboard",
"slug": "test-dashboard"
}
dashboard = DashboardDAO.create(properties)
assert dashboard.id is not None
assert dashboard.dashboard_title == "Test Dashboard"
assert dashboard.slug == "test-dashboard"
def test_dashboard_find_by_slug(session):
"""Test finding dashboard by slug"""
# Create test data
dashboard = Dashboard(
dashboard_title="Test Dashboard",
slug="test-dashboard"
)
session.add(dashboard)
session.commit()
# Test the DAO method
found_dashboard = DashboardDAO.find_by_slug("test-dashboard")
assert found_dashboard is not None
assert found_dashboard.dashboard_title == "Test Dashboard"
def test_dashboard_delete(session):
"""Test deleting a dashboard"""
dashboard = Dashboard(dashboard_title="Test Dashboard")
session.add(dashboard)
session.commit()
dashboard_id = dashboard.id
DashboardDAO.delete(dashboard)
deleted_dashboard = DashboardDAO.find_by_id(dashboard_id)
assert deleted_dashboard is None
```
### Integration Tests
```python
def test_create_dataset_with_columns_atomic(app_context):
"""Test that creating dataset with columns is atomic"""
dataset_properties = {"table_name": "test_table"}
columns = [{"column_name": "col1"}, {"column_name": "col2"}]
# This should succeed completely or fail completely
dataset = DatasetDAO.create_with_columns_and_metrics(
dataset_properties, columns, []
)
assert dataset.id is not None
assert len(dataset.columns) == 2
```

View File

@@ -0,0 +1,235 @@
---
title: Design Guidelines
sidebar_position: 1
---
<!--
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.
-->
# Design Guidelines
This is an area to host resources and documentation supporting the evolution and proper use of Superset design system elements. If content is to be added to this section or requires revisiting, a proposal should be submitted to the `dev@superset.apache.org` email list with either a text proposal or a link to a GitHub issue providing the markdown that will be added to this wiki. The Dev list will have the chance to review the proposal and arrive at lazy consensus. A committer may then copy/paste the markdown to this wiki, and make it public.
## Capitalization Guidelines
### Sentence case
Use sentence-case capitalization for everything in the UI (except these exceptions below).
Sentence case is predominantly lowercase. Capitalize only the initial character of the first word, and other words that require capitalization, like:
- **Proper nouns.** Objects in the product _are not_ considered proper nouns e.g. dashboards, charts, saved queries etc. Proprietary feature names eg. SQL Lab, Preset Manager _are_ considered proper nouns
- **Acronyms** (e.g. CSS, HTML)
- When referring to **UI labels that are themselves capitalized** from sentence case (e.g. page titles - Dashboards page, Charts page, Saved queries page, etc.)
- User input that is reflected in the UI. E.g. a user-named a dashboard tab
**Sentence case vs. Title case:**
- Title case: "A Dog Takes a Walk in Paris"
- Sentence case: "A dog takes a walk in Paris"
**Why sentence case?**
- It's generally accepted as the quickest to read
- It's the easiest form to distinguish between common and proper nouns
### How to refer to UI elements
When writing about a UI element, use the same capitalization as used in the UI.
For example, if an input field is labeled "Name" then you refer to this as the "Name input field". Similarly, if a button has the label "Save" in it, then it is correct to refer to the "Save button".
Where a product page is titled "Settings", you refer to this in writing as follows:
"Edit your personal information on the Settings page".
Often a product page will have the same title as the objects it contains. In this case, refer to the page as it appears in the UI, and the objects as common nouns:
- Upload a dashboard on the Dashboards page
- Go to Dashboards
- View dashboard
- View all dashboards
- Upload CSS templates on the CSS templates page
- Queries that you save will appear on the Saved queries page
- Create custom queries in SQL Lab then create dashboards
### Exceptions to sentence case
1. **Acronyms and abbreviations.**
Examples: URL, CSV, XML, CSS, SQL, SSH, URI, NaN, CRON, CC, BCC
2. **Proper nouns and brand names.**
Examples: Apache, Superset, AntD JavaScript, GeoJSON, Slack, Google Sheets, SQLAlchemy
3. **Technical terms derived from proper nouns.**
Examples: Jinja, Gaussian, European (as in European time zone)
4. **Key names.** Capitalize button labels and UI elements as they appear in the product UI.
Examples: Shift (as in the keyboard button), Enter key
5. **Named queries or specific labeled items.**
Examples: Query A, Query B
6. **Database names.** Always capitalize names of database engines and connectors.
Examples: Presto, Trino, Drill, Hive, Google Sheets
## Button Design Guidelines
### Overview
**Button variants:**
1. Primary
2. Secondary
3. Tertiary
4. Destructive
**Button styles:** Each button variant has three styles:
1. Text
2. Icon+text
3. Icon only
Primary buttons have a fourth style: dropdown.
**Usage:** Buttons communicate actions that users can take. Do not use for navigations, instead use links.
**Purpose:**
| Button Type | Description |
|------------|-------------|
| Primary | Main call to action, just 1 per page not including modals or main headers |
| Secondary | Secondary actions, always in conjunction with a primary |
| Tertiary | For less prominent actions; can be used in isolation or paired with a primary button |
| Destructive | For actions that could have destructive effects on the user's data |
### Format
#### Anatomy
Button text is centered using the Label style. Icons appear left of text when combined. If no text label exists, an icon must indicate the button's function.
#### Button size
- Default dimensions: 160px width × 32px height
- Text: 11px, Inter Medium, all caps
- Corners: 4px border radius
- Minimum padding: 8px around text
- Width can decrease if space is limited, but maintain minimum padding
#### Button groups
- Group related buttons to establish visual hierarchy
- Avoid overwhelming users with too many actions
- Limit calls to action; use tertiary/ghost buttons for layouts with 3+ actions
- Maintain consistent styles within groups when possible
- Space buttons 8px apart vertically or horizontally
#### Content guidelines
Button labels should be clear and predictable. Use the "\{verb\} + \{noun\}" format, except for common actions like "Done," "Close," "Cancel," "Add," or "Delete." This formula provides necessary context and aids translation, though compact UIs or localization needs may warrant exceptions.
## Error Message Design Guidelines
### Definition
Interface errors appear when the application can't do what the user wants, typically because:
- The app technically fails to complete the request
- The app can't understand the user input
- The user tries to combine operations that can't work together
In all cases, encountering errors increases user friction and frustration while trying to use the application. Providing an error experience that helps the user understand what happened and their next steps is key to building user confidence and increasing engagement.
### General best practices
**The best error experience is no error at all.** Before implementing error patterns, consider what you might do in the interface before the user would encounter an error to prevent it from happening at all. This might look like:
- Providing tooltips or microcopy to help users understand how to interact with the interface
- Disabling parts of the UI until desired conditions are met (e.g. disabling a Save button until mandatory fields in a form are completed)
- Correcting an error automatically (e.g. autocorrecting spelling errors)
**Only report errors users care about.** The only errors that should appear in the interface are errors that require user acknowledgement and action (even if that action is "try again later" or "contact support").
**Do not start the user in an error state.** If user inputs are required to display an initial interface (e.g. a chart in Explore), use empty states or field highlighting to drive users to the required action.
### Patterns
#### Pattern selection
Select one pattern per error (e.g. do not implement an inline and banner pattern for the same error).
| When the error... | Use... |
|------------------|--------|
| Is directly related to a UI control | Inline error |
| Is not directly related to a UI control | Banner error |
#### Inline
Inline errors are used when the source of the error is directly related to a UI control (text input, selector, etc.) such as the user not populating a required field or entering a number in a text field.
##### Anatomy
Use the `LabeledErrorBoundInput` component for this error pattern.
1. **Message** Provides direction on how to populate the input correctly.
##### Implementation details
- Where and when relevant, scroll the screen to the UI control with the error
- When multiple inline errors are present, scroll to the topmost error
#### Banner
Banner errors are used when the source of the error is not directly related to a UI control (text input, selector, etc.) such as a technical failure or a loading problem.
##### Anatomy
Use the `ErrorAlert` component for this error pattern.
1. **Headline** (optional): 1-2 word summary of the error
2. **Message**: What went wrong and what users should do next
3. **Expand option** (optional): "See more"/"See less"
4. **Details** (optional): Additional helpful context
5. **Modal** (optional): For spatial constraints using `ToastType.DANGER`
##### Implementation details
- Place the banner near the content area most relevant to the error
- For chart errors in Explore, use the chart area
- For modal errors, use the modal footer
- For app-wide errors, use the top of the screen
### Content guidelines
Effective error messages communicate:
1. What went wrong
2. What users should do next
Error messages should be:
- Clear and accurate, leaving no room for misinterpretation
- Short and concise
- Understandable to non-technical users
- Non-blaming and avoiding negative language
**Example:**
❌ "Cannot delete a datasource that has slices attached to it."
✅ "Please delete all charts using this dataset before deleting the dataset."

View File

@@ -0,0 +1,51 @@
---
title: Overview
sidebar_position: 1
---
<!--
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.
-->
# Frontend Style Guidelines
This is a list of statements that describe how we do frontend development in Superset. While they might not be 100% true for all files in the repo, they represent the gold standard we strive towards for frontend quality and style.
- We develop using TypeScript.
- See: [SIP-36](https://github.com/apache/superset/issues/9101)
- We use React for building components, and Redux to manage app/global state.
- See: [Component Style Guidelines and Best Practices](./frontend/component-style-guidelines)
- We prefer functional components to class components and use hooks for local component state.
- We use [Ant Design](https://ant.design/) components from our component library whenever possible, only building our own custom components when it's required.
- See: [SIP-48](https://github.com/apache/superset/issues/11283)
- We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files.
- See: [SIP-37](https://github.com/apache/superset/issues/9145)
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines)
- We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests.
- See: [SIP-56](https://github.com/apache/superset/issues/11830)
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines)
- We add tests for every new component or file added to the frontend.
- We organize our repo so similar files live near each other, and tests are co-located with the files they test.
- See: [SIP-61](https://github.com/apache/superset/issues/12098)
- We prefer small, easily testable files and components.
- We use OXC (oxlint) and Prettier to automatically fix lint errors and format the code.
- We do not debate code formatting style in PRs, instead relying on automated tooling to enforce it.
- If there's not a linting rule, we don't have a rule!
- See: [Linting How-Tos](../contributing/howtos#typescript--javascript)
- We use [React Storybook](https://storybook.js.org/) to help preview/test and stabilize our components
- A public Storybook with components from the `master` branch is available [here](https://apache-superset.github.io/superset-ui/?path=/story/*)

View File

@@ -0,0 +1,192 @@
---
title: Component Style Guidelines and Best Practices
sidebar_position: 2
---
<!--
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.
-->
# Component Style Guidelines and Best Practices
This documentation illustrates how we approach component development in Superset and provides examples to help you in writing new components or updating existing ones by following our community-approved standards.
This guide is intended primarily for reusable components. Whenever possible, all new components should be designed with reusability in mind.
## General Guidelines
- We use [Ant Design](https://ant.design/) as our component library. Do not build a new component if Ant Design provides one but rather instead extend or customize what the library provides
- Always style your component using Emotion and always prefer the theme variables whenever applicable. See: [Emotion Styling Guidelines and Best Practices](./emotion-styling-guidelines)
- All components should be made to be reusable whenever possible
- All components should follow the structure and best practices as detailed below
### Directory and component structure
```
superset-frontend/src/components
{ComponentName}/
index.tsx
{ComponentName}.test.tsx
{ComponentName}.stories.tsx
```
**Components root directory:** Components that are meant to be re-used across different parts of the application should go in the `superset-frontend/src/components` directory. Components that are meant to be specific for a single part of the application should be located in the nearest directory where the component is used, for example, `superset-frontend/src/Explore/components`
**Exporting the component:** All components within the `superset-frontend/src/components` directory should be exported from `superset-frontend/src/components/index.ts` to facilitate their imports by other components
**Component directory name:** Use `PascalCase` for the component directory name
**Storybook:** Components should come with a storybook file whenever applicable, with the following naming convention `\{ComponentName\}.stories.tsx`. More details about Storybook below
**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines) for more details
**Reference naming:** Use `PascalCase` for React components and `camelCase` for component instances
**BAD:**
```jsx
import mainNav from './MainNav';
```
**GOOD:**
```jsx
import MainNav from './MainNav';
```
**BAD:**
```jsx
const NavItem = <MainNav />;
```
**GOOD:**
```jsx
const navItem = <MainNav />;
```
**Component naming:** Use the file name as the component name
**BAD:**
```jsx
import MainNav from './MainNav/index';
```
**GOOD:**
```jsx
import MainNav from './MainNav';
```
**Props naming:** Do not use DOM related props for different purposes
**BAD:**
```jsx
<MainNav style="big" />
```
**GOOD:**
```jsx
<MainNav variant="big" />
```
**Importing dependencies:** Only import what you need
**BAD:**
```jsx
import * as React from "react";
```
**GOOD:**
```jsx
import React, { useState } from "react";
```
**Default VS named exports:** As recommended by [TypeScript](https://www.typescriptlang.org/docs/handbook/modules.html), "If a module's primary purpose is to house one specific export, then you should consider exporting it as a default export. This makes both importing and actually using the import a little easier". If you're exporting multiple objects, use named exports instead.
_As a default export_
```jsx
import MainNav from './MainNav';
```
_As a named export_
```jsx
import { MainNav, SecondaryNav } from './Navbars';
```
**ARIA roles:** Always make sure you are writing accessible components by using the official [ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques)
## Use TypeScript
All components should be written in TypeScript and their extensions should be `.ts` or `.tsx`
### type vs interface
Validate all props with the correct types. This replaces the need for a run-time validation as provided by the prop-types library.
```tsx
type HeadingProps = {
param: string;
}
export default function Heading({ children }: HeadingProps) {
return <h2>{children}</h2>
}
```
Use `type` for your component props and state. Use `interface` when you want to enable _declaration merging_.
### Define default values for non-required props
In order to improve the readability of your code and reduce assumptions, always add default values for non required props, when applicable, for example:
```tsx
const applyDiscount = (price: number, discount = 0.05) => price * (1 - discount);
```
## Functional components and Hooks
We prefer functional components and the usage of hooks over class components.
### useState
Always explicitly declare the type unless the type can easily be assumed by the declaration.
```tsx
const [customer, setCustomer] = useState<ICustomer | null>(null);
```
### useReducer
Always prefer `useReducer` over `useState` when your state has complex logics.
### useMemo and useCallback
Always memoize when your components take functions or complex objects as props to avoid unnecessary rerenders.
### Custom hooks
All custom hooks should be located in the directory `/src/hooks`. Before creating a new custom hook, make sure that is not available in the existing custom hooks.
## Storybook
Each component should come with its dedicated storybook file.
**One component per story:** Each storybook file should only contain one component unless substantially different variants are required
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/src/components/Select/Select.stories.tsx` as an example.
**Isolated state:** The storybook should show how the component works in an isolated state and with as few dependencies as possible
**Use args:** It should be possible to test the component with every variant of the available props. We recommend using [args](https://storybook.js.org/docs/react/writing-stories/args)

View File

@@ -0,0 +1,277 @@
---
title: Emotion Styling Guidelines and Best Practices
sidebar_position: 3
---
<!--
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.
-->
# Emotion Styling Guidelines and Best Practices
## Emotion Styling Guidelines
### DO these things:
- **DO** use `styled` when you want to include additional (nested) class selectors in your styles
- **DO** use `styled` components when you intend to export a styled component for re-use elsewhere
- **DO** use `css` when you want to amend/merge sets of styles compositionally
- **DO** use `css` when you're making a small, or single-use set of styles for a component
- **DO** move your style definitions from direct usage in the `css` prop to an external variable when they get long
- **DO** prefer tagged template literals (`css={css`...`}`) over style objects wherever possible for maximum style portability/consistency (note: typescript support may be diminished, but IDE plugins like [this](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components) make life easy)
- **DO** use `useTheme` to get theme variables. `withTheme` should be only used for wrapping legacy Class-based components.
### DON'T do these things:
- **DON'T** use `styled` for small, single-use style tweaks that would be easier to read/review if they were inline
- **DON'T** export incomplete AntD components (make sure all their compound components are exported)
## Emotion Tips and Strategies
The first thing to consider when adding styles to an element is how much you think a style might be reusable in other areas of Superset. Always err on the side of reusability here. Nobody wants to chase styling inconsistencies, or try to debug little endless overrides scattered around the codebase. The more we can consolidate, the less will have to be figured out by those who follow. Reduce, reuse, recycle.
## When to use `css` or `styled`
In short, either works for just about any use case! And you'll see them used somewhat interchangeably in the existing codebase. But we need a way to weigh it when we encounter the choice, so here's one way to think about it:
A good use of `styled` syntax if you want to re-use a styled component. In other words, if you wanted to export flavors of a component for use, like so:
```jsx
const StatusThing = styled.div`
padding: 10px;
border-radius: 10px;
`;
export const InfoThing = styled(StatusThing)`
background: blue;
&::before {
content: "";
}
`;
export const WarningThing = styled(StatusThing)`
background: orange;
&::before {
content: "⚠️";
}
`;
export const TerribleThing = styled(StatusThing)`
background: red;
&::before {
content: "🔥";
}
`;
```
You can also use `styled` when you're building a bigger component, and just want to have some custom bits for internal use in your JSX. For example:
```jsx
const SeparatorOnlyUsedInThisComponent = styled.hr`
height: 12px;
border: 0;
box-shadow: inset 0 12px 12px -12px rgba(0, 0, 0, 0.5);
`;
function SuperComplicatedComponent(props) {
return (
<>
Daily standup for {user.name}!
<SeparatorOnlyUsedInThisComponent />
<h2>Yesterday:</h2>
// spit out a list of accomplishments
<SeparatorOnlyUsedInThisComponent />
<h2>Today:</h2>
// spit out a list of plans
<SeparatorOnlyUsedInThisComponent />
<h2>Tomorrow:</h2>
// spit out a list of goals
</>
);
}
```
The `css` prop, in reality, shares all the same styling capabilities as `styled` but it does have some particular use cases that jump out as sensible. For example, if you just want to style one element in your component, you could add the styles inline like so:
```jsx
function SomeFanciness(props) {
return (
<>
Here's an awesome report card for {user.name}!
<div
css={css`
box-shadow: 5px 5px 10px #ccc;
border-radius: 10px;
`}
>
<h2>Yesterday:</h2>
// ...some stuff
<h2>Today:</h2>
// ...some stuff
<h2>Tomorrow:</h2>
// ...some stuff
</div>
</>
);
}
```
You can also define the styles as a variable, external to your JSX. This is handy if the styles get long and you just want it out of the way. This is also handy if you want to apply the same styles to disparate element types, kind of like you might use a CSS class on varied elements. Here's a trumped up example:
```jsx
function FakeGlobalNav(props) {
const menuItemStyles = css`
display: block;
border-bottom: 1px solid cadetblue;
font-family: "Comic Sans", cursive;
`;
return (
<Nav>
<a css={menuItemStyles} href="#">One link</a>
<Link css={menuItemStyles} to={url}>Another link</Link>
<div css={menuItemStyles} onClick={() => alert('clicked')}>Another link</div>
</Nav>
);
}
```
## CSS tips and tricks
### `css` lets you write actual CSS
By default the `css` prop uses the object syntax with JS style definitions, like so:
```jsx
<div css={{
borderRadius: 10,
marginTop: 10,
backgroundColor: '#00FF00'
}}>Howdy</div>
```
But you can use the `css` interpolator as well to get away from icky JS styling syntax. Doesn't this look cleaner?
```jsx
<div css={css`
border-radius: 10px;
margin-top: 10px;
background-color: #00FF00;
`}>Howdy</div>
```
You might say "whatever… I can read and write JS syntax just fine." Well, that's great. But… let's say you're migrating in some of our legacy LESS styles… now it's copy/paste! Or if you want to migrate to or from `styled` syntax… also copy/paste!
### You can combine `css` definitions with array syntax
You can use multiple groupings of styles with the `css` interpolator, and combine/override them in array syntax, like so:
```jsx
function AnotherSillyExample(props) {
const shadowedCard = css`
box-shadow: 2px 2px 4px #999;
padding: 4px;
`;
const infoCard = css`
background-color: #33f;
border-radius: 4px;
`;
const overrideInfoCard = css`
background-color: #f33;
`;
return (
<div className="App">
Combining two classes:
<div css={[shadowedCard, infoCard]}>Hello</div>
Combining again, but now with overrides:
<div css={[shadowedCard, infoCard, overrideInfoCard]}>Hello</div>
</div>
);
}
```
### Style variations with props
You can give any component a custom prop, and reference that prop in your component styles, effectively using the prop to turn on a "flavor" of that component.
For example, let's make a styled component that acts as a card. Of course, this could be done with any AntD component, or any component at all. But we'll do this with a humble `div` to illustrate the point:
```jsx
const SuperCard = styled.div`
${({ theme, cutout }) => `
padding: ${theme.gridUnit * 2}px;
border-radius: ${theme.borderRadius}px;
box-shadow: 10px 5px 10px #ccc ${cutout && 'inset'};
border: 1px solid ${cutout ? 'transparent' : theme.colors.secondary.light3};
`}
`;
```
Then just use the component as `<SuperCard>Some content</SuperCard>` or with the (potentially dynamic) prop: `<SuperCard cutout>Some content</SuperCard>`
## Styled component tips
### No need to use `theme` the hard way
It's very tempting (and commonly done) to use the `theme` prop inline in the template literal like so:
```jsx
const SomeStyledThing = styled.div`
padding: ${({ theme }) => theme.gridUnit * 2}px;
border-radius: ${({ theme }) => theme.borderRadius}px;
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
`;
```
Instead, you can make things a little easier to read/type by writing it like so:
```jsx
const SomeStyledThing = styled.div`
${({ theme }) => `
padding: ${theme.gridUnit * 2}px;
border-radius: ${theme.borderRadius}px;
border: 1px solid ${theme.colors.secondary.light3};
`}
`;
```
## Extend an AntD component with custom styling
As mentioned, you want to keep your styling as close to the root of your component system as possible, to minimize repetitive styling/overrides, and err on the side of reusability. In some cases, that means you'll want to globally tweak one of our core components to match our design system. In Superset, that's Ant Design (AntD).
AntD uses a cool trick called compound components. For example, the `Menu` component also lets you use `Menu.Item`, `Menu.SubMenu`, `Menu.ItemGroup`, and `Menu.Divider`.
### The `Object.assign` trick
Let's say you want to override an AntD component called `Foo`, and have `Foo.Bar` display some custom CSS for the `Bar` compound component. You can do it effectively like so:
```jsx
import {
Foo as AntdFoo,
} from 'antd';
export const StyledBar = styled(AntdFoo.Bar)`
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
export const Foo = Object.assign(AntdFoo, {
Bar: StyledBar,
});
```
You can then import this customized `Foo` and use `Foo.Bar` as expected. You should probably save your creation in `src/components` for maximum reusability, and add a Storybook entry so future engineers can view your creation, and designers can better understand how it fits the Superset Design System.