mirror of
https://github.com/apache/superset.git
synced 2026-07-02 04:45:37 +00:00
Compare commits
1 Commits
docs/testi
...
fix-sql-la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b253006ecb |
@@ -1,10 +0,0 @@
|
|||||||
# JavaScript to TypeScript Migration Command
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
```
|
|
||||||
/js-to-ts <core-filename>
|
|
||||||
```
|
|
||||||
- `<core-filename>` - Path to CORE file relative to `superset-frontend/` (e.g., `src/utils/common.js`, `src/middleware/loggerMiddleware.js`)
|
|
||||||
|
|
||||||
## Agent Instructions
|
|
||||||
**See:** [../projects/js-to-ts/AGENT.md](../projects/js-to-ts/AGENT.md) for complete migration guide.
|
|
||||||
@@ -1,684 +0,0 @@
|
|||||||
# JavaScript to TypeScript Migration Agent Guide
|
|
||||||
|
|
||||||
**Complete technical reference for converting JavaScript/JSX files to TypeScript/TSX in Apache Superset frontend.**
|
|
||||||
|
|
||||||
**Agent Role:** Atomic migration unit - migrate the core file + ALL related tests/mocks as one cohesive unit. Use `git mv` to preserve history, NO `git commit`. NO global import changes. Report results upon completion.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Migration Principles
|
|
||||||
|
|
||||||
1. **Atomic migration units** - Core file + all related tests/mocks migrate together
|
|
||||||
2. **Zero `any` types** - Use proper TypeScript throughout
|
|
||||||
3. **Leverage existing types** - Reuse established definitions
|
|
||||||
4. **Type inheritance** - Derivatives extend base component types
|
|
||||||
5. **Strategic placement** - File types for maximum discoverability
|
|
||||||
6. **Surgical improvements** - Enhance existing types during migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 0: Dependency Check (MANDATORY)
|
|
||||||
|
|
||||||
**Command:**
|
|
||||||
```bash
|
|
||||||
grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" superset-frontend/{filename}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Decision:**
|
|
||||||
- ✅ No matches → Proceed with atomic migration (core + tests + mocks)
|
|
||||||
- ❌ Matches found → EXIT with dependency report (see format below)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1: Identify Related Files (REQUIRED)
|
|
||||||
|
|
||||||
**Atomic Migration Scope:**
|
|
||||||
For core file `src/utils/example.js`, also migrate:
|
|
||||||
- `src/utils/example.test.js` / `src/utils/example.test.jsx`
|
|
||||||
- `src/utils/example.spec.js` / `src/utils/example.spec.jsx`
|
|
||||||
- `src/utils/__mocks__/example.js`
|
|
||||||
- Any other related test/mock files found by pattern matching
|
|
||||||
|
|
||||||
**Find all related test and mock files:**
|
|
||||||
```bash
|
|
||||||
# Pattern-based search for related files
|
|
||||||
basename=$(basename {filename} .js)
|
|
||||||
dirname=$(dirname superset-frontend/{filename})
|
|
||||||
|
|
||||||
# Find test files
|
|
||||||
find "$dirname" -name "${basename}.test.js" -o -name "${basename}.test.jsx"
|
|
||||||
find "$dirname" -name "${basename}.spec.js" -o -name "${basename}.spec.jsx"
|
|
||||||
|
|
||||||
# Find mock files
|
|
||||||
find "$dirname" -name "__mocks__/${basename}.js"
|
|
||||||
find "$dirname" -name "${basename}.mock.js"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Migration Requirement:** All discovered related files MUST be migrated together as one atomic unit.
|
|
||||||
|
|
||||||
**Test File Creation:** If NO test files exist for the core file, CREATE a minimal test file using the following pattern:
|
|
||||||
- Location: Same directory as core file
|
|
||||||
- Name: `{basename}.test.ts` (e.g., `DebouncedMessageQueue.test.ts`)
|
|
||||||
- Content: Basic test structure importing and testing the main functionality
|
|
||||||
- Use proper TypeScript types in test file
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗺️ Type Reference Map
|
|
||||||
|
|
||||||
### From `@superset-ui/core`
|
|
||||||
```typescript
|
|
||||||
// Data & Query
|
|
||||||
QueryFormData, QueryData, JsonObject, AnnotationData, AdhocMetric
|
|
||||||
LatestQueryFormData, GenericDataType, DatasourceType, ExtraFormData
|
|
||||||
DataMaskStateWithId, NativeFilterScope, NativeFiltersState, NativeFilterTarget
|
|
||||||
|
|
||||||
// UI & Theme
|
|
||||||
FeatureFlagMap, LanguagePack, ColorSchemeConfig, SequentialSchemeConfig
|
|
||||||
```
|
|
||||||
|
|
||||||
### From `@superset-ui/chart-controls`
|
|
||||||
```typescript
|
|
||||||
Dataset, ColumnMeta, ControlStateMapping
|
|
||||||
```
|
|
||||||
|
|
||||||
### From Local Types (`src/types/`)
|
|
||||||
```typescript
|
|
||||||
// Authentication
|
|
||||||
User, UserWithPermissionsAndRoles, BootstrapUser, PermissionsAndRoles
|
|
||||||
|
|
||||||
// Dashboard
|
|
||||||
Dashboard, DashboardState, DashboardInfo, DashboardLayout, LayoutItem
|
|
||||||
ComponentType, ChartConfiguration, ActiveFilters
|
|
||||||
|
|
||||||
// Charts
|
|
||||||
Chart, ChartState, ChartStatus, ChartLinkedDashboard, Slice, SaveActionType
|
|
||||||
|
|
||||||
// Data
|
|
||||||
Datasource, Database, Owner, Role
|
|
||||||
|
|
||||||
// UI Components
|
|
||||||
TagType, FavoriteStatus, Filter, ImportResourceName
|
|
||||||
```
|
|
||||||
|
|
||||||
### From Domain Types
|
|
||||||
```typescript
|
|
||||||
// src/dashboard/types.ts
|
|
||||||
RootState, ChartsState, DatasourcesState, FilterBarOrientation
|
|
||||||
ChartCrossFiltersConfig, ActiveTabs, MenuKeys
|
|
||||||
|
|
||||||
// src/explore/types.ts
|
|
||||||
ExplorePageInitialData, ExplorePageState, ExploreResponsePayload, OptionSortType
|
|
||||||
|
|
||||||
// src/SqlLab/types.ts
|
|
||||||
[SQL Lab specific types]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Type Organization Strategy
|
|
||||||
|
|
||||||
### Type Placement Hierarchy
|
|
||||||
|
|
||||||
1. **Component-Colocated** (90% of cases)
|
|
||||||
```typescript
|
|
||||||
// Same file as component
|
|
||||||
interface MyComponentProps {
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Feature-Shared**
|
|
||||||
```typescript
|
|
||||||
// src/[domain]/components/[Feature]/types.ts
|
|
||||||
export interface FilterConfiguration {
|
|
||||||
filterId: string;
|
|
||||||
targets: NativeFilterTarget[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Domain-Wide**
|
|
||||||
```typescript
|
|
||||||
// src/[domain]/types.ts
|
|
||||||
export interface ExploreFormData extends QueryFormData {
|
|
||||||
viz_type: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Global**
|
|
||||||
```typescript
|
|
||||||
// src/types/[TypeName].ts
|
|
||||||
export interface ApiResponse<T> {
|
|
||||||
result: T;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Discovery Commands
|
|
||||||
```bash
|
|
||||||
# Search existing types before creating
|
|
||||||
find superset-frontend/src -name "types.ts" -exec grep -l "[TypeConcept]" {} \;
|
|
||||||
grep -r "interface.*Props\|type.*Props" superset-frontend/src/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Derivative Component Patterns
|
|
||||||
|
|
||||||
**Rule:** Components that extend others should extend their type interfaces.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ Base component type
|
|
||||||
interface SelectProps {
|
|
||||||
value: string | number;
|
|
||||||
options: SelectOption[];
|
|
||||||
onChange: (value: string | number) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Derivative extends base
|
|
||||||
interface ChartSelectProps extends SelectProps {
|
|
||||||
charts: Chart[];
|
|
||||||
onChartSelect: (chart: Chart) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Derivative with modified props
|
|
||||||
interface DatabaseSelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
|
|
||||||
value: number; // Narrowed type
|
|
||||||
onChange: (databaseId: number) => void; // Specific signature
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Patterns:**
|
|
||||||
- **Extension:** `extends BaseProps` - adds new props
|
|
||||||
- **Omission:** `Omit<BaseProps, 'prop'>` - removes props
|
|
||||||
- **Modification:** `Omit<BaseProps, 'prop'> & { prop: NewType }` - changes prop type
|
|
||||||
- **Restriction:** Override with narrower types (union → specific)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Migration Recipe
|
|
||||||
|
|
||||||
### Step 2: File Conversion
|
|
||||||
```bash
|
|
||||||
# Use git mv to preserve history
|
|
||||||
git mv component.js component.ts
|
|
||||||
git mv Component.jsx Component.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Import & Type Setup
|
|
||||||
```typescript
|
|
||||||
// Import order (enforced by linting)
|
|
||||||
import { FC, ReactNode } from 'react';
|
|
||||||
import { JsonObject, QueryFormData } from '@superset-ui/core';
|
|
||||||
import { Dataset } from '@superset-ui/chart-controls';
|
|
||||||
import type { Dashboard } from 'src/types/Dashboard';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Function & Component Typing
|
|
||||||
```typescript
|
|
||||||
// Functions with proper parameter/return types
|
|
||||||
export function processData(
|
|
||||||
data: Dataset[],
|
|
||||||
config: JsonObject
|
|
||||||
): ProcessedData[] {
|
|
||||||
// implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component props with inheritance
|
|
||||||
interface ComponentProps extends BaseProps {
|
|
||||||
data: Chart[];
|
|
||||||
onSelect: (id: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Component: FC<ComponentProps> = ({ data, onSelect }) => {
|
|
||||||
// implementation
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: State & Redux Typing
|
|
||||||
```typescript
|
|
||||||
// Hooks with specific types
|
|
||||||
const [data, setData] = useState<Chart[]>([]);
|
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Redux with existing RootState
|
|
||||||
const mapStateToProps = (state: RootState) => ({
|
|
||||||
charts: state.charts,
|
|
||||||
user: state.user,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧠 Type Debugging Strategies (Real-World Learnings)
|
|
||||||
|
|
||||||
### The Evolution of Type Approaches
|
|
||||||
When you hit type errors, follow this debugging evolution:
|
|
||||||
|
|
||||||
#### 1. ❌ Idealized Union Types (First Attempt)
|
|
||||||
```typescript
|
|
||||||
// Looks clean but doesn't match reality
|
|
||||||
type DatasourceInput = Datasource | QueryEditor;
|
|
||||||
```
|
|
||||||
**Problem**: Real calling sites pass variations, not exact types.
|
|
||||||
|
|
||||||
#### 2. ❌ Overly Precise Types (Second Attempt)
|
|
||||||
```typescript
|
|
||||||
// Tried to match exact calling signatures
|
|
||||||
type DatasourceInput =
|
|
||||||
| IDatasource // From DatasourcePanel
|
|
||||||
| (QueryEditor & { columns: ColumnMeta[] }); // From SaveQuery
|
|
||||||
```
|
|
||||||
**Problem**: Too rigid, doesn't handle legacy variations.
|
|
||||||
|
|
||||||
#### 3. ✅ Flexible Interface (Final Solution)
|
|
||||||
```typescript
|
|
||||||
// Captures what the function actually needs
|
|
||||||
interface DatasourceInput {
|
|
||||||
name?: string | null; // Allow null for compatibility
|
|
||||||
datasource_name?: string | null; // Legacy variations
|
|
||||||
columns?: any[]; // Multiple column types accepted
|
|
||||||
database?: { id?: number };
|
|
||||||
// ... other optional properties
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Success**: Works with all calling sites, focuses on function needs.
|
|
||||||
|
|
||||||
### Type Debugging Process
|
|
||||||
1. **Start with compilation errors** - they show exact mismatches
|
|
||||||
2. **Examine actual usage** - look at calling sites, not idealized types
|
|
||||||
3. **Build flexible interfaces** - capture what functions need, not rigid contracts
|
|
||||||
4. **Iterate based on downstream validation** - let calling sites guide your types
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Anti-Patterns to Avoid
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ Never use any
|
|
||||||
const obj: any = {};
|
|
||||||
|
|
||||||
// ✅ Use proper types
|
|
||||||
const obj: Record<string, JsonObject> = {};
|
|
||||||
|
|
||||||
// ❌ Don't recreate base component props
|
|
||||||
interface ChartSelectProps {
|
|
||||||
value: string; // Duplicated from SelectProps
|
|
||||||
onChange: () => void; // Duplicated from SelectProps
|
|
||||||
charts: Chart[]; // New prop
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Inherit and extend
|
|
||||||
interface ChartSelectProps extends SelectProps {
|
|
||||||
charts: Chart[]; // Only new props
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ Don't create ad-hoc type variations
|
|
||||||
interface UserInfo {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Extend existing types (DRY principle)
|
|
||||||
import { User } from 'src/types/bootstrapTypes';
|
|
||||||
type UserDisplayInfo = Pick<User, 'firstName' | 'lastName' | 'email'>;
|
|
||||||
|
|
||||||
// ❌ Don't create overly rigid unions
|
|
||||||
type StrictInput = ExactTypeA | ExactTypeB;
|
|
||||||
|
|
||||||
// ✅ Create flexible interfaces for function parameters
|
|
||||||
interface FlexibleInput {
|
|
||||||
// Focus on what the function actually needs
|
|
||||||
commonProperty: string;
|
|
||||||
optionalVariations?: any; // Allow for legacy variations
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📍 DRY Type Guidelines (WHERE TYPES BELONG)
|
|
||||||
|
|
||||||
### Type Placement Rules
|
|
||||||
**CRITICAL**: Type variations must live close to where they belong, not scattered across files.
|
|
||||||
|
|
||||||
#### ✅ Proper Type Organization
|
|
||||||
```typescript
|
|
||||||
// ❌ Don't create one-off interfaces in utility files
|
|
||||||
// src/utils/datasourceUtils.ts
|
|
||||||
interface DatasourceInput { /* custom interface */ } // Wrong!
|
|
||||||
|
|
||||||
// ✅ Use existing types or extend them in their proper domain
|
|
||||||
// src/utils/datasourceUtils.ts
|
|
||||||
import { IDatasource } from 'src/explore/components/DatasourcePanel';
|
|
||||||
import { QueryEditor } from 'src/SqlLab/types';
|
|
||||||
|
|
||||||
// Create flexible interface that references existing types
|
|
||||||
interface FlexibleDatasourceInput {
|
|
||||||
// Properties that actually exist across variations
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Type Location Hierarchy
|
|
||||||
1. **Domain Types**: `src/{domain}/types.ts` (dashboard, explore, SqlLab)
|
|
||||||
2. **Component Types**: Co-located with components
|
|
||||||
3. **Global Types**: `src/types/` directory
|
|
||||||
4. **Utility Types**: Only when they truly don't belong elsewhere
|
|
||||||
|
|
||||||
#### ✅ DRY Type Patterns
|
|
||||||
```typescript
|
|
||||||
// ✅ Extend existing domain types
|
|
||||||
interface SaveQueryData extends Pick<QueryEditor, 'sql' | 'dbId' | 'catalog'> {
|
|
||||||
columns: ColumnMeta[]; // Add what's needed
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Create flexible interfaces for cross-domain utilities
|
|
||||||
interface CrossDomainInput {
|
|
||||||
// Common properties that exist across different source types
|
|
||||||
name?: string | null; // Accommodate legacy null values
|
|
||||||
// Only include properties the function actually uses
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 PropTypes Auto-Generation (Elegant Approach)
|
|
||||||
|
|
||||||
**IMPORTANT**: Superset has `babel-plugin-typescript-to-proptypes` configured to automatically generate PropTypes from TypeScript interfaces. Use this instead of manual PropTypes duplication!
|
|
||||||
|
|
||||||
### ❌ Manual PropTypes Duplication (Avoid This)
|
|
||||||
```typescript
|
|
||||||
export interface MyComponentProps {
|
|
||||||
title: string;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8+ lines of manual PropTypes duplication 😱
|
|
||||||
const propTypes = PropTypes.shape({
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
count: PropTypes.number,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default propTypes;
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ Auto-Generated PropTypes (Use This)
|
|
||||||
```typescript
|
|
||||||
import { InferProps } from 'prop-types';
|
|
||||||
|
|
||||||
export interface MyComponentProps {
|
|
||||||
title: string;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single validator function - babel plugin auto-generates PropTypes! ✨
|
|
||||||
export default function MyComponentValidator(props: MyComponentProps) {
|
|
||||||
return null; // PropTypes auto-assigned by babel-plugin-typescript-to-proptypes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: For consumers needing PropTypes type inference
|
|
||||||
export type MyComponentPropsInferred = InferProps<typeof MyComponentValidator>;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Migration Pattern for Type-Only Files
|
|
||||||
|
|
||||||
**When migrating type-only files with manual PropTypes:**
|
|
||||||
|
|
||||||
1. **Keep the TypeScript interfaces** (single source of truth)
|
|
||||||
2. **Replace manual PropTypes** with validator function
|
|
||||||
3. **Remove PropTypes imports** and manual shape definitions
|
|
||||||
4. **Add InferProps import** if type inference needed
|
|
||||||
|
|
||||||
**Example Migration:**
|
|
||||||
```typescript
|
|
||||||
// Before: 25+ lines with manual PropTypes duplication
|
|
||||||
export interface AdhocFilterType { /* ... */ }
|
|
||||||
const adhocFilterTypePropTypes = PropTypes.oneOfType([...]);
|
|
||||||
|
|
||||||
// After: 3 lines with auto-generation
|
|
||||||
export interface AdhocFilterType { /* ... */ }
|
|
||||||
export default function AdhocFilterValidator(props: { filter: AdhocFilterType }) {
|
|
||||||
return null; // Auto-generated PropTypes by babel plugin
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component PropTypes Pattern
|
|
||||||
|
|
||||||
**For React components, the babel plugin works automatically:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ComponentProps {
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MyComponent: FC<ComponentProps> = ({ title, onClick }) => {
|
|
||||||
// Component implementation
|
|
||||||
};
|
|
||||||
|
|
||||||
// PropTypes automatically generated by babel plugin - no manual work needed!
|
|
||||||
export default MyComponent;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Auto-Generation Benefits
|
|
||||||
|
|
||||||
- ✅ **Single source of truth**: TypeScript interfaces drive PropTypes
|
|
||||||
- ✅ **No duplication**: Eliminate 15-20 lines of manual PropTypes code
|
|
||||||
- ✅ **Automatic updates**: Changes to TypeScript automatically update PropTypes
|
|
||||||
- ✅ **Type safety**: Compile-time checking ensures PropTypes match interfaces
|
|
||||||
- ✅ **Backward compatibility**: Existing JavaScript components continue working
|
|
||||||
|
|
||||||
### Babel Plugin Configuration
|
|
||||||
|
|
||||||
The plugin is already configured in `babel.config.js`:
|
|
||||||
```javascript
|
|
||||||
['babel-plugin-typescript-to-proptypes', { loose: true }]
|
|
||||||
```
|
|
||||||
|
|
||||||
**No additional setup required** - just use TypeScript interfaces and the plugin handles the rest!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Test File Migration Patterns
|
|
||||||
|
|
||||||
### Test File Priority
|
|
||||||
- **Always migrate test files** alongside production files
|
|
||||||
- **Test files are often leaf nodes** - good starting candidates
|
|
||||||
- **Create tests if missing** - Leverage new TypeScript types for better test coverage
|
|
||||||
|
|
||||||
### Test-Specific Type Patterns
|
|
||||||
```typescript
|
|
||||||
// Mock interfaces for testing
|
|
||||||
interface MockStore {
|
|
||||||
getState: () => Partial<RootState>; // Partial allows minimal mocking
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type-safe mocking for complex objects
|
|
||||||
const mockDashboardInfo: Partial<DashboardInfo> as DashboardInfo = {
|
|
||||||
id: 123,
|
|
||||||
json_metadata: '{}',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sinon stub typing
|
|
||||||
let postStub: sinon.SinonStub;
|
|
||||||
beforeEach(() => {
|
|
||||||
postStub = sinon.stub(SupersetClient, 'post');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use stub reference instead of original method
|
|
||||||
expect(postStub.callCount).toBe(1);
|
|
||||||
expect(postStub.getCall(0).args[0].endpoint).toMatch('/api/');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test Migration Recipe
|
|
||||||
1. **Migrate production file first** (if both need migration)
|
|
||||||
2. **Update test imports** to point to `.ts/.tsx` files
|
|
||||||
3. **Add proper mock typing** using `Partial<T> as T` pattern
|
|
||||||
4. **Fix stub typing** - Use stub references, not original methods
|
|
||||||
5. **Verify all tests pass** with TypeScript compilation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Type Conflict Resolution
|
|
||||||
|
|
||||||
### Multiple Type Definitions Issue
|
|
||||||
**Problem**: Same type name defined in multiple files causes compilation errors.
|
|
||||||
|
|
||||||
**Example**: `DashboardInfo` defined in both:
|
|
||||||
- `src/dashboard/reducers/types.ts` (minimal)
|
|
||||||
- `src/dashboard/components/Header/types.ts` (different shape)
|
|
||||||
- `src/dashboard/types.ts` (complete - used by RootState)
|
|
||||||
|
|
||||||
### Resolution Strategy
|
|
||||||
1. **Identify the authoritative type**:
|
|
||||||
```bash
|
|
||||||
# Find which type is used by RootState/main interfaces
|
|
||||||
grep -r "DashboardInfo" src/dashboard/types.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Use import from authoritative source**:
|
|
||||||
```typescript
|
|
||||||
// ✅ Import from main domain types
|
|
||||||
import { RootState, DashboardInfo } from 'src/dashboard/types';
|
|
||||||
|
|
||||||
// ❌ Don't import from component-specific files
|
|
||||||
import { DashboardInfo } from 'src/dashboard/components/Header/types';
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Mock complex types in tests**:
|
|
||||||
```typescript
|
|
||||||
// For testing - provide minimal required fields
|
|
||||||
const mockInfo: Partial<DashboardInfo> as DashboardInfo = {
|
|
||||||
id: 123,
|
|
||||||
json_metadata: '{}',
|
|
||||||
// Only provide fields actually used in test
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Type Hierarchy Discovery Commands
|
|
||||||
```bash
|
|
||||||
# Find all definitions of a type
|
|
||||||
grep -r "interface.*TypeName\|type.*TypeName" src/
|
|
||||||
|
|
||||||
# Find import usage patterns
|
|
||||||
grep -r "import.*TypeName" src/
|
|
||||||
|
|
||||||
# Check what RootState uses
|
|
||||||
grep -A 10 -B 10 "TypeName" src/*/types.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent Constraints (CRITICAL)
|
|
||||||
|
|
||||||
1. **Use git mv** - Run `git mv file.js file.ts` to preserve git history, but NO `git commit`
|
|
||||||
2. **NO global import changes** - Don't update imports across codebase
|
|
||||||
3. **Type files OK** - Can modify existing type files to improve/align types
|
|
||||||
4. **Single-File TypeScript Validation** (CRITICAL) - tsc has known issues with multi-file compilation:
|
|
||||||
- **Core Issue**: TypeScript's `tsc` has documented problems validating multiple files simultaneously in complex projects
|
|
||||||
- **Solution**: ALWAYS validate files one at a time using individual `tsc` calls
|
|
||||||
- **Command Pattern**: `cd superset-frontend && npx tscw --noEmit --allowJs --composite false --project tsconfig.json {single-file-path}`
|
|
||||||
- **Why**: Multi-file validation can produce false positives, miss real errors, and conflict during parallel agent execution
|
|
||||||
5. **Downstream Impact Validation** (CRITICAL) - Your migration affects calling sites:
|
|
||||||
- **Find downstream files**: `find superset-frontend/src -name "*.tsx" -o -name "*.ts" | xargs grep -l "your-core-filename" 2>/dev/null || echo "No files found"`
|
|
||||||
- **Validate each downstream file individually**: `cd superset-frontend && npx tscw --noEmit --allowJs --composite false --project tsconfig.json {each-downstream-file}`
|
|
||||||
- **Fix type mismatches** you introduced in calling sites
|
|
||||||
- **NEVER ignore downstream errors** - they indicate your types don't match reality
|
|
||||||
6. **Avoid Project-Wide Validation During Migration**:
|
|
||||||
- **NEVER use `npm run type`** during parallel agent execution - produces unreliable results
|
|
||||||
- **Single-file validation is authoritative** - trust individual file checks over project-wide scans
|
|
||||||
6. **ESLint validation** - Run `npm run eslint -- --fix {file}` for each migrated file to auto-fix formatting/linting issues
|
|
||||||
6. Zero `any` types - use proper TypeScript types
|
|
||||||
7. Search existing types before creating new ones
|
|
||||||
8. Follow patterns from this guide
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Report Format
|
|
||||||
|
|
||||||
```
|
|
||||||
SUCCESS: Atomic Migration of {core-filename}
|
|
||||||
|
|
||||||
## Files Migrated (Atomic Unit)
|
|
||||||
- Core: {core-filename} → {core-filename.ts/tsx}
|
|
||||||
- Tests: {list-of-test-files} → {list-of-test-files.ts/tsx} OR "CREATED: {basename}.test.ts"
|
|
||||||
- Mocks: {list-of-mock-files} → {list-of-mock-files.ts}
|
|
||||||
- Type files modified: {list-of-type-files}
|
|
||||||
|
|
||||||
## Types Created/Improved
|
|
||||||
- {TypeName}: {location} ({scope}) - {rationale}
|
|
||||||
- {ExistingType}: enhanced in {location} - {improvement-description}
|
|
||||||
|
|
||||||
## Documentation Recommendations
|
|
||||||
- ADD_TO_DIRECTORY: {TypeName} - {reason}
|
|
||||||
- NO_DOCUMENTATION: {TypeName} - {reason}
|
|
||||||
|
|
||||||
## Quality Validation
|
|
||||||
- **Single-File TypeScript Validation**: ✅ PASS - Core files individually validated
|
|
||||||
- Core file: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {core-file}`
|
|
||||||
- Test files: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {test-file}` (if exists)
|
|
||||||
- **Downstream Impact Check**: ✅ PASS - Found {N} files importing this module, all validate individually
|
|
||||||
- Downstream files: {list-of-files-that-import-your-module}
|
|
||||||
- Individual validation: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {each-downstream-file}`
|
|
||||||
- **ESLint validation**: ✅ PASS (using `npm run eslint -- --fix {files}` to auto-fix formatting)
|
|
||||||
- **Zero any types**: ✅ PASS
|
|
||||||
- **Local imports resolved**: ✅ PASS
|
|
||||||
- **Functionality preserved**: ✅ PASS
|
|
||||||
- **Tests pass** (if test file): ✅ PASS
|
|
||||||
- **Follow-up action required**: {YES/NO}
|
|
||||||
|
|
||||||
## Validation Strategy Notes
|
|
||||||
- **Single-file approach used**: Avoided multi-file tsc validation due to known TypeScript compilation issues
|
|
||||||
- **Project-wide validation skipped**: `npm run type` not used during parallel migration to prevent false positives
|
|
||||||
|
|
||||||
## Migration Learnings
|
|
||||||
- Type conflicts encountered: {describe any multiple type definitions}
|
|
||||||
- Mock patterns used: {describe test mocking approaches}
|
|
||||||
- Import hierarchy decisions: {note authoritative type sources used}
|
|
||||||
- PropTypes strategy: {AUTO_GENERATED via babel plugin | MANUAL_DUPLICATION_REMOVED | N/A}
|
|
||||||
|
|
||||||
## Improvement Suggestions for Documentation
|
|
||||||
- AGENT.md enhancement: {suggest additions to migration guide}
|
|
||||||
- Common pattern identified: {note reusable patterns for future migrations}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependency Block Report Format
|
|
||||||
|
|
||||||
```
|
|
||||||
DEPENDENCY_BLOCK: Cannot migrate {filename}
|
|
||||||
|
|
||||||
## Blocking Dependencies
|
|
||||||
- {path}: {type} - {usage} - {priority}
|
|
||||||
|
|
||||||
## Impact Analysis
|
|
||||||
- Estimated types: {number}
|
|
||||||
- Expected locations: {list}
|
|
||||||
- Cross-domain: {YES/NO}
|
|
||||||
|
|
||||||
## Recommended Order
|
|
||||||
{ordered-list}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Quick Reference
|
|
||||||
|
|
||||||
**Type Utilities:**
|
|
||||||
- `Record<K, V>` - Object with specific key/value types
|
|
||||||
- `Partial<T>` - All properties optional
|
|
||||||
- `Pick<T, K>` - Subset of properties
|
|
||||||
- `Omit<T, K>` - Exclude specific properties
|
|
||||||
- `NonNullable<T>` - Exclude null/undefined
|
|
||||||
|
|
||||||
**Event Types:**
|
|
||||||
- `MouseEvent<HTMLButtonElement>`
|
|
||||||
- `ChangeEvent<HTMLInputElement>`
|
|
||||||
- `FormEvent<HTMLFormElement>`
|
|
||||||
|
|
||||||
**React Types:**
|
|
||||||
- `FC<Props>` - Functional component
|
|
||||||
- `ReactNode` - Any renderable content
|
|
||||||
- `CSSProperties` - Style objects
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Remember:** Every type should add value and clarity. The goal is meaningful type safety that catches bugs and improves developer experience.
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
# JS-to-TS Coordinator Workflow
|
|
||||||
|
|
||||||
**Role:** Strategic migration coordination - select leaf-node files, trigger agents, review results, handle integration, manage dependencies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Core File Selection Strategy
|
|
||||||
|
|
||||||
**Target ONLY Core Files**: Coordinators identify core files (production code), agents handle related tests/mocks atomically.
|
|
||||||
|
|
||||||
**File Analysis Commands**:
|
|
||||||
```bash
|
|
||||||
# Find CORE files with no JS/JSX dependencies (exclude tests/mocks) - SIZE PRIORITIZED
|
|
||||||
find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | xargs wc -l | sort -n | head -20
|
|
||||||
|
|
||||||
# Alternative: Get file sizes in lines with paths
|
|
||||||
find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | while read file; do
|
|
||||||
lines=$(wc -l < "$file")
|
|
||||||
echo "$lines $file"
|
|
||||||
done | sort -n | head -20
|
|
||||||
|
|
||||||
# Check dependencies for core files only (start with smallest)
|
|
||||||
for file in <core-files-sorted-by-size>; do
|
|
||||||
echo "=== $file ($(wc -l < "$file") lines) ==="
|
|
||||||
grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" "$file" || echo "✅ LEAF CANDIDATE"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Identify heavily imported files (migrate last)
|
|
||||||
grep -r "from.*utils/common" superset-frontend/src/ | wc -l
|
|
||||||
|
|
||||||
# Quick leaf analysis with size priority
|
|
||||||
find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | head -30 | while read file; do
|
|
||||||
deps=$(grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" "$file" | wc -l)
|
|
||||||
lines=$(wc -l < "$file")
|
|
||||||
if [ "$deps" -eq 0 ]; then
|
|
||||||
echo "✅ LEAF: $lines lines - $file"
|
|
||||||
fi
|
|
||||||
done | sort -n
|
|
||||||
```
|
|
||||||
|
|
||||||
**Priority Order** (Smallest files first for easier wins):
|
|
||||||
1. **Small leaf files** (<50 lines) - No JS/JSX imports, quick TypeScript conversion
|
|
||||||
2. **Medium leaf files** (50-200 lines) - Self-contained utilities and helpers
|
|
||||||
3. **Small dependency files** (<100 lines) - Import only already-migrated files
|
|
||||||
4. **Larger components** (200+ lines) - Complex but well-contained functionality
|
|
||||||
5. **Core foundational files** (utils/common.js, controls.jsx) - migrate last regardless of size
|
|
||||||
|
|
||||||
**Size-First Benefits**:
|
|
||||||
- Faster completion builds momentum
|
|
||||||
- Earlier validation of migration patterns
|
|
||||||
- Easier rollback if issues arise
|
|
||||||
- Better success rate for agent learning
|
|
||||||
|
|
||||||
**Migration Unit**: Each agent call migrates:
|
|
||||||
- 1 core file (primary target)
|
|
||||||
- All related `*.test.js/jsx` files
|
|
||||||
- All related `*.mock.js` files
|
|
||||||
- All related `__mocks__/` files
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Task Creation & Agent Control
|
|
||||||
|
|
||||||
### Task Triggering
|
|
||||||
When triggering the `/js-to-ts` command:
|
|
||||||
- **Task Title**: Use the core filename as the task title (e.g., "DebouncedMessageQueue.js migration", "hostNamesConfig.js migration")
|
|
||||||
- **Task Description**: Include the full relative path to help agent locate the file
|
|
||||||
- **Reference**: Point agent to [AGENT.md](./AGENT.md) for technical instructions
|
|
||||||
|
|
||||||
### Post-Processing Workflow
|
|
||||||
After each agent completes:
|
|
||||||
|
|
||||||
1. **Review Agent Report**: Always read and analyze the complete agent report
|
|
||||||
2. **Share Summary**: Provide user with key highlights from agent's work:
|
|
||||||
- Files migrated (core + tests/mocks)
|
|
||||||
- Types created or improved
|
|
||||||
- Any validation issues or coordinator actions needed
|
|
||||||
3. **Quality Assessment**: Evaluate agent's TypeScript implementation against criteria:
|
|
||||||
- ✅ **Type Usage**: Proper types used, no `any` types
|
|
||||||
- ✅ **Type Filing**: Types placed in correct hierarchy (component → feature → domain → global)
|
|
||||||
- ✅ **Side Effects**: No unintended changes to other files
|
|
||||||
- ✅ **Import Alignment**: Proper .ts/.tsx import extensions
|
|
||||||
4. **Integration Decision**:
|
|
||||||
- **COMMIT**: If agent work is complete and high quality
|
|
||||||
- **FIX & COMMIT**: If minor issues need coordinator fixes
|
|
||||||
- **ROLLBACK**: If major issues require complete rework
|
|
||||||
5. **Next Action**: Ask user preference - commit this work or trigger next migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Integration Decision Framework
|
|
||||||
|
|
||||||
**Automatic Integration** ✅:
|
|
||||||
- `npm run type` passes without errors
|
|
||||||
- Agent created clean TypeScript with proper types
|
|
||||||
- Types appropriately filed in hierarchy
|
|
||||||
|
|
||||||
**Coordinator Integration** (Fix Side-Effects) 🔧:
|
|
||||||
- `npm run type` fails BUT agent's work is high quality
|
|
||||||
- Good type usage, proper patterns, well-organized
|
|
||||||
- Side-effects are manageable TypeScript compilation errors
|
|
||||||
- **Coordinator Action**: Integrate the change, then fix global compilation issues
|
|
||||||
|
|
||||||
**Rollback Only** ❌:
|
|
||||||
- Agent introduced `any` types or poor type choices
|
|
||||||
- Types poorly organized or conflicting with existing patterns
|
|
||||||
- Fundamental approach issues requiring complete rework
|
|
||||||
|
|
||||||
**Integration Process**:
|
|
||||||
1. **Review**: Agent already used `git mv` to preserve history
|
|
||||||
2. **Fix Side-Effects**: Update dependent files with proper import extensions
|
|
||||||
3. **Resolve Types**: Fix any cascading type issues across codebase
|
|
||||||
4. **Validate**: Ensure `npm run type` passes after fixes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Common Integration Patterns
|
|
||||||
|
|
||||||
**Common Side-Effects (Expect These)**:
|
|
||||||
- **Type import conflicts**: Multiple definitions of same type name
|
|
||||||
- **Mock object typing**: Tests need complete type satisfaction
|
|
||||||
- **Stub method references**: Use stub vars instead of original methods
|
|
||||||
|
|
||||||
**Coordinator Fixes (Standard Process)**:
|
|
||||||
1. **Import Resolution**:
|
|
||||||
```bash
|
|
||||||
# Find authoritative type source
|
|
||||||
grep -r "TypeName" src/*/types.ts
|
|
||||||
# Import from domain types (src/dashboard/types.ts) not component types
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Test Mock Completion**:
|
|
||||||
```typescript
|
|
||||||
// Use Partial<T> as T pattern for minimal mocking
|
|
||||||
const mockDashboard: Partial<DashboardInfo> as DashboardInfo = {
|
|
||||||
id: 123,
|
|
||||||
json_metadata: '{}',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Stub Reference Fixes**:
|
|
||||||
```typescript
|
|
||||||
// ✅ Use stub variable
|
|
||||||
expect(postStub.callCount).toBe(1);
|
|
||||||
// ❌ Don't use original method
|
|
||||||
expect(SupersetClient.post.callCount).toBe(1);
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Validation Commands**:
|
|
||||||
```bash
|
|
||||||
npm run type # TypeScript compilation
|
|
||||||
npm test -- filename # Test functionality
|
|
||||||
git status # Should show rename, not add/delete
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. File Categories for Planning
|
|
||||||
|
|
||||||
### Leaf Files (Start Here)
|
|
||||||
**Self-contained files with minimal JS/JSX dependencies**:
|
|
||||||
- Test files (80 files) - Usually only import the file being tested
|
|
||||||
- Utility files without internal dependencies
|
|
||||||
- Components importing only external libraries
|
|
||||||
|
|
||||||
### Heavily Imported Files (Migrate Last)
|
|
||||||
**Core files that many others depend on**:
|
|
||||||
- `utils/common.js` - Core utility functions
|
|
||||||
- `utils/reducerUtils.js` - Redux helpers
|
|
||||||
- `@superset-ui/core` equivalent files
|
|
||||||
- Major state management files (`explore/store.js`, `dashboard/actions/`)
|
|
||||||
|
|
||||||
### Complex Components (Middle Priority)
|
|
||||||
**Large files requiring careful type analysis**:
|
|
||||||
- `components/Datasource/DatasourceEditor.jsx` (1,809 lines)
|
|
||||||
- `explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx` (1,031 lines)
|
|
||||||
- `explore/components/ExploreViewContainer/index.jsx` (911 lines)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Success Metrics & Continuous Improvement
|
|
||||||
|
|
||||||
**Per-File Gates**:
|
|
||||||
- ✅ `npm run type` passes after each migration
|
|
||||||
- ✅ Zero `any` types introduced
|
|
||||||
- ✅ All imports properly typed
|
|
||||||
- ✅ Types filed in correct hierarchy
|
|
||||||
|
|
||||||
**Linear Scheduling**:
|
|
||||||
When agents report `DEPENDENCY_BLOCK`:
|
|
||||||
- Queue dependencies in linear order
|
|
||||||
- Process one file at a time to avoid conflicts
|
|
||||||
- Handle cascading type changes between files
|
|
||||||
|
|
||||||
**After Each Migration**:
|
|
||||||
1. **Update guides** with new patterns discovered
|
|
||||||
2. **Document coordinator fixes** that become common
|
|
||||||
3. **Enhance agent instructions** based on recurring issues
|
|
||||||
4. **Track success metrics** - automatic vs coordinator integration rates
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# JavaScript to TypeScript Migration Project
|
|
||||||
|
|
||||||
Progressive migration of 219 JS/JSX files to TypeScript in Apache Superset frontend.
|
|
||||||
|
|
||||||
## 📁 Project Documentation
|
|
||||||
|
|
||||||
- **[AGENT.md](./AGENT.md)** - Complete technical migration guide for agents (includes type reference, patterns, validation)
|
|
||||||
- **[COORDINATOR.md](./COORDINATOR.md)** - Strategic workflow for coordinators (file selection, task management, integration)
|
|
||||||
|
|
||||||
## 🎯 Quick Start
|
|
||||||
|
|
||||||
**For Agents:** Read [AGENT.md](./AGENT.md) for complete migration instructions
|
|
||||||
**For Coordinators:** Read [COORDINATOR.md](./COORDINATOR.md) for workflow and [AGENT.md](./AGENT.md) for supervision
|
|
||||||
|
|
||||||
**Command:** `/js-to-ts <filename>` - See [../../commands/js-to-ts.md](../../commands/js-to-ts.md)
|
|
||||||
|
|
||||||
## 📊 Migration Progress
|
|
||||||
|
|
||||||
**Scope**: 219 files total (112 JS + 107 JSX)
|
|
||||||
- Production files: 139 (63%)
|
|
||||||
- Test files: 80 (37%)
|
|
||||||
|
|
||||||
**Strategy**: Leaf-first migration with dependency-aware coordination
|
|
||||||
|
|
||||||
### Completed Migrations ✅
|
|
||||||
|
|
||||||
1. **roundDecimal** - `plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.js`
|
|
||||||
- Migrated core + test files
|
|
||||||
- Added proper TypeScript function signature with optional precision parameter
|
|
||||||
- All tests pass
|
|
||||||
|
|
||||||
2. **timeGrainSqlaAnimationOverrides** - `src/explore/controlPanels/timeGrainSqlaAnimationOverrides.js`
|
|
||||||
- Migrated to TypeScript with ControlPanelState and Dataset types
|
|
||||||
- Added TimeGrainOverrideState interface for return type
|
|
||||||
- Used type guards for safe property access
|
|
||||||
|
|
||||||
3. **DebouncedMessageQueue** - `src/utils/DebouncedMessageQueue.js`
|
|
||||||
- Migrated to TypeScript with proper generics
|
|
||||||
- Created DebouncedMessageQueueOptions interface
|
|
||||||
- **CREATED test file** with 4 comprehensive test cases
|
|
||||||
- Excellent class property typing with private/readonly modifiers
|
|
||||||
|
|
||||||
**Files Migrated**: 3/219 (1.4%)
|
|
||||||
**Tests Created**: 2 (roundDecimal had existing, DebouncedMessageQueue created)
|
|
||||||
|
|
||||||
### Next Candidates (Leaf Nodes) 🎯
|
|
||||||
|
|
||||||
**Identified leaf files with no JS/JSX dependencies:**
|
|
||||||
- `src/utils/hostNamesConfig.js` - Domain configuration utility
|
|
||||||
- `src/explore/controlPanels/Separator.js` - Control panel configuration
|
|
||||||
- `src/middleware/loggerMiddleware.js` - Logging middleware
|
|
||||||
|
|
||||||
**Migration Quality**: All completed migrations have:
|
|
||||||
- ✅ Zero `any` types
|
|
||||||
- ✅ Proper TypeScript compilation
|
|
||||||
- ✅ ESLint validation passed
|
|
||||||
- ✅ Test coverage (created where missing)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Success Metrics
|
|
||||||
|
|
||||||
**Per-File Gates**:
|
|
||||||
- ✅ `npm run type` passes after each migration
|
|
||||||
- ✅ Zero `any` types introduced
|
|
||||||
- ✅ All imports properly typed
|
|
||||||
- ✅ Types filed in correct hierarchy
|
|
||||||
|
|
||||||
**Overall Progress**:
|
|
||||||
- **Automatic Integration Rate**: 100% (3/3 migrations required no coordinator fixes)
|
|
||||||
- **Test Coverage**: Improved (1 new test file created)
|
|
||||||
- **Type Safety**: Enhanced with proper interfaces and generics
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This is a claudette-managed progressive refactor. All documentation and coordination resources are organized under `.claude/projects/js-to-ts/`*
|
|
||||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -1 +1 @@
|
|||||||
../AGENTS.md
|
../LLMS.md
|
||||||
70
.github/workflows/bashlib.sh
vendored
70
.github/workflows/bashlib.sh
vendored
@@ -182,76 +182,6 @@ cypress-run-all() {
|
|||||||
kill $flaskProcessId
|
kill $flaskProcessId
|
||||||
}
|
}
|
||||||
|
|
||||||
playwright-install() {
|
|
||||||
cd "$GITHUB_WORKSPACE/superset-frontend"
|
|
||||||
|
|
||||||
say "::group::Install Playwright browsers"
|
|
||||||
npx playwright install --with-deps chromium
|
|
||||||
# Create output directories for test results and debugging
|
|
||||||
mkdir -p playwright-results
|
|
||||||
mkdir -p test-results
|
|
||||||
say "::endgroup::"
|
|
||||||
}
|
|
||||||
|
|
||||||
playwright-run() {
|
|
||||||
local APP_ROOT=$1
|
|
||||||
|
|
||||||
# Start Flask from the project root (same as Cypress)
|
|
||||||
cd "$GITHUB_WORKSPACE"
|
|
||||||
local flasklog="${HOME}/flask-playwright.log"
|
|
||||||
local port=8081
|
|
||||||
PLAYWRIGHT_BASE_URL="http://localhost:${port}"
|
|
||||||
if [ -n "$APP_ROOT" ]; then
|
|
||||||
export SUPERSET_APP_ROOT=$APP_ROOT
|
|
||||||
PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL}${APP_ROOT}/
|
|
||||||
fi
|
|
||||||
export PLAYWRIGHT_BASE_URL
|
|
||||||
|
|
||||||
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
|
|
||||||
local flaskProcessId=$!
|
|
||||||
|
|
||||||
# Ensure cleanup on exit
|
|
||||||
trap "kill $flaskProcessId 2>/dev/null || true" EXIT
|
|
||||||
|
|
||||||
# Wait for server to be ready with health check
|
|
||||||
local timeout=60
|
|
||||||
say "Waiting for Flask server to start on port $port..."
|
|
||||||
while [ $timeout -gt 0 ]; do
|
|
||||||
if curl -f ${PLAYWRIGHT_BASE_URL}/health >/dev/null 2>&1; then
|
|
||||||
say "Flask server is ready"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
timeout=$((timeout - 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ $timeout -eq 0 ]; then
|
|
||||||
echo "::error::Flask server failed to start within 60 seconds"
|
|
||||||
echo "::group::Flask startup log"
|
|
||||||
cat "$flasklog"
|
|
||||||
echo "::endgroup::"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Change to frontend directory for Playwright execution
|
|
||||||
cd "$GITHUB_WORKSPACE/superset-frontend"
|
|
||||||
|
|
||||||
say "::group::Run Playwright tests"
|
|
||||||
echo "Running Playwright with baseURL: ${PLAYWRIGHT_BASE_URL}"
|
|
||||||
npx playwright test auth/login --reporter=github --output=playwright-results
|
|
||||||
local status=$?
|
|
||||||
say "::endgroup::"
|
|
||||||
|
|
||||||
# After job is done, print out Flask log for debugging
|
|
||||||
echo "::group::Flask log for Playwright run"
|
|
||||||
cat "$flasklog"
|
|
||||||
echo "::endgroup::"
|
|
||||||
# make sure the program exits
|
|
||||||
kill $flaskProcessId
|
|
||||||
|
|
||||||
return $status
|
|
||||||
}
|
|
||||||
|
|
||||||
eyes-storybook-dependencies() {
|
eyes-storybook-dependencies() {
|
||||||
say "::group::install eyes-storyook dependencies"
|
say "::group::install eyes-storyook dependencies"
|
||||||
sudo apt-get update -y && sudo apt-get -y install gconf-service ca-certificates libxshmfence-dev fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libglib2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils libappindicator1
|
sudo apt-get update -y && sudo apt-get -y install gconf-service ca-certificates libxshmfence-dev fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libglib2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils libappindicator1
|
||||||
|
|||||||
22
.github/workflows/showtime-trigger.yml
vendored
22
.github/workflows/showtime-trigger.yml
vendored
@@ -61,8 +61,17 @@ jobs:
|
|||||||
console.log(`📊 Permission level for ${actor}: ${permission.permission}`);
|
console.log(`📊 Permission level for ${actor}: ${permission.permission}`);
|
||||||
const authorized = ['write', 'admin'].includes(permission.permission);
|
const authorized = ['write', 'admin'].includes(permission.permission);
|
||||||
|
|
||||||
// If this is a synchronize event from unauthorized user, check if Showtime is active and set blocked label
|
if (!authorized) {
|
||||||
if (!authorized && context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
|
console.log(`🚨 Unauthorized user ${actor} - skipping all operations`);
|
||||||
|
core.setOutput('authorized', 'false');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Authorized maintainer: ${actor}`);
|
||||||
|
core.setOutput('authorized', 'true');
|
||||||
|
|
||||||
|
// If this is a synchronize event, check if Showtime is active and set blocked label
|
||||||
|
if (context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
|
||||||
console.log(`🔒 Synchronize event detected - checking if Showtime is active`);
|
console.log(`🔒 Synchronize event detected - checking if Showtime is active`);
|
||||||
|
|
||||||
// Check if PR has any circus tent labels (Showtime is in use)
|
// Check if PR has any circus tent labels (Showtime is in use)
|
||||||
@@ -90,15 +99,6 @@ jobs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorized) {
|
|
||||||
console.log(`🚨 Unauthorized user ${actor} - skipping all operations`);
|
|
||||||
core.setOutput('authorized', 'false');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Authorized maintainer: ${actor}`);
|
|
||||||
core.setOutput('authorized', 'true');
|
|
||||||
|
|
||||||
- name: Install Superset Showtime
|
- name: Install Superset Showtime
|
||||||
if: steps.auth.outputs.authorized == 'true'
|
if: steps.auth.outputs.authorized == 'true'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/superset-frontend.yml
vendored
2
.github/workflows/superset-frontend.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
|||||||
- name: tsc
|
- name: tsc
|
||||||
run: |
|
run: |
|
||||||
docker run --rm $TAG bash -c \
|
docker run --rm $TAG bash -c \
|
||||||
"npm run plugins:build && npm run type"
|
"npm run type"
|
||||||
|
|
||||||
validate-frontend:
|
validate-frontend:
|
||||||
needs: frontend-build
|
needs: frontend-build
|
||||||
|
|||||||
141
.github/workflows/superset-playwright.yml
vendored
141
.github/workflows/superset-playwright.yml
vendored
@@ -1,141 +0,0 @@
|
|||||||
name: Playwright E2E Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
- "[0-9].[0-9]*"
|
|
||||||
pull_request:
|
|
||||||
types: [synchronize, opened, reopened, ready_for_review]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
ref:
|
|
||||||
description: 'The branch or tag to checkout'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
pr_id:
|
|
||||||
description: 'The pull request ID to checkout'
|
|
||||||
required: false
|
|
||||||
default: ''
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
playwright-tests:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
# Allow workflow to succeed even if tests fail during shadow mode
|
|
||||||
continue-on-error: true
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
browser: ["chromium"]
|
|
||||||
app_root: ["", "/app/prefix"]
|
|
||||||
env:
|
|
||||||
SUPERSET_ENV: development
|
|
||||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
|
||||||
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
|
|
||||||
PYTHONPATH: ${{ github.workspace }}
|
|
||||||
REDIS_PORT: 16379
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: superset
|
|
||||||
POSTGRES_PASSWORD: superset
|
|
||||||
ports:
|
|
||||||
- 15432:5432
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
ports:
|
|
||||||
- 16379:6379
|
|
||||||
steps:
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# Conditional checkout based on context (same as Cypress workflow)
|
|
||||||
- name: Checkout for push or pull_request event
|
|
||||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
submodules: recursive
|
|
||||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
|
||||||
- name: Checkout using ref (workflow_dispatch)
|
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
ref: ${{ github.event.inputs.ref }}
|
|
||||||
submodules: recursive
|
|
||||||
- name: Checkout using PR ID (workflow_dispatch)
|
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
|
||||||
submodules: recursive
|
|
||||||
# -------------------------------------------------------
|
|
||||||
- name: Check for file changes
|
|
||||||
id: check
|
|
||||||
uses: ./.github/actions/change-detector/
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Setup Python
|
|
||||||
uses: ./.github/actions/setup-backend/
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
- name: Setup postgres
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
with:
|
|
||||||
run: setup-postgres
|
|
||||||
- name: Import test data
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
with:
|
|
||||||
run: testdata
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version-file: './superset-frontend/.nvmrc'
|
|
||||||
- name: Install npm dependencies
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
with:
|
|
||||||
run: npm-install
|
|
||||||
- name: Build javascript packages
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
with:
|
|
||||||
run: build-instrumented-assets
|
|
||||||
- name: Install Playwright
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
with:
|
|
||||||
run: playwright-install
|
|
||||||
- name: Run Playwright
|
|
||||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
|
||||||
uses: ./.github/actions/cached-dependencies
|
|
||||||
env:
|
|
||||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
|
||||||
with:
|
|
||||||
run: playwright-run ${{ matrix.app_root }}
|
|
||||||
- name: Set safe app root
|
|
||||||
if: failure()
|
|
||||||
id: set-safe-app-root
|
|
||||||
run: |
|
|
||||||
APP_ROOT="${{ matrix.app_root }}"
|
|
||||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
|
||||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
|
||||||
- name: Upload Playwright Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ github.workspace }}/superset-frontend/playwright-results/
|
|
||||||
${{ github.workspace }}/superset-frontend/test-results/
|
|
||||||
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
|
||||||
@@ -82,7 +82,6 @@ intro_header.txt
|
|||||||
|
|
||||||
# for LLMs
|
# for LLMs
|
||||||
llm-context.md
|
llm-context.md
|
||||||
AGENTS.md
|
|
||||||
LLMS.md
|
LLMS.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
CURSOR.md
|
CURSOR.md
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
|
|||||||
|
|
||||||
### Testing Strategy Migration
|
### Testing Strategy Migration
|
||||||
- **Prefer unit tests** over integration tests
|
- **Prefer unit tests** over integration tests
|
||||||
- **Prefer integration tests** over end-to-end tests
|
- **Prefer integration tests** over Cypress end-to-end tests
|
||||||
- **Use Playwright for E2E tests** - Migrating from Cypress
|
- **Cypress is last resort** - Actively moving away from Cypress
|
||||||
- **Cypress is deprecated** - Will be removed once migration is completed
|
|
||||||
- **Use Jest + React Testing Library** for component testing
|
- **Use Jest + React Testing Library** for component testing
|
||||||
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
|
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
|
||||||
|
|
||||||
@@ -68,11 +67,7 @@ superset/
|
|||||||
|
|
||||||
### Apache License Headers
|
### Apache License Headers
|
||||||
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
|
||||||
- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
- **LLM instruction files are excluded** - Files like LLMS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
|
||||||
|
|
||||||
### Code Comments
|
|
||||||
- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
|
|
||||||
- **Write timeless comments** - Comments should remain accurate regardless of when they're read
|
|
||||||
|
|
||||||
## Documentation Requirements
|
## Documentation Requirements
|
||||||
|
|
||||||
@@ -102,17 +97,6 @@ superset/
|
|||||||
- **`selectOption()`** - Select component helper
|
- **`selectOption()`** - Select component helper
|
||||||
- **React Testing Library** - NO Enzyme (removed)
|
- **React Testing Library** - NO Enzyme (removed)
|
||||||
|
|
||||||
### Test Structure Guidelines
|
|
||||||
- **Use `test()` instead of `describe()` and `it()`** - Follow the [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principle
|
|
||||||
- **Why**: Reduces unnecessary nesting, improves test isolation, and makes tests more readable
|
|
||||||
- **Pattern**: Write flat test files with descriptive test names that fully describe what's being tested
|
|
||||||
- **Example**: Instead of nested `describe('Component', () => { it('should render', ...) })`, use `test('Component renders correctly', ...)`
|
|
||||||
- **Benefits**:
|
|
||||||
- Each test stands alone with a clear, searchable name
|
|
||||||
- Easier to run individual tests
|
|
||||||
- Forces you to write more descriptive test names
|
|
||||||
- Reduces cognitive overhead from nested context switching
|
|
||||||
|
|
||||||
### Test Database Patterns
|
### Test Database Patterns
|
||||||
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
|
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
|
||||||
- **API tests**: Update expected columns when adding new model fields
|
- **API tests**: Update expected columns when adding new model fields
|
||||||
@@ -123,18 +107,6 @@ superset/
|
|||||||
npm run test # All tests
|
npm run test # All tests
|
||||||
npm run test -- filename.test.tsx # Single file
|
npm run test -- filename.test.tsx # Single file
|
||||||
|
|
||||||
# E2E Tests (Playwright - NEW)
|
|
||||||
npm run playwright:test # All Playwright tests
|
|
||||||
npm run playwright:ui # Interactive UI mode
|
|
||||||
npm run playwright:headed # See browser during tests
|
|
||||||
npx playwright test tests/auth/login.spec.ts # Single file
|
|
||||||
npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
|
|
||||||
|
|
||||||
# E2E Tests (Cypress - DEPRECATED)
|
|
||||||
cd superset-frontend/cypress-base
|
|
||||||
npm run cypress-run-chrome # All Cypress tests (headless)
|
|
||||||
npm run cypress-debug # Interactive Cypress UI
|
|
||||||
|
|
||||||
# Backend
|
# Backend
|
||||||
pytest # All tests
|
pytest # All tests
|
||||||
pytest tests/unit_tests/specific_test.py # Single file
|
pytest tests/unit_tests/specific_test.py # Single file
|
||||||
@@ -28,7 +28,6 @@ x-superset-image: &superset-image apachesuperset.docker.scarf.sh/apache/superset
|
|||||||
x-superset-volumes:
|
x-superset-volumes:
|
||||||
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
||||||
- ./docker:/app/docker
|
- ./docker:/app/docker
|
||||||
- ./superset-core:/app/superset-core
|
|
||||||
- superset_home:/app/superset_home
|
- superset_home:/app/superset_home
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|||||||
@@ -29,11 +29,9 @@ x-superset-volumes: &superset-volumes
|
|||||||
# /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
# /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
||||||
- ./docker:/app/docker
|
- ./docker:/app/docker
|
||||||
- ./superset:/app/superset
|
- ./superset:/app/superset
|
||||||
- ./superset-core:/app/superset-core
|
|
||||||
- ./superset-frontend:/app/superset-frontend
|
- ./superset-frontend:/app/superset-frontend
|
||||||
- superset_home:/app/superset_home
|
- superset_home:/app/superset_home
|
||||||
- ./tests:/app/tests
|
- ./tests:/app/tests
|
||||||
- superset_data:/app/data
|
|
||||||
x-common-build: &common-build
|
x-common-build: &common-build
|
||||||
context: .
|
context: .
|
||||||
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
||||||
@@ -276,5 +274,3 @@ volumes:
|
|||||||
external: false
|
external: false
|
||||||
redis:
|
redis:
|
||||||
external: false
|
external: false
|
||||||
superset_data:
|
|
||||||
external: false
|
|
||||||
|
|||||||
@@ -21,16 +21,9 @@ set -eo pipefail
|
|||||||
# Make python interactive
|
# Make python interactive
|
||||||
if [ "$DEV_MODE" == "true" ]; then
|
if [ "$DEV_MODE" == "true" ]; then
|
||||||
if [ "$(whoami)" = "root" ] && command -v uv > /dev/null 2>&1; then
|
if [ "$(whoami)" = "root" ] && command -v uv > /dev/null 2>&1; then
|
||||||
# Always ensure superset-core is available
|
|
||||||
echo "Installing superset-core in editable mode"
|
|
||||||
uv pip install --no-deps -e /app/superset-core
|
|
||||||
|
|
||||||
# Only reinstall the main app for non-worker processes
|
|
||||||
if [ "$1" != "worker" ] && [ "$1" != "beat" ]; then
|
|
||||||
echo "Reinstalling the app in editable mode"
|
echo "Reinstalling the app in editable mode"
|
||||||
uv pip install -e .
|
uv pip install -e .
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt"
|
REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt"
|
||||||
PORT=${PORT:-8088}
|
PORT=${PORT:-8088}
|
||||||
@@ -41,8 +34,7 @@ if [ "$CYPRESS_CONFIG" == "true" ]; then
|
|||||||
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
|
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
|
||||||
PORT=8081
|
PORT=8081
|
||||||
fi
|
fi
|
||||||
# Skip postgres requirements installation for workers to avoid conflicts
|
if [[ "$DATABASE_DIALECT" == postgres* ]] && [ "$(whoami)" = "root" ]; then
|
||||||
if [[ "$DATABASE_DIALECT" == postgres* ]] && [ "$(whoami)" = "root" ] && [ "$1" != "worker" ] && [ "$1" != "beat" ]; then
|
|
||||||
# older images may not have the postgres dev requirements installed
|
# older images may not have the postgres dev requirements installed
|
||||||
echo "Installing postgres requirements"
|
echo "Installing postgres requirements"
|
||||||
if command -v uv > /dev/null 2>&1; then
|
if command -v uv > /dev/null 2>&1; then
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ Screenshots will be taken but no messages actually sent as long as `ALERT_REPORT
|
|||||||
#### In your `Dockerfile`
|
#### In your `Dockerfile`
|
||||||
|
|
||||||
You'll need to extend the Superset image to include a headless browser. Your options include:
|
You'll need to extend the Superset image to include a headless browser. Your options include:
|
||||||
- Use Playwright with Chrome: this is the recommended approach as of version 4.1.x or greater. A working example of a Dockerfile that installs these tools is provided under "Building your own production Docker image" on the [Docker Builds](/docs/installation/docker-builds#building-your-own-production-docker-image) page. Read the code comments there as you'll also need to change a feature flag in your config.
|
- Use Playwright with Chrome: this is the recommended approach as of version >=4.1.x. A working example of a Dockerfile that installs these tools is provided under “Building your own production Docker image” on the [Docker Builds](/docs/installation/docker-builds#building-your-own-production-docker-image) page. Read the code comments there as you'll also need to change a feature flag in your config.
|
||||||
- Use Firefox: you'll need to install geckodriver and Firefox.
|
- Use Firefox: you'll need to install geckodriver and Firefox.
|
||||||
- Use Chrome without Playwright: you'll need to install Chrome and set the value of `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
|
- Use Chrome without Playwright: you'll need to install Chrome and set the value of `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
|
||||||
|
|
||||||
In Superset versions prior to 4.1, users installed Firefox or Chrome and that was documented here.
|
In Superset versions <=4.0x, users installed Firefox or Chrome and that was documented here.
|
||||||
|
|
||||||
Only the worker container needs the browser.
|
Only the worker container needs the browser.
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ are compatible with Superset.
|
|||||||
| [IBM Netezza Performance Server](/docs/configuration/databases#ibm-netezza-performance-server) | `pip install nzalchemy` | `netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
| [IBM Netezza Performance Server](/docs/configuration/databases#ibm-netezza-performance-server) | `pip install nzalchemy` | `netezza+nzpy://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||||
| [MySQL](/docs/configuration/databases#mysql) | `pip install mysqlclient` | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
| [MySQL](/docs/configuration/databases#mysql) | `pip install mysqlclient` | `mysql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||||
| [OceanBase](/docs/configuration/databases#oceanbase) | `pip install oceanbase_py` | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
| [OceanBase](/docs/configuration/databases#oceanbase) | `pip install oceanbase_py` | `oceanbase://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||||
| [Oracle](/docs/configuration/databases#oracle) | `pip install oracledb` | `oracle://<username>:<password>@<hostname>:<port>` |
|
| [Oracle](/docs/configuration/databases#oracle) | `pip install cx_Oracle` | `oracle://<username>:<password>@<hostname>:<port>` |
|
||||||
| [Parseable](/docs/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
|
| [Parseable](/docs/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
|
||||||
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
|
||||||
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
|
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
|
||||||
|
|||||||
@@ -10,15 +10,8 @@ version: 1
|
|||||||
## Jinja Templates
|
## Jinja Templates
|
||||||
|
|
||||||
SQL Lab and Explore supports [Jinja templating](https://jinja.palletsprojects.com/en/2.11.x/) in queries.
|
SQL Lab and Explore supports [Jinja templating](https://jinja.palletsprojects.com/en/2.11.x/) in queries.
|
||||||
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/configuration/configuring-superset#feature-flags) needs to be enabled in `superset_config.py`.
|
To enable templating, the `ENABLE_TEMPLATE_PROCESSING` [feature flag](/docs/configuration/configuring-superset#feature-flags) needs to be enabled in
|
||||||
|
`superset_config.py`. When templating is enabled, python code can be embedded in virtual datasets and
|
||||||
> #### ⚠️ Security Warning
|
|
||||||
>
|
|
||||||
> While powerful, this feature executes template code on the server. Within the Superset security model, this is **intended functionality**, as users with permissions to edit charts and virtual datasets are considered **trusted users**.
|
|
||||||
>
|
|
||||||
> If you grant these permissions to untrusted users, this feature can be exploited as a **Server-Side Template Injection (SSTI)** vulnerability. Do not enable `ENABLE_TEMPLATE_PROCESSING` unless you fully understand and accept the associated security risks.
|
|
||||||
|
|
||||||
When templating is enabled, python code can be embedded in virtual datasets and
|
|
||||||
in Custom SQL in the filter and metric controls in Explore. By default, the following variables are
|
in Custom SQL in the filter and metric controls in Explore. By default, the following variables are
|
||||||
made available in the Jinja context:
|
made available in the Jinja context:
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ version: 1
|
|||||||
# Theming Superset
|
# Theming Superset
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
`apache-superset>=6.0`
|
apache-superset>=6.0
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Superset now rides on **Ant Design v5's token-based theming**.
|
Superset now rides on **Ant Design v5's token-based theming**.
|
||||||
|
|||||||
@@ -631,7 +631,7 @@ can find all of the workflows and other assets under the `.github/` folder. This
|
|||||||
|
|
||||||
- running the backend unit test suites (`tests/`)
|
- running the backend unit test suites (`tests/`)
|
||||||
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
|
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
|
||||||
- running our Playwright end-to-end tests (`superset-frontend/playwright/`) and legacy Cypress tests (`superset-frontend/cypress-base/`)
|
- running our Cypress end-to-end tests (`superset-frontend/cypress-base/`)
|
||||||
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
|
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
|
||||||
- checking for all sorts of other rules conventions
|
- checking for all sorts of other rules conventions
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ Committers may also update title to reflect the issue/PR content if the author-p
|
|||||||
|
|
||||||
If the PR passes CI tests and does not have any `need:` labels, it is ready for review, add label `review` and/or `design-review`.
|
If the PR passes CI tests and does not have any `need:` labels, it is ready for review, add label `review` and/or `design-review`.
|
||||||
|
|
||||||
If an issue/PR has been inactive for at least 30 days, it will be closed. If it does not have any status label, add `inactive`.
|
If an issue/PR has been inactive for >=30 days, it will be closed. If it does not have any status label, add `inactive`.
|
||||||
|
|
||||||
When creating a PR, if you're aiming to have it included in a specific release, please tag it with the version label. For example, to have a PR considered for inclusion in Superset 1.1 use the label `v1.1`.
|
When creating a PR, if you're aiming to have it included in a specific release, please tag it with the version label. For example, to have a PR considered for inclusion in Superset 1.1 use the label `v1.1`.
|
||||||
|
|
||||||
|
|||||||
@@ -225,57 +225,21 @@ npm run test -- path/to/file.js
|
|||||||
|
|
||||||
### E2E Integration Testing
|
### E2E Integration Testing
|
||||||
|
|
||||||
**Note: We are migrating from Cypress to Playwright. Use Playwright for new tests.**
|
For E2E testing, we recommend that you use a `docker compose` backend
|
||||||
|
|
||||||
#### Playwright (Recommended - NEW)
|
|
||||||
|
|
||||||
For E2E testing with Playwright, use the same `docker compose` backend:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CYPRESS_CONFIG=true docker compose up --build
|
CYPRESS_CONFIG=true docker compose up --build
|
||||||
```
|
```
|
||||||
|
`docker compose` will get to work and expose a Cypress-ready Superset app.
|
||||||
|
This app uses a different database schema (`superset_cypress`) to keep it isolated from
|
||||||
|
your other dev environment(s), a specific set of examples, and a set of configurations that
|
||||||
|
aligns with the expectations within the end-to-end tests. Also note that it's served on a
|
||||||
|
different port than the default port for the backend (`8088`).
|
||||||
|
|
||||||
The backend setup is identical - this exposes a test-ready Superset app on port 8081 with isolated database schema (`superset_cypress`), test data, and configurations.
|
Now in another terminal, let's get ready to execute some Cypress commands. First, tell cypress
|
||||||
|
to connect to the Cypress-ready Superset backend.
|
||||||
|
|
||||||
Now in another terminal, run Playwright tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Navigate to frontend directory (Playwright config is here)
|
|
||||||
cd superset-frontend
|
|
||||||
|
|
||||||
# Run all Playwright tests
|
|
||||||
npm run playwright:test
|
|
||||||
# or: npx playwright test
|
|
||||||
|
|
||||||
# Run with interactive UI for debugging
|
|
||||||
npm run playwright:ui
|
|
||||||
# or: npx playwright test --ui
|
|
||||||
|
|
||||||
# Run in headed mode (see browser)
|
|
||||||
npm run playwright:headed
|
|
||||||
# or: npx playwright test --headed
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
npx playwright test tests/auth/login.spec.ts
|
|
||||||
|
|
||||||
# Run with debug mode (step through tests)
|
|
||||||
npm run playwright:debug tests/auth/login.spec.ts
|
|
||||||
# or: npx playwright test --debug tests/auth/login.spec.ts
|
|
||||||
|
|
||||||
# Generate test report
|
|
||||||
npx playwright show-report
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Configuration is in `superset-frontend/playwright.config.ts`. Base URL is automatically set to `http://localhost:8088` but will use `PLAYWRIGHT_BASE_URL` if provided.
|
|
||||||
|
|
||||||
#### Cypress (DEPRECATED - will be removed in Phase 5)
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
Cypress is being phased out in favor of Playwright. Use Playwright for all new tests.
|
|
||||||
:::
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set base URL for Cypress
|
|
||||||
CYPRESS_BASE_URL=http://localhost:8081
|
CYPRESS_BASE_URL=http://localhost:8081
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ maintainers:
|
|||||||
- name: craig-rueda
|
- name: craig-rueda
|
||||||
email: craig@craigrueda.com
|
email: craig@craigrueda.com
|
||||||
url: https://github.com/craig-rueda
|
url: https://github.com/craig-rueda
|
||||||
version: 0.15.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
version: 0.15.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: 13.4.4
|
version: 13.4.4
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
|||||||
|
|
||||||
# superset
|
# superset
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||||
|
|
||||||
@@ -203,7 +203,6 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
|
|||||||
| supersetNode.connections.db_name | string | `"superset"` | |
|
| supersetNode.connections.db_name | string | `"superset"` | |
|
||||||
| supersetNode.connections.db_pass | string | `"superset"` | |
|
| supersetNode.connections.db_pass | string | `"superset"` | |
|
||||||
| supersetNode.connections.db_port | string | `"5432"` | |
|
| supersetNode.connections.db_port | string | `"5432"` | |
|
||||||
| supersetNode.connections.db_type | string | `"postgresql"` | Database type for Superset metadata (Supported types: "postgresql", "mysql") |
|
|
||||||
| supersetNode.connections.db_user | string | `"superset"` | |
|
| supersetNode.connections.db_user | string | `"superset"` | |
|
||||||
| supersetNode.connections.redis_cache_db | string | `"1"` | |
|
| supersetNode.connections.redis_cache_db | string | `"1"` | |
|
||||||
| supersetNode.connections.redis_celery_db | string | `"0"` | |
|
| supersetNode.connections.redis_celery_db | string | `"0"` | |
|
||||||
|
|||||||
@@ -96,18 +96,7 @@ CACHE_CONFIG = {
|
|||||||
}
|
}
|
||||||
DATA_CACHE_CONFIG = CACHE_CONFIG
|
DATA_CACHE_CONFIG = CACHE_CONFIG
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{env('DB_USER')}:{env('DB_PASS')}@{env('DB_HOST')}:{env('DB_PORT')}/{env('DB_NAME')}"
|
||||||
if os.getenv("SQLALCHEMY_DATABASE_URI"):
|
|
||||||
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")
|
|
||||||
else:
|
|
||||||
{{- if eq .Values.supersetNode.connections.db_type "postgresql" }}
|
|
||||||
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
|
|
||||||
{{- else if eq .Values.supersetNode.connections.db_type "mysql" }}
|
|
||||||
SQLALCHEMY_DATABASE_URI = f"mysql+mysqldb://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
|
|
||||||
{{- else }}
|
|
||||||
{{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
|
||||||
class CeleryConfig:
|
class CeleryConfig:
|
||||||
|
|||||||
@@ -289,8 +289,6 @@ supersetNode:
|
|||||||
enabled: false
|
enabled: false
|
||||||
ssl_cert_reqs: CERT_NONE
|
ssl_cert_reqs: CERT_NONE
|
||||||
# You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false
|
# You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false
|
||||||
# -- Database type for Superset metadata (Supported types: "postgresql", "mysql")
|
|
||||||
db_type: "postgresql"
|
|
||||||
db_host: "{{ .Release.Name }}-postgresql"
|
db_host: "{{ .Release.Name }}-postgresql"
|
||||||
db_port: "5432"
|
db_port: "5432"
|
||||||
db_user: superset
|
db_user: superset
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ dependencies = [
|
|||||||
"packaging",
|
"packaging",
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||||
"pandas[excel]>=2.0.3, <2.2",
|
"pandas[excel]>=2.0.3, <2.1",
|
||||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||||
# --------------------------
|
# --------------------------
|
||||||
"parsedatetime",
|
"parsedatetime",
|
||||||
@@ -84,7 +84,6 @@ dependencies = [
|
|||||||
"pgsanity",
|
"pgsanity",
|
||||||
"Pillow>=11.0.0, <12",
|
"Pillow>=11.0.0, <12",
|
||||||
"polyline>=2.0.0, <3.0",
|
"polyline>=2.0.0, <3.0",
|
||||||
"pydantic>=2.8.0",
|
|
||||||
"pyparsing>=3.0.6, <4",
|
"pyparsing>=3.0.6, <4",
|
||||||
"python-dateutil",
|
"python-dateutil",
|
||||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||||
@@ -100,7 +99,7 @@ dependencies = [
|
|||||||
"slack_sdk>=3.19.0, <4",
|
"slack_sdk>=3.19.0, <4",
|
||||||
"sqlalchemy>=1.4, <2",
|
"sqlalchemy>=1.4, <2",
|
||||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||||
"sqlglot>=27.15.2, <28",
|
"sqlglot>=27.3.0, <28",
|
||||||
# newer pandas needs 0.9+
|
# newer pandas needs 0.9+
|
||||||
"tabulate>=0.9.0, <1.0",
|
"tabulate>=0.9.0, <1.0",
|
||||||
"typing-extensions>=4, <5",
|
"typing-extensions>=4, <5",
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ alembic==1.15.2
|
|||||||
# via flask-migrate
|
# via flask-migrate
|
||||||
amqp==5.3.1
|
amqp==5.3.1
|
||||||
# via kombu
|
# via kombu
|
||||||
annotated-types==0.7.0
|
|
||||||
# via pydantic
|
|
||||||
apispec==6.6.1
|
apispec==6.6.1
|
||||||
# via
|
# via
|
||||||
# -r requirements/base.in
|
# -r requirements/base.in
|
||||||
@@ -117,9 +115,7 @@ flask==2.3.3
|
|||||||
# flask-sqlalchemy
|
# flask-sqlalchemy
|
||||||
# flask-wtf
|
# flask-wtf
|
||||||
flask-appbuilder==5.0.0
|
flask-appbuilder==5.0.0
|
||||||
# via
|
# via apache-superset (pyproject.toml)
|
||||||
# apache-superset (pyproject.toml)
|
|
||||||
# apache-superset-core
|
|
||||||
flask-babel==3.1.0
|
flask-babel==3.1.0
|
||||||
# via flask-appbuilder
|
# via flask-appbuilder
|
||||||
flask-caching==2.3.1
|
flask-caching==2.3.1
|
||||||
@@ -160,7 +156,6 @@ greenlet==3.1.1
|
|||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# sqlalchemy
|
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
@@ -267,7 +262,7 @@ packaging==25.0
|
|||||||
# limits
|
# limits
|
||||||
# marshmallow
|
# marshmallow
|
||||||
# shillelagh
|
# shillelagh
|
||||||
pandas==2.1.4
|
pandas==2.0.3
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
paramiko==3.5.1
|
paramiko==3.5.1
|
||||||
# via
|
# via
|
||||||
@@ -299,10 +294,6 @@ pyasn1-modules==0.4.2
|
|||||||
# via google-auth
|
# via google-auth
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.11.7
|
|
||||||
# via apache-superset (pyproject.toml)
|
|
||||||
pydantic-core==2.33.2
|
|
||||||
# via pydantic
|
|
||||||
pygments==2.19.1
|
pygments==2.19.1
|
||||||
# via rich
|
# via rich
|
||||||
pyjwt==2.10.1
|
pyjwt==2.10.1
|
||||||
@@ -395,7 +386,7 @@ sqlalchemy-utils==0.38.3
|
|||||||
# via
|
# via
|
||||||
# apache-superset (pyproject.toml)
|
# apache-superset (pyproject.toml)
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
sqlglot==27.15.2
|
sqlglot==27.3.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
sshtunnel==0.4.0
|
sshtunnel==0.4.0
|
||||||
# via apache-superset (pyproject.toml)
|
# via apache-superset (pyproject.toml)
|
||||||
@@ -413,15 +404,10 @@ typing-extensions==4.14.0
|
|||||||
# alembic
|
# alembic
|
||||||
# cattrs
|
# cattrs
|
||||||
# limits
|
# limits
|
||||||
# pydantic
|
|
||||||
# pydantic-core
|
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
# referencing
|
# referencing
|
||||||
# selenium
|
# selenium
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# typing-inspection
|
|
||||||
typing-inspection==0.4.1
|
|
||||||
# via pydantic
|
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
# via
|
# via
|
||||||
# kombu
|
# kombu
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ amqp==5.3.1
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# kombu
|
# kombu
|
||||||
annotated-types==0.7.0
|
|
||||||
# via
|
|
||||||
# -c requirements/base-constraint.txt
|
|
||||||
# pydantic
|
|
||||||
apispec==6.6.1
|
apispec==6.6.1
|
||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
@@ -216,7 +212,6 @@ flask-appbuilder==5.0.0
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
# apache-superset-core
|
|
||||||
flask-babel==3.1.0
|
flask-babel==3.1.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
@@ -331,7 +326,6 @@ greenlet==3.1.1
|
|||||||
# apache-superset
|
# apache-superset
|
||||||
# gevent
|
# gevent
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# sqlalchemy
|
|
||||||
grpcio==1.71.0
|
grpcio==1.71.0
|
||||||
# via
|
# via
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -537,7 +531,7 @@ packaging==25.0
|
|||||||
# pytest
|
# pytest
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# sqlalchemy-bigquery
|
# sqlalchemy-bigquery
|
||||||
pandas==2.1.4
|
pandas==2.0.3
|
||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -637,14 +631,6 @@ pycparser==2.22
|
|||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# cffi
|
# cffi
|
||||||
pydantic==2.11.7
|
|
||||||
# via
|
|
||||||
# -c requirements/base-constraint.txt
|
|
||||||
# apache-superset
|
|
||||||
pydantic-core==2.33.2
|
|
||||||
# via
|
|
||||||
# -c requirements/base-constraint.txt
|
|
||||||
# pydantic
|
|
||||||
pydata-google-auth==1.9.0
|
pydata-google-auth==1.9.0
|
||||||
# via pandas-gbq
|
# via pandas-gbq
|
||||||
pydruid==0.6.9
|
pydruid==0.6.9
|
||||||
@@ -848,7 +834,7 @@ sqlalchemy-utils==0.38.3
|
|||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
# flask-appbuilder
|
# flask-appbuilder
|
||||||
sqlglot==27.15.2
|
sqlglot==27.3.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
# apache-superset
|
# apache-superset
|
||||||
@@ -888,17 +874,10 @@ typing-extensions==4.14.0
|
|||||||
# apache-superset
|
# apache-superset
|
||||||
# cattrs
|
# cattrs
|
||||||
# limits
|
# limits
|
||||||
# pydantic
|
|
||||||
# pydantic-core
|
|
||||||
# pyopenssl
|
# pyopenssl
|
||||||
# referencing
|
# referencing
|
||||||
# selenium
|
# selenium
|
||||||
# shillelagh
|
# shillelagh
|
||||||
# typing-inspection
|
|
||||||
typing-inspection==0.4.1
|
|
||||||
# via
|
|
||||||
# -c requirements/base-constraint.txt
|
|
||||||
# pydantic
|
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
# via
|
# via
|
||||||
# -c requirements/base-constraint.txt
|
# -c requirements/base-constraint.txt
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ module.exports = {
|
|||||||
'plugin:react-hooks/recommended',
|
'plugin:react-hooks/recommended',
|
||||||
'plugin:react-prefer-function-component/recommended',
|
'plugin:react-prefer-function-component/recommended',
|
||||||
'plugin:storybook/recommended',
|
'plugin:storybook/recommended',
|
||||||
'plugin:react-you-might-not-need-an-effect/legacy-recommended',
|
|
||||||
],
|
],
|
||||||
parser: '@babel/eslint-parser',
|
parser: '@babel/eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
@@ -324,7 +323,6 @@ module.exports = {
|
|||||||
'*.stories.tsx',
|
'*.stories.tsx',
|
||||||
'*.stories.jsx',
|
'*.stories.jsx',
|
||||||
'fixtures.*',
|
'fixtures.*',
|
||||||
'playwright/**/*',
|
|
||||||
],
|
],
|
||||||
excludedFiles: 'cypress-base/cypress/**/*',
|
excludedFiles: 'cypress-base/cypress/**/*',
|
||||||
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
|
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
|
||||||
@@ -399,13 +397,6 @@ module.exports = {
|
|||||||
'react/no-void-elements': 0,
|
'react/no-void-elements': 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ['playwright/**/*'],
|
|
||||||
rules: {
|
|
||||||
'import/no-unresolved': 0, // Playwright is not installed in main build
|
|
||||||
'import/no-extraneous-dependencies': 0, // Playwright is not installed in main build
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
// eslint-disable-next-line no-dupe-keys
|
// eslint-disable-next-line no-dupe-keys
|
||||||
rules: {
|
rules: {
|
||||||
@@ -413,6 +404,13 @@ module.exports = {
|
|||||||
'icons/no-fa-icons-usage': 'error',
|
'icons/no-fa-icons-usage': 'error',
|
||||||
'i18n-strings/no-template-vars': ['error', true],
|
'i18n-strings/no-template-vars': ['error', true],
|
||||||
'i18n-strings/sentence-case-buttons': 'error',
|
'i18n-strings/sentence-case-buttons': 'error',
|
||||||
|
camelcase: [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
allow: ['^UNSAFE_'],
|
||||||
|
properties: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
'class-methods-use-this': 0,
|
'class-methods-use-this': 0,
|
||||||
curly: 2,
|
curly: 2,
|
||||||
'func-names': 0,
|
'func-names': 0,
|
||||||
|
|||||||
1
superset-frontend/.gitignore
vendored
1
superset-frontend/.gitignore
vendored
@@ -3,4 +3,3 @@ cypress/screenshots
|
|||||||
cypress/videos
|
cypress/videos
|
||||||
src/temp
|
src/temp
|
||||||
.temp_cache/
|
.temp_cache/
|
||||||
.tsbuildinfo
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ describe('Charts list', () => {
|
|||||||
interceptDelete();
|
interceptDelete();
|
||||||
cy.getBySel('sort-header').contains('Name').click();
|
cy.getBySel('sort-header').contains('Name').click();
|
||||||
|
|
||||||
// Modal closes immediately without this
|
// Modal closes immediatly without this
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
|
|
||||||
cy.getBySel('table-row').eq(0).contains('3 - Sample chart');
|
cy.getBySel('table-row').eq(0).contains('3 - Sample chart');
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
// ***********************************************
|
||||||
|
// Tests for setting controls in the UI
|
||||||
|
// ***********************************************
|
||||||
|
import { interceptChart, setSelectSearchInput } from 'cypress/utils';
|
||||||
|
|
||||||
|
describe('Datasource control', () => {
|
||||||
|
const newMetricName = `abc${Date.now()}`;
|
||||||
|
|
||||||
|
it('should allow edit dataset', () => {
|
||||||
|
interceptChart({ legacy: false }).as('chartData');
|
||||||
|
|
||||||
|
cy.visitChartByName('Num Births Trend');
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
|
cy.get('[data-test="datasource-menu-trigger"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-test="edit-dataset"]').click();
|
||||||
|
|
||||||
|
cy.get('[data-test="edit-dataset-tabs"]').within(() => {
|
||||||
|
cy.contains('Metrics').click();
|
||||||
|
});
|
||||||
|
// create new metric
|
||||||
|
cy.get('[data-test="crud-add-table-item"]', { timeout: 10000 }).click();
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.get('.ant-table-body [data-test="textarea-editable-title-input"]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('.ant-table-body [data-test="textarea-editable-title-input"]')
|
||||||
|
.first()
|
||||||
|
.focus();
|
||||||
|
cy.focused().clear({ force: true });
|
||||||
|
cy.focused().type(`${newMetricName}{enter}`, { force: true });
|
||||||
|
|
||||||
|
cy.get('[data-test="datasource-modal-save"]').click();
|
||||||
|
cy.get('.ant-modal-confirm-btns button').contains('OK').click();
|
||||||
|
// select new metric
|
||||||
|
cy.get('[data-test=metrics]')
|
||||||
|
.contains('Drop columns/metrics here or click')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('input[aria-label="Select saved metrics"]')
|
||||||
|
.should('exist')
|
||||||
|
.then($input => {
|
||||||
|
setSelectSearchInput($input, newMetricName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete metric
|
||||||
|
cy.get('[data-test="datasource-menu-trigger"]').click();
|
||||||
|
cy.get('[data-test="edit-dataset"]').click();
|
||||||
|
cy.get('.ant-modal-content').within(() => {
|
||||||
|
cy.get('[data-test="collection-tab-Metrics"]')
|
||||||
|
.contains('Metrics')
|
||||||
|
.click();
|
||||||
|
});
|
||||||
|
cy.get(`[data-test="textarea-editable-title-input"]`)
|
||||||
|
.contains(newMetricName)
|
||||||
|
.closest('tr')
|
||||||
|
.find('[data-test="crud-delete-icon"]')
|
||||||
|
.click();
|
||||||
|
cy.get('[data-test="datasource-modal-save"]').click();
|
||||||
|
cy.get('.ant-modal-confirm-btns button').contains('OK').click();
|
||||||
|
cy.get('[data-test="metrics"]').contains(newMetricName).should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Color scheme control', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptChart({ legacy: false }).as('chartData');
|
||||||
|
|
||||||
|
cy.visitChartByName('Num Births Trend');
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show color options with and without tooltips', () => {
|
||||||
|
cy.get('#controlSections-tab-CUSTOMIZE').click();
|
||||||
|
cy.get('.ant-select-selection-item .color-scheme-label').contains(
|
||||||
|
'Superset Colors',
|
||||||
|
);
|
||||||
|
cy.get('.ant-select-selection-item .color-scheme-label').trigger(
|
||||||
|
'mouseover',
|
||||||
|
);
|
||||||
|
cy.get('.color-scheme-tooltip').should('be.visible');
|
||||||
|
cy.get('.color-scheme-tooltip').contains('Superset Colors');
|
||||||
|
cy.get('.Control[data-test="color_scheme"]').scrollIntoView();
|
||||||
|
cy.get('.Control[data-test="color_scheme"] input[type="search"]').focus();
|
||||||
|
|
||||||
|
cy.get('.color-scheme-label')
|
||||||
|
.contains('Superset Colors')
|
||||||
|
.trigger('mouseover');
|
||||||
|
|
||||||
|
cy.get('.color-scheme-label')
|
||||||
|
.contains('Superset Colors')
|
||||||
|
.trigger('mouseout');
|
||||||
|
|
||||||
|
cy.focused().type('lyftColors');
|
||||||
|
cy.getBySel('lyftColors').should('exist');
|
||||||
|
cy.getBySel('lyftColors').trigger('mouseover', { force: true });
|
||||||
|
cy.get('.color-scheme-tooltip').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('VizType control', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptChart({ legacy: false }).as('tableChartData');
|
||||||
|
interceptChart({ legacy: false }).as('bigNumberChartData');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can change vizType', () => {
|
||||||
|
cy.visitChartByName('Daily Totals');
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@tableChartData' });
|
||||||
|
|
||||||
|
cy.contains('View all charts').click();
|
||||||
|
|
||||||
|
cy.get('.ant-modal-content').within(() => {
|
||||||
|
cy.get('button').contains('KPI').click(); // change categories
|
||||||
|
cy.get('[role="button"]').contains('Big Number').click();
|
||||||
|
cy.get('button').contains('Select').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
|
cy.verifySliceSuccess({
|
||||||
|
waitAlias: '@bigNumberChartData',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test datatable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
interceptChart({ legacy: false }).as('tableChartData');
|
||||||
|
interceptChart({ legacy: false }).as('lineChartData');
|
||||||
|
cy.visitChartByName('Daily Totals');
|
||||||
|
});
|
||||||
|
it('Data Pane opens and loads results', () => {
|
||||||
|
cy.contains('Results').click();
|
||||||
|
cy.get('[data-test="row-count-label"]').contains('26 rows');
|
||||||
|
cy.get('.ant-empty-description').should('not.exist');
|
||||||
|
});
|
||||||
|
it('Datapane loads view samples', () => {
|
||||||
|
cy.intercept(
|
||||||
|
'**/datasource/samples?force=false&datasource_type=table&datasource_id=*',
|
||||||
|
).as('Samples');
|
||||||
|
cy.contains('Samples').click();
|
||||||
|
cy.wait('@Samples');
|
||||||
|
cy.get('.ant-tabs-tab-active').contains('Samples');
|
||||||
|
cy.get('[data-test="row-count-label"]').contains('1k rows');
|
||||||
|
cy.get('.ant-empty-description').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Groupby control', () => {
|
||||||
|
it('Set groupby', () => {
|
||||||
|
interceptChart({ legacy: false }).as('chartData');
|
||||||
|
|
||||||
|
cy.visitChartByName('Num Births Trend');
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
|
cy.get('[data-test=groupby]')
|
||||||
|
.contains('Drop columns here or click')
|
||||||
|
.click();
|
||||||
|
cy.get('[id="adhoc-metric-edit-tabs-tab-simple"]').click();
|
||||||
|
cy.get('input[aria-label="Column"]').click();
|
||||||
|
cy.get('input[aria-label="Column"]').type('state{enter}');
|
||||||
|
cy.get('[data-test="ColumnEdit#save"]').contains('Save').click();
|
||||||
|
|
||||||
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -33,7 +33,6 @@ module.exports = {
|
|||||||
'^@superset-ui/([^/]+)$': '<rootDir>/node_modules/@superset-ui/$1/src',
|
'^@superset-ui/([^/]+)$': '<rootDir>/node_modules/@superset-ui/$1/src',
|
||||||
// mapping @apache-superset/core to local package
|
// mapping @apache-superset/core to local package
|
||||||
'^@apache-superset/core$': '<rootDir>/packages/superset-core/src',
|
'^@apache-superset/core$': '<rootDir>/packages/superset-core/src',
|
||||||
'^@apache-superset/core/(.*)$': '<rootDir>/packages/superset-core/src/$1',
|
|
||||||
},
|
},
|
||||||
testEnvironment: '<rootDir>/spec/helpers/jsDomWithFetchAPI.ts',
|
testEnvironment: '<rootDir>/spec/helpers/jsDomWithFetchAPI.ts',
|
||||||
modulePathIgnorePatterns: ['<rootDir>/packages/generator-superset'],
|
modulePathIgnorePatterns: ['<rootDir>/packages/generator-superset'],
|
||||||
|
|||||||
161
superset-frontend/package-lock.json
generated
161
superset-frontend/package-lock.json
generated
@@ -54,8 +54,6 @@
|
|||||||
"@visx/scale": "^3.5.0",
|
"@visx/scale": "^3.5.0",
|
||||||
"@visx/tooltip": "^3.0.0",
|
"@visx/tooltip": "^3.0.0",
|
||||||
"@visx/xychart": "^3.5.1",
|
"@visx/xychart": "^3.5.1",
|
||||||
"ag-grid-community": "34.2.0",
|
|
||||||
"ag-grid-react": "34.2.0",
|
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.24.6",
|
||||||
"chrono-node": "^2.7.8",
|
"chrono-node": "^2.7.8",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
@@ -161,7 +159,6 @@
|
|||||||
"@hot-loader/react-dom": "^17.0.2",
|
"@hot-loader/react-dom": "^17.0.2",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||||
"@playwright/test": "^1.49.1",
|
|
||||||
"@storybook/addon-actions": "8.1.11",
|
"@storybook/addon-actions": "8.1.11",
|
||||||
"@storybook/addon-controls": "8.1.11",
|
"@storybook/addon-controls": "8.1.11",
|
||||||
"@storybook/addon-essentials": "8.1.11",
|
"@storybook/addon-essentials": "8.1.11",
|
||||||
@@ -232,7 +229,6 @@
|
|||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.5.1",
|
|
||||||
"eslint-plugin-storybook": "^0.8.0",
|
"eslint-plugin-storybook": "^0.8.0",
|
||||||
"eslint-plugin-testing-library": "^6.4.0",
|
"eslint-plugin-testing-library": "^6.4.0",
|
||||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||||
@@ -8886,9 +8882,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@ndelangen/get-tarball/node_modules/tar-fs": {
|
"node_modules/@ndelangen/get-tarball/node_modules/tar-fs": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10113,22 +10109,6 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"url": "https://opencollective.com/unts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
|
||||||
"version": "1.55.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
|
|
||||||
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright": "1.55.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@pnpm/config.env-replace": {
|
"node_modules/@pnpm/config.env-replace": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
||||||
@@ -18716,27 +18696,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ag-charts-types": {
|
"node_modules/ag-charts-types": {
|
||||||
"version": "12.2.0",
|
"version": "12.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.0.2.tgz",
|
||||||
"integrity": "sha512-d2qQrQirt9wP36YW5HPuOvXsiajyiFnr1CTsoCbs02bavPDz7Lk2jHp64+waM4YKgXb3GN7gafbBI9Qgk33BmQ==",
|
"integrity": "sha512-AWM1Y+XW+9VMmV3AbzdVEnreh/I2C9Pmqpc2iLmtId3Xbvmv7O56DqnuDb9EXjK5uPxmyUerTP+utL13UGcztw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ag-grid-community": {
|
"node_modules/ag-grid-community": {
|
||||||
"version": "34.2.0",
|
"version": "34.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.0.2.tgz",
|
||||||
"integrity": "sha512-peS7THEMYwpIrwLQHmkRxw/TlOnddD/F5A88RqlBxf8j+WqVYRWMOOhU5TqymGcha7z2oZ8IoL9ROl3gvtdEjg==",
|
"integrity": "sha512-hVJp5vrmwHRB10YjfSOVni5YJkO/v+asLjT72S4YnIFSx8lAgyPmByNJgtojk1aJ5h6Up93jTEmGDJeuKiWWLA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ag-charts-types": "12.2.0"
|
"ag-charts-types": "12.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ag-grid-react": {
|
"node_modules/ag-grid-react": {
|
||||||
"version": "34.2.0",
|
"version": "34.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-34.0.2.tgz",
|
||||||
"integrity": "sha512-dLKFw6hz75S0HLuZvtcwjm+gyiI4gXVzHEu7lWNafWAX0mb8DhogEOP5wbzAlsN6iCfi7bK/cgZImZFjenlqwg==",
|
"integrity": "sha512-1KBXkTvwtZiYVlSuDzBkiqfHjZgsATOmpLZdAtdmsCSOOOEWai0F9zHHgBuHfyciAE4nrbQWfojkx8IdnwsKFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ag-grid-community": "34.2.0",
|
"ag-grid-community": "34.0.2",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -25930,36 +25910,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-you-might-not-need-an-effect": {
|
|
||||||
"version": "0.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.5.1.tgz",
|
|
||||||
"integrity": "sha512-Gi2kfHLkXUT3j+IAwgb8TEhY10iMwsdwSsgbIxk98zPpuPW7M52ey9fU1oPZrWUlyekr5eXwUCjeTHekS6Isrw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"eslint-utils": "^3.0.0",
|
|
||||||
"globals": "^16.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": ">=8.40.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-react-you-might-not-need-an-effect/node_modules/globals": {
|
|
||||||
"version": "16.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
|
|
||||||
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-react/node_modules/doctrine": {
|
"node_modules/eslint-plugin-react/node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -26075,25 +26025,6 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-utils": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"eslint-visitor-keys": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/mysticatea"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": ">=5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-visitor-keys": {
|
"node_modules/eslint-visitor-keys": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||||
@@ -45586,53 +45517,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
|
||||||
"version": "1.55.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
|
|
||||||
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.55.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright-core": {
|
|
||||||
"version": "1.55.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
|
|
||||||
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright/node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/png-async": {
|
"node_modules/png-async": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/png-async/-/png-async-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/png-async/-/png-async-0.9.4.tgz",
|
||||||
@@ -60740,7 +60624,7 @@
|
|||||||
},
|
},
|
||||||
"packages/superset-core": {
|
"packages/superset-core": {
|
||||||
"name": "@apache-superset/core",
|
"name": "@apache-superset/core",
|
||||||
"version": "0.0.1-rc5",
|
"version": "0.0.1-rc3",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.26.4",
|
"@babel/cli": "^7.26.4",
|
||||||
@@ -63437,10 +63321,10 @@
|
|||||||
"version": "0.20.3",
|
"version": "0.20.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
@@ -63465,15 +63349,14 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@babel/runtime": "^7.28.2",
|
"@babel/runtime": "^7.28.2",
|
||||||
"@fontsource/fira-code": "^5.2.6",
|
"@fontsource/fira-code": "^5.2.6",
|
||||||
"@fontsource/inter": "^5.2.6",
|
"@fontsource/inter": "^5.2.6",
|
||||||
"@types/json-bigint": "^1.0.4",
|
"@types/json-bigint": "^1.0.4",
|
||||||
"@visx/responsive": "^3.12.0",
|
"@visx/responsive": "^3.12.0",
|
||||||
"ace-builds": "^1.43.1",
|
"ace-builds": "^1.43.1",
|
||||||
"ag-grid-community": "34.2.0",
|
"ag-grid-community": "^34.0.2",
|
||||||
"ag-grid-react": "34.2.0",
|
"ag-grid-react": "34.0.2",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
@@ -65344,8 +65227,6 @@
|
|||||||
"d3-array": "^1.2.4",
|
"d3-array": "^1.2.4",
|
||||||
"d3-color": "^1.4.1",
|
"d3-color": "^1.4.1",
|
||||||
"d3-scale": "^3.0.0",
|
"d3-scale": "^3.0.0",
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"handlebars": "^4.7.8",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
@@ -65512,7 +65393,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@superset-ui/chart-controls": "*",
|
"@superset-ui/chart-controls": "*",
|
||||||
"@superset-ui/core": "*",
|
"@superset-ui/core": "*",
|
||||||
"@testing-library/dom": "^8.20.1",
|
"@testing-library/dom": "^8.20.1",
|
||||||
@@ -65564,7 +65444,6 @@
|
|||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@superset-ui/chart-controls": "*",
|
"@superset-ui/chart-controls": "*",
|
||||||
"@superset-ui/core": "*",
|
"@superset-ui/core": "*",
|
||||||
"echarts": "*",
|
"echarts": "*",
|
||||||
@@ -66742,7 +66621,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@superset-ui/chart-controls": "*",
|
"@superset-ui/chart-controls": "*",
|
||||||
"@superset-ui/core": "*",
|
"@superset-ui/core": "*",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
@@ -67874,7 +67752,6 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@superset-ui/chart-controls": "*",
|
"@superset-ui/chart-controls": "*",
|
||||||
"@superset-ui/core": "*",
|
"@superset-ui/core": "*",
|
||||||
"@testing-library/dom": "^8.20.1",
|
"@testing-library/dom": "^8.20.1",
|
||||||
|
|||||||
@@ -63,11 +63,6 @@
|
|||||||
"plugins:release-conventional": "npm run prune && npm run plugins:build && lerna publish --conventional-commits --create-release github --yes",
|
"plugins:release-conventional": "npm run prune && npm run plugins:build && lerna publish --conventional-commits --create-release github --yes",
|
||||||
"plugins:release-from-tag": "npm run prune && npm run plugins:build && lerna publish from-package --yes",
|
"plugins:release-from-tag": "npm run prune && npm run plugins:build && lerna publish from-package --yes",
|
||||||
"plugins:storybook": "cd packages/superset-ui-demo && npm run storybook",
|
"plugins:storybook": "cd packages/superset-ui-demo && npm run storybook",
|
||||||
"playwright:test": "playwright test",
|
|
||||||
"playwright:ui": "playwright test --ui",
|
|
||||||
"playwright:headed": "playwright test --headed",
|
|
||||||
"playwright:debug": "playwright test --debug",
|
|
||||||
"playwright:report": "playwright show-report",
|
|
||||||
"prettier": "npm run _prettier -- --write",
|
"prettier": "npm run _prettier -- --write",
|
||||||
"prettier-check": "npm run _prettier -- --check",
|
"prettier-check": "npm run _prettier -- --check",
|
||||||
"prod": "npm run build",
|
"prod": "npm run build",
|
||||||
@@ -127,8 +122,6 @@
|
|||||||
"@visx/scale": "^3.5.0",
|
"@visx/scale": "^3.5.0",
|
||||||
"@visx/tooltip": "^3.0.0",
|
"@visx/tooltip": "^3.0.0",
|
||||||
"@visx/xychart": "^3.5.1",
|
"@visx/xychart": "^3.5.1",
|
||||||
"ag-grid-community": "34.2.0",
|
|
||||||
"ag-grid-react": "34.2.0",
|
|
||||||
"antd": "^5.24.6",
|
"antd": "^5.24.6",
|
||||||
"chrono-node": "^2.7.8",
|
"chrono-node": "^2.7.8",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
@@ -234,7 +227,6 @@
|
|||||||
"@hot-loader/react-dom": "^17.0.2",
|
"@hot-loader/react-dom": "^17.0.2",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||||
"@playwright/test": "^1.49.1",
|
|
||||||
"@storybook/addon-actions": "8.1.11",
|
"@storybook/addon-actions": "8.1.11",
|
||||||
"@storybook/addon-controls": "8.1.11",
|
"@storybook/addon-controls": "8.1.11",
|
||||||
"@storybook/addon-essentials": "8.1.11",
|
"@storybook/addon-essentials": "8.1.11",
|
||||||
@@ -305,7 +297,6 @@
|
|||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
"eslint-plugin-react-prefer-function-component": "^3.3.0",
|
||||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.5.1",
|
|
||||||
"eslint-plugin-storybook": "^0.8.0",
|
"eslint-plugin-storybook": "^0.8.0",
|
||||||
"eslint-plugin-testing-library": "^6.4.0",
|
"eslint-plugin-testing-library": "^6.4.0",
|
||||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||||
|
|||||||
@@ -22,6 +22,19 @@ To add the package to Superset, go to the `superset-frontend` subdirectory in yo
|
|||||||
npm i -S ../../<%= packageName %>
|
npm i -S ../../<%= packageName %>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If your Superset plugin exists in the `superset-frontend` directory and you wish to resolve TypeScript errors about `@superset-ui/core` not being resolved correctly, add the following to your `tsconfig.json` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../packages/superset-ui-chart-controls"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../packages/superset-ui-core"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
You may also wish to add the following to the `include` array in `tsconfig.json` to make Superset types available to your plugin:
|
You may also wish to add the following to the `include` array in `tsconfig.json` to make Superset types available to your plugin:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "../..",
|
"allowSyntheticDefaultImports": true,
|
||||||
"outDir": "lib"
|
"declaration": true,
|
||||||
},
|
"declarationDir": "lib",
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"esModuleInterop": true,
|
||||||
"exclude": [
|
"forceConsistentCasingInFileNames": true,
|
||||||
"src/**/*.js",
|
"isolatedModules": false,
|
||||||
"src/**/*.jsx",
|
"jsx": "react",
|
||||||
"src/**/*.test.*",
|
"lib": [
|
||||||
"src/**/*.stories.*"
|
"dom",
|
||||||
|
"esnext"
|
||||||
],
|
],
|
||||||
"references": [
|
"module": "esnext",
|
||||||
{ "path": "../../packages/superset-core" },
|
"moduleResolution": "node",
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
"noEmitOnError": true,
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"pretty": true,
|
||||||
|
"removeComments": false,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2015",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"composite": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["jest"],
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"lib",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"types/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@apache-superset/core",
|
"name": "@apache-superset/core",
|
||||||
"version": "0.0.1-rc5",
|
"version": "0.0.1-rc3",
|
||||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"allowSyntheticDefaultImports": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
"declaration": true,
|
||||||
"baseUrl": "../..",
|
"declarationDir": "lib",
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
|
"strict": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"jsx": "preserve",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "es2020",
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "types/**/*"],
|
"include": ["src/**/*.ts*"],
|
||||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"]
|
"exclude": ["lib"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
"lib"
|
"lib"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { css, styled, t } from '@superset-ui/core';
|
import { css, GenericDataType, styled, t } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import {
|
import {
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
QuestionOutlined,
|
QuestionOutlined,
|
||||||
|
|||||||
@@ -16,11 +16,9 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { useEffect, useState } from 'react';
|
||||||
Popover,
|
import { Popover, type PopoverProps } from '@superset-ui/core/components';
|
||||||
type PopoverProps,
|
import type ReactAce from 'react-ace';
|
||||||
SQLEditor,
|
|
||||||
} from '@superset-ui/core/components';
|
|
||||||
import { CalculatorOutlined } from '@ant-design/icons';
|
import { CalculatorOutlined } from '@ant-design/icons';
|
||||||
import { css, styled, useTheme, t } from '@superset-ui/core';
|
import { css, styled, useTheme, t } from '@superset-ui/core';
|
||||||
|
|
||||||
@@ -37,10 +35,24 @@ const StyledCalculatorIcon = styled(CalculatorOutlined)`
|
|||||||
|
|
||||||
export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [AceEditor, setAceEditor] = useState<typeof ReactAce | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([
|
||||||
|
import('react-ace'),
|
||||||
|
import('ace-builds/src-min-noconflict/mode-sql'),
|
||||||
|
]).then(([reactAceModule]) => {
|
||||||
|
setAceEditor(() => reactAceModule.default);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!AceEditor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
<SQLEditor
|
<AceEditor
|
||||||
|
mode="sql"
|
||||||
value={props.sqlExpression}
|
value={props.sqlExpression}
|
||||||
editorProps={{ $blockScrolling: true }}
|
editorProps={{ $blockScrolling: true }}
|
||||||
setOptions={{
|
setOptions={{
|
||||||
@@ -53,6 +65,7 @@ export const SQLPopover = (props: PopoverProps & { sqlExpression: string }) => {
|
|||||||
wrapEnabled
|
wrapEnabled
|
||||||
style={{
|
style={{
|
||||||
border: `1px solid ${theme.colorBorder}`,
|
border: `1px solid ${theme.colorBorder}`,
|
||||||
|
background: theme.colorPrimaryBg,
|
||||||
maxWidth: theme.sizeUnit * 100,
|
maxWidth: theme.sizeUnit * 100,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,8 +16,13 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { DTTM_ALIAS, QueryColumn, QueryMode, t } from '@superset-ui/core';
|
import {
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
DTTM_ALIAS,
|
||||||
|
GenericDataType,
|
||||||
|
QueryColumn,
|
||||||
|
QueryMode,
|
||||||
|
t,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
import { ColumnMeta, SortSeriesData, SortSeriesType } from './types';
|
||||||
|
|
||||||
export const DEFAULT_MAX_ROW = 100000;
|
export const DEFAULT_MAX_ROW = 100000;
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { DatasourceType } from '@superset-ui/core';
|
import { DatasourceType, GenericDataType } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { Dataset } from './types';
|
import { Dataset } from './types';
|
||||||
|
|
||||||
export const TestDataset: Dataset = {
|
export const TestDataset: Dataset = {
|
||||||
|
|||||||
@@ -20,32 +20,20 @@ import { t } from '@superset-ui/core';
|
|||||||
import { ControlPanelSectionConfig } from '../types';
|
import { ControlPanelSectionConfig } from '../types';
|
||||||
|
|
||||||
export const matrixifyEnableSection: ControlPanelSectionConfig = {
|
export const matrixifyEnableSection: ControlPanelSectionConfig = {
|
||||||
label: t('Matrixify'),
|
label: t('Enable matrixify'),
|
||||||
expanded: true,
|
expanded: true,
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
name: 'matrixify_enable_horizontal_layout',
|
name: 'matrixify_enabled',
|
||||||
config: {
|
config: {
|
||||||
type: 'CheckboxControl',
|
type: 'CheckboxControl',
|
||||||
label: t('Enable horizontal layout (columns)'),
|
label: t('Enable matrixify'),
|
||||||
|
default: false,
|
||||||
|
renderTrigger: true,
|
||||||
description: t(
|
description: t(
|
||||||
'Create matrix columns by placing charts side-by-side',
|
'Transform this chart into a matrix/grid of charts based on dimensions or metrics',
|
||||||
),
|
),
|
||||||
default: false,
|
|
||||||
renderTrigger: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: 'matrixify_enable_vertical_layout',
|
|
||||||
config: {
|
|
||||||
type: 'CheckboxControl',
|
|
||||||
label: t('Enable vertical layout (rows)'),
|
|
||||||
description: t('Create matrix rows by stacking charts vertically'),
|
|
||||||
default: false,
|
|
||||||
renderTrigger: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -54,11 +42,9 @@ export const matrixifyEnableSection: ControlPanelSectionConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const matrixifySection: ControlPanelSectionConfig = {
|
export const matrixifySection: ControlPanelSectionConfig = {
|
||||||
label: t('Cell layout & styling'),
|
label: t('Matrixify'),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
|
||||||
controls?.matrixify_enable_vertical_layout?.value === true ||
|
|
||||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -119,10 +105,9 @@ export const matrixifySection: ControlPanelSectionConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const matrixifyRowSection: ControlPanelSectionConfig = {
|
export const matrixifyRowSection: ControlPanelSectionConfig = {
|
||||||
label: t('Vertical layout (rows)'),
|
label: t('Vertical layout'),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
|
||||||
controls?.matrixify_enable_vertical_layout?.value === true,
|
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
['matrixify_show_row_labels'],
|
['matrixify_show_row_labels'],
|
||||||
['matrixify_mode_rows'],
|
['matrixify_mode_rows'],
|
||||||
@@ -133,14 +118,13 @@ export const matrixifyRowSection: ControlPanelSectionConfig = {
|
|||||||
['matrixify_topn_metric_rows'],
|
['matrixify_topn_metric_rows'],
|
||||||
['matrixify_topn_order_rows'],
|
['matrixify_topn_order_rows'],
|
||||||
],
|
],
|
||||||
tabOverride: 'matrixify',
|
tabOverride: 'data',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matrixifyColumnSection: ControlPanelSectionConfig = {
|
export const matrixifyColumnSection: ControlPanelSectionConfig = {
|
||||||
label: t('Horizontal layout (columns)'),
|
label: t('Horizontal layout'),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) => controls?.matrixify_enabled?.value === true,
|
||||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
|
||||||
controlSetRows: [
|
controlSetRows: [
|
||||||
['matrixify_show_column_headers'],
|
['matrixify_show_column_headers'],
|
||||||
['matrixify_mode_columns'],
|
['matrixify_mode_columns'],
|
||||||
@@ -151,5 +135,5 @@ export const matrixifyColumnSection: ControlPanelSectionConfig = {
|
|||||||
['matrixify_topn_metric_columns'],
|
['matrixify_topn_metric_columns'],
|
||||||
['matrixify_topn_order_columns'],
|
['matrixify_topn_order_columns'],
|
||||||
],
|
],
|
||||||
tabOverride: 'matrixify',
|
tabOverride: 'data',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
import {
|
import {
|
||||||
ContributionType,
|
ContributionType,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
|
GenericDataType,
|
||||||
getColumnLabel,
|
getColumnLabel,
|
||||||
getMetricLabel,
|
getMetricLabel,
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
QueryFormMetric,
|
QueryFormMetric,
|
||||||
t,
|
t,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import {
|
import {
|
||||||
ControlPanelState,
|
ControlPanelState,
|
||||||
ControlState,
|
ControlState,
|
||||||
|
|||||||
@@ -17,8 +17,12 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { QueryColumn, t, validateNonEmpty } from '@superset-ui/core';
|
import {
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
GenericDataType,
|
||||||
|
QueryColumn,
|
||||||
|
t,
|
||||||
|
validateNonEmpty,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ExtraControlProps,
|
ExtraControlProps,
|
||||||
SharedControlConfig,
|
SharedControlConfig,
|
||||||
|
|||||||
@@ -18,49 +18,21 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { t, validateNonEmpty } from '@superset-ui/core';
|
import { t } from '@superset-ui/core';
|
||||||
import { SharedControlConfig } from '../types';
|
import { SharedControlConfig } from '../types';
|
||||||
import { dndAdhocMetricControl } from './dndControls';
|
import { dndAdhocMetricControl } from './dndControls';
|
||||||
import { defineSavedMetrics } from '../utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matrixify control definitions
|
* Matrixify control definitions
|
||||||
* Controls for transforming charts into matrix/grid layouts
|
* Controls for transforming charts into matrix/grid layouts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Utility function to check if matrixify controls should be visible
|
|
||||||
const isMatrixifyVisible = (
|
|
||||||
controls: any,
|
|
||||||
axis: 'rows' | 'columns',
|
|
||||||
mode?: 'metrics' | 'dimensions',
|
|
||||||
selectionMode?: 'members' | 'topn',
|
|
||||||
) => {
|
|
||||||
const layoutControl = `matrixify_enable_${axis === 'rows' ? 'vertical' : 'horizontal'}_layout`;
|
|
||||||
const modeControl = `matrixify_mode_${axis}`;
|
|
||||||
const selectionModeControl = `matrixify_dimension_selection_mode_${axis}`;
|
|
||||||
|
|
||||||
const isLayoutEnabled = controls?.[layoutControl]?.value === true;
|
|
||||||
|
|
||||||
if (!isLayoutEnabled) return false;
|
|
||||||
|
|
||||||
if (mode) {
|
|
||||||
const isModeMatch = controls?.[modeControl]?.value === mode;
|
|
||||||
if (!isModeMatch) return false;
|
|
||||||
|
|
||||||
if (selectionMode && mode === 'dimensions') {
|
|
||||||
return controls?.[selectionModeControl]?.value === selectionMode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize the controls object that will be populated dynamically
|
// Initialize the controls object that will be populated dynamically
|
||||||
const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||||
|
|
||||||
// Dynamically add axis-specific controls (rows and columns)
|
// Dynamically add axis-specific controls (rows and columns)
|
||||||
(['columns', 'rows'] as const).forEach(axisParam => {
|
['columns', 'rows'].forEach(axisParam => {
|
||||||
const axis: 'rows' | 'columns' = axisParam;
|
const axis = axisParam; // Capture the value in a local variable
|
||||||
|
|
||||||
matrixifyControls[`matrixify_mode_${axis}`] = {
|
matrixifyControls[`matrixify_mode_${axis}`] = {
|
||||||
type: 'RadioButtonControl',
|
type: 'RadioButtonControl',
|
||||||
@@ -71,18 +43,17 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
|||||||
['dimensions', t('Dimension members')],
|
['dimensions', t('Dimension members')],
|
||||||
],
|
],
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) => isMatrixifyVisible(controls, axis),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixifyControls[`matrixify_${axis}`] = {
|
matrixifyControls[`matrixify_${axis}`] = {
|
||||||
...dndAdhocMetricControl,
|
...dndAdhocMetricControl,
|
||||||
label: t(`Metrics`),
|
label: t(`Metrics`),
|
||||||
multi: true,
|
multi: true,
|
||||||
validators: [], // No validation - rely on visibility
|
validators: [], // Not required
|
||||||
|
// description: t(`Select metrics for ${axis}`),
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
visibility: ({ controls }) =>
|
||||||
visibility: ({ controls }) => isMatrixifyVisible(controls, axis, 'metrics'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'metrics',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combined dimension and values control
|
// Combined dimension and values control
|
||||||
@@ -91,9 +62,8 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
|||||||
label: t(`Dimension selection`),
|
label: t(`Dimension selection`),
|
||||||
description: t(`Select dimension and values`),
|
description: t(`Select dimension and values`),
|
||||||
default: { dimension: '', values: [] },
|
default: { dimension: '', values: [] },
|
||||||
validators: [], // No validation - rely on visibility
|
validators: [], // Not required
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
shouldMapStateToProps: (prevState, state) => {
|
shouldMapStateToProps: (prevState, state) => {
|
||||||
// Recalculate when any relevant form_data field changes
|
// Recalculate when any relevant form_data field changes
|
||||||
const fieldsToCheck = [
|
const fieldsToCheck = [
|
||||||
@@ -112,40 +82,24 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
|||||||
const getValue = (key: string, defaultValue?: any) =>
|
const getValue = (key: string, defaultValue?: any) =>
|
||||||
form_data?.[key] ?? controls?.[key]?.value ?? defaultValue;
|
form_data?.[key] ?? controls?.[key]?.value ?? defaultValue;
|
||||||
|
|
||||||
const selectionMode = getValue(
|
|
||||||
`matrixify_dimension_selection_mode_${axis}`,
|
|
||||||
'members',
|
|
||||||
);
|
|
||||||
|
|
||||||
const isVisible = isMatrixifyVisible(controls, axis, 'dimensions');
|
|
||||||
|
|
||||||
// Validate dimension is selected when visible
|
|
||||||
const dimensionValidator = (value: any) => {
|
|
||||||
if (!value?.dimension) {
|
|
||||||
return t('Dimension is required');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Additional validation for topN mode
|
|
||||||
const validators = isVisible
|
|
||||||
? [dimensionValidator, validateNonEmpty]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasource,
|
datasource,
|
||||||
selectionMode,
|
selectionMode: getValue(
|
||||||
|
`matrixify_dimension_selection_mode_${axis}`,
|
||||||
|
'members',
|
||||||
|
),
|
||||||
topNMetric: getValue(`matrixify_topn_metric_${axis}`),
|
topNMetric: getValue(`matrixify_topn_metric_${axis}`),
|
||||||
topNValue: getValue(`matrixify_topn_value_${axis}`),
|
topNValue: getValue(`matrixify_topn_value_${axis}`),
|
||||||
topNOrder: getValue(`matrixify_topn_order_${axis}`),
|
topNOrder: getValue(`matrixify_topn_order_${axis}`),
|
||||||
formData: form_data,
|
formData: form_data,
|
||||||
validators,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
isMatrixifyVisible(controls, axis, 'dimensions'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dimension picker for TopN mode (just dimension, no values)
|
||||||
|
// NOTE: This is now handled by matrixify_dimension control, so hiding it
|
||||||
matrixifyControls[`matrixify_topn_dimension_${axis}`] = {
|
matrixifyControls[`matrixify_topn_dimension_${axis}`] = {
|
||||||
type: 'SelectControl',
|
type: 'SelectControl',
|
||||||
label: t('Dimension'),
|
label: t('Dimension'),
|
||||||
@@ -173,67 +127,33 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
|||||||
['topn', t('Top n')],
|
['topn', t('Top n')],
|
||||||
],
|
],
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
isMatrixifyVisible(controls, axis, 'dimensions'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions',
|
||||||
};
|
};
|
||||||
|
|
||||||
// TopN controls
|
// TopN controls
|
||||||
matrixifyControls[`matrixify_topn_value_${axis}`] = {
|
matrixifyControls[`matrixify_topn_value_${axis}`] = {
|
||||||
type: 'NumberControl',
|
type: 'TextControl',
|
||||||
label: t(`Number of top values`),
|
label: t(`Number of top values`),
|
||||||
description: t(`How many top values to select`),
|
description: t(`How many top values to select`),
|
||||||
default: 10,
|
default: 10,
|
||||||
isInt: true,
|
isInt: true,
|
||||||
validators: [],
|
|
||||||
renderTrigger: true,
|
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
|
||||||
mapStateToProps: ({ controls }) => {
|
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
|
||||||
const isVisible = isMatrixifyVisible(
|
|
||||||
controls,
|
|
||||||
axis,
|
|
||||||
'dimensions',
|
|
||||||
'topn',
|
'topn',
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
validators: isVisible ? [validateNonEmpty] : [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixifyControls[`matrixify_topn_metric_${axis}`] = {
|
matrixifyControls[`matrixify_topn_metric_${axis}`] = {
|
||||||
...dndAdhocMetricControl,
|
...dndAdhocMetricControl,
|
||||||
label: t(`Metric for ordering`),
|
label: t(`Metric for ordering`),
|
||||||
multi: false,
|
multi: false,
|
||||||
validators: [],
|
validators: [], // Not required
|
||||||
description: t(`Metric to use for ordering Top N values`),
|
description: t(`Metric to use for ordering Top N values`),
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
|
||||||
mapStateToProps: (state, controlState) => {
|
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
|
||||||
const { controls, datasource } = state;
|
|
||||||
const isVisible = isMatrixifyVisible(
|
|
||||||
controls,
|
|
||||||
axis,
|
|
||||||
'dimensions',
|
|
||||||
'topn',
|
'topn',
|
||||||
);
|
|
||||||
|
|
||||||
const originalProps =
|
|
||||||
dndAdhocMetricControl.mapStateToProps?.(state, controlState) || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...originalProps,
|
|
||||||
columns: datasource?.columns || [],
|
|
||||||
savedMetrics: defineSavedMetrics(datasource),
|
|
||||||
datasource,
|
|
||||||
datasourceType: datasource?.type,
|
|
||||||
validators: isVisible ? [validateNonEmpty] : [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixifyControls[`matrixify_topn_order_${axis}`] = {
|
matrixifyControls[`matrixify_topn_order_${axis}`] = {
|
||||||
@@ -244,10 +164,10 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
|||||||
['asc', t('Ascending')],
|
['asc', t('Ascending')],
|
||||||
['desc', t('Descending')],
|
['desc', t('Descending')],
|
||||||
],
|
],
|
||||||
renderTrigger: true,
|
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
|
controls?.[`matrixify_mode_${axis}`]?.value === 'dimensions' &&
|
||||||
|
controls?.[`matrixify_dimension_selection_mode_${axis}`]?.value ===
|
||||||
|
'topn',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,22 +213,15 @@ matrixifyControls.matrixify_charts_per_row = {
|
|||||||
!controls?.matrixify_fit_columns_dynamically?.value,
|
!controls?.matrixify_fit_columns_dynamically?.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixifyControls.matrixify_enable_vertical_layout = {
|
// Main enable control
|
||||||
|
matrixifyControls.matrixify_enabled = {
|
||||||
type: 'CheckboxControl',
|
type: 'CheckboxControl',
|
||||||
label: t('Enable vertical layout (rows)'),
|
label: t('Enable matrixify'),
|
||||||
description: t('Create matrix rows by stacking charts vertically'),
|
description: t(
|
||||||
|
'Transform this chart into a matrix/grid of charts based on dimensions or metrics',
|
||||||
|
),
|
||||||
default: false,
|
default: false,
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
};
|
|
||||||
|
|
||||||
matrixifyControls.matrixify_enable_horizontal_layout = {
|
|
||||||
type: 'CheckboxControl',
|
|
||||||
label: t('Enable horizontal layout (columns)'),
|
|
||||||
description: t('Create matrix columns by placing charts side-by-side'),
|
|
||||||
default: false,
|
|
||||||
renderTrigger: true,
|
|
||||||
tabOverride: 'matrixify',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cell title control for Matrixify
|
// Cell title control for Matrixify
|
||||||
@@ -321,8 +234,8 @@ matrixifyControls.matrixify_cell_title_template = {
|
|||||||
default: '',
|
default: '',
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
controls?.matrixify_enable_vertical_layout?.value === true ||
|
(controls?.matrixify_mode_rows?.value ||
|
||||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
controls?.matrixify_mode_columns?.value) !== undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Matrix display controls
|
// Matrix display controls
|
||||||
@@ -332,9 +245,9 @@ matrixifyControls.matrixify_show_row_labels = {
|
|||||||
description: t('Display labels for each row on the left side of the matrix'),
|
description: t('Display labels for each row on the left side of the matrix'),
|
||||||
default: true,
|
default: true,
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
controls?.matrixify_enable_vertical_layout?.value === true,
|
(controls?.matrixify_mode_rows?.value ||
|
||||||
|
controls?.matrixify_mode_columns?.value) !== undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
matrixifyControls.matrixify_show_column_headers = {
|
matrixifyControls.matrixify_show_column_headers = {
|
||||||
@@ -343,9 +256,9 @@ matrixifyControls.matrixify_show_column_headers = {
|
|||||||
description: t('Display headers for each column at the top of the matrix'),
|
description: t('Display headers for each column at the top of the matrix'),
|
||||||
default: true,
|
default: true,
|
||||||
renderTrigger: true,
|
renderTrigger: true,
|
||||||
tabOverride: 'matrixify',
|
|
||||||
visibility: ({ controls }) =>
|
visibility: ({ controls }) =>
|
||||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
(controls?.matrixify_mode_rows?.value ||
|
||||||
|
controls?.matrixify_mode_columns?.value) !== undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { matrixifyControls };
|
export { matrixifyControls };
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ensureIsArray, ValueOf } from '@superset-ui/core';
|
import { ensureIsArray, GenericDataType, ValueOf } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
import {
|
||||||
import { ControlPanelState, isDataset, isQueryResponse } from '../types';
|
ControlPanelState,
|
||||||
|
isDataset,
|
||||||
|
isQueryResponse,
|
||||||
|
} from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
export function checkColumnType(
|
export function checkColumnType(
|
||||||
columnName: string,
|
columnName: string,
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { QueryColumn, QueryResponse } from '@superset-ui/core';
|
import { GenericDataType, QueryColumn, QueryResponse } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { ColumnMeta, Dataset, isDataset, isQueryResponse } from '../types';
|
import { ColumnMeta, Dataset, isDataset, isQueryResponse } from '../types';
|
||||||
|
|
||||||
export function columnsByType(
|
export function columnsByType(
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
GenericDataType,
|
||||||
getColumnLabel,
|
getColumnLabel,
|
||||||
isPhysicalColumn,
|
isPhysicalColumn,
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { checkColumnType, ControlStateMapping } from '..';
|
import { checkColumnType, ControlStateMapping } from '..';
|
||||||
|
|
||||||
export function isSortable(controls: ControlStateMapping): boolean {
|
export function isSortable(controls: ControlStateMapping): boolean {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { render } from '@superset-ui/core/spec';
|
import { render } from '@superset-ui/core/spec';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
import { GenericDataType } from '@superset-ui/core';
|
||||||
|
|
||||||
import { ColumnOption, ColumnOptionProps } from '../../src';
|
import { ColumnOption, ColumnOptionProps } from '../../src';
|
||||||
|
|
||||||
jest.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
|
jest.mock('@superset-ui/chart-controls/components/SQLPopover', () => ({
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
import { isValidElement } from 'react';
|
import { isValidElement } from 'react';
|
||||||
import { render, screen } from '@superset-ui/core/spec';
|
import { render, screen } from '@superset-ui/core/spec';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
import { GenericDataType } from '@superset-ui/core';
|
||||||
|
|
||||||
import { ColumnTypeLabel, ColumnTypeLabelProps } from '../../src';
|
import { ColumnTypeLabel, ColumnTypeLabelProps } from '../../src';
|
||||||
|
|
||||||
describe('ColumnOption', () => {
|
describe('ColumnOption', () => {
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
|
"noEmit": true,
|
||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"extends": "../../../tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"../types/**/*",
|
||||||
|
"../../../types/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { testQueryResponse } from '@superset-ui/core';
|
import { GenericDataType, testQueryResponse } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { checkColumnType, TestDataset } from '../../src';
|
import { checkColumnType, TestDataset } from '../../src';
|
||||||
|
|
||||||
test('checkColumnType columns from a Dataset', () => {
|
test('checkColumnType columns from a Dataset', () => {
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { DatasourceType, testQueryResponse } from '@superset-ui/core';
|
import {
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
DatasourceType,
|
||||||
|
GenericDataType,
|
||||||
|
testQueryResponse,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import { columnChoices } from '../../src';
|
import { columnChoices } from '../../src';
|
||||||
|
|
||||||
describe('columnChoices()', () => {
|
describe('columnChoices()', () => {
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { testQueryResponse, testQueryResults } from '@superset-ui/core';
|
import {
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
GenericDataType,
|
||||||
|
testQueryResponse,
|
||||||
|
testQueryResults,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
Dataset,
|
Dataset,
|
||||||
getTemporalColumns,
|
getTemporalColumns,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
import { ControlStateMapping } from '@superset-ui/chart-controls';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
import { GenericDataType } from '@superset-ui/core';
|
||||||
import { isSortable } from '../../src/utils/isSortable';
|
import { isSortable } from '../../src/utils/isSortable';
|
||||||
|
|
||||||
const controls: ControlStateMapping = {
|
const controls: ControlStateMapping = {
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"declarationDir": "lib",
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"rootDir": "src",
|
"rootDir": "src"
|
||||||
"declarationDir": "lib"
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "types/**/*"],
|
"exclude": [
|
||||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
|
"lib",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"types/**/*",
|
||||||
|
"../../types/**/*"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../superset-core" },
|
{
|
||||||
{ "path": "../superset-ui-core" }
|
"path": "../superset-ui-core"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,14 @@
|
|||||||
"lib"
|
"lib"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apache-superset/core": "*",
|
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@babel/runtime": "^7.28.2",
|
"@babel/runtime": "^7.28.2",
|
||||||
"@fontsource/fira-code": "^5.2.6",
|
"@fontsource/fira-code": "^5.2.6",
|
||||||
"@fontsource/inter": "^5.2.6",
|
"@fontsource/inter": "^5.2.6",
|
||||||
"@types/json-bigint": "^1.0.4",
|
"@types/json-bigint": "^1.0.4",
|
||||||
"ace-builds": "^1.43.1",
|
"ace-builds": "^1.43.1",
|
||||||
"ag-grid-community": "34.2.0",
|
"ag-grid-community": "^34.0.2",
|
||||||
"ag-grid-react": "34.2.0",
|
"ag-grid-react": "34.0.2",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"csstype": "^3.1.3",
|
"csstype": "^3.1.3",
|
||||||
|
|||||||
@@ -276,10 +276,10 @@ export function generateMatrixifyGrid(
|
|||||||
|
|
||||||
const cellFormData = generateCellFormData(
|
const cellFormData = generateCellFormData(
|
||||||
formData,
|
formData,
|
||||||
rowCount > 0 ? config.rows : null,
|
rowCount > 1 ? config.rows : null,
|
||||||
colCount > 0 ? config.columns : null,
|
colCount > 1 ? config.columns : null,
|
||||||
rowCount > 0 ? row : null,
|
rowCount > 1 ? row : null,
|
||||||
colCount > 0 ? col : null,
|
colCount > 1 ? col : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate title using template if provided
|
// Generate title using template if provided
|
||||||
|
|||||||
@@ -74,8 +74,7 @@ test('should create single group when fitting columns dynamically', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: true,
|
matrixify_fit_columns_dynamically: true,
|
||||||
matrixify_charts_per_row: 3,
|
matrixify_charts_per_row: 3,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -124,8 +123,7 @@ test('should create multiple groups when not fitting columns dynamically', () =>
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: false,
|
matrixify_fit_columns_dynamically: false,
|
||||||
matrixify_charts_per_row: 3,
|
matrixify_charts_per_row: 3,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -160,8 +158,7 @@ test('should handle exact division of columns', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: false,
|
matrixify_fit_columns_dynamically: false,
|
||||||
matrixify_charts_per_row: 2,
|
matrixify_charts_per_row: 2,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -189,8 +186,7 @@ test('should handle case where charts_per_row exceeds total columns', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: false,
|
matrixify_fit_columns_dynamically: false,
|
||||||
matrixify_charts_per_row: 5,
|
matrixify_charts_per_row: 5,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -220,8 +216,7 @@ test('should show headers for each group when wrapping occurs', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: false,
|
matrixify_fit_columns_dynamically: false,
|
||||||
matrixify_charts_per_row: 2,
|
matrixify_charts_per_row: 2,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -255,8 +250,7 @@ test('should show headers only on first row when not wrapping', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: true, // No wrapping
|
matrixify_fit_columns_dynamically: true, // No wrapping
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
matrixify_show_column_headers: true,
|
matrixify_show_column_headers: true,
|
||||||
@@ -285,8 +279,7 @@ test('should hide headers when disabled', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_show_row_labels: false,
|
matrixify_show_row_labels: false,
|
||||||
matrixify_show_column_headers: false,
|
matrixify_show_column_headers: false,
|
||||||
};
|
};
|
||||||
@@ -313,8 +306,7 @@ test('should place cells correctly in wrapped layout', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
matrixify_fit_columns_dynamically: false,
|
matrixify_fit_columns_dynamically: false,
|
||||||
matrixify_charts_per_row: 2,
|
matrixify_charts_per_row: 2,
|
||||||
matrixify_show_row_labels: true,
|
matrixify_show_row_labels: true,
|
||||||
@@ -344,8 +336,7 @@ test('should handle null grid gracefully', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = renderWithTheme(
|
const { container } = renderWithTheme(
|
||||||
@@ -366,8 +357,7 @@ test('should handle empty grid gracefully', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = renderWithTheme(
|
const { container } = renderWithTheme(
|
||||||
@@ -391,8 +381,7 @@ test('should use default values for missing configuration', () => {
|
|||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'test_chart',
|
viz_type: 'test_chart',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_enable_horizontal_layout: true,
|
|
||||||
// Missing optional configurations
|
// Missing optional configurations
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -128,13 +128,9 @@ function MatrixifyGridRenderer({
|
|||||||
[formData],
|
[formData],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine layout parameters - only show headers/labels if layout is enabled
|
// Determine layout parameters
|
||||||
const showRowLabels =
|
const showRowLabels = formData.matrixify_show_row_labels ?? true;
|
||||||
formData.matrixify_enable_vertical_layout === true &&
|
const showColumnHeaders = formData.matrixify_show_column_headers ?? true;
|
||||||
(formData.matrixify_show_row_labels ?? true);
|
|
||||||
const showColumnHeaders =
|
|
||||||
formData.matrixify_enable_horizontal_layout === true &&
|
|
||||||
(formData.matrixify_show_column_headers ?? true);
|
|
||||||
const rowHeight = formData.matrixify_row_height || DEFAULT_ROW_HEIGHT;
|
const rowHeight = formData.matrixify_row_height || DEFAULT_ROW_HEIGHT;
|
||||||
const fitColumnsDynamically =
|
const fitColumnsDynamically =
|
||||||
formData.matrixify_fit_columns_dynamically ?? true;
|
formData.matrixify_fit_columns_dynamically ?? true;
|
||||||
|
|||||||
@@ -37,11 +37,10 @@ test('isMatrixifyEnabled should return false when no matrixify configuration exi
|
|||||||
expect(isMatrixifyEnabled(formData)).toBe(false);
|
expect(isMatrixifyEnabled(formData)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isMatrixifyEnabled should return false when layout controls are false', () => {
|
test('isMatrixifyEnabled should return false when matrixify_enabled is false', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: false,
|
matrixify_enabled: false,
|
||||||
matrixify_enable_horizontal_layout: false,
|
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
} as MatrixifyFormData;
|
} as MatrixifyFormData;
|
||||||
@@ -52,7 +51,7 @@ test('isMatrixifyEnabled should return false when layout controls are false', ()
|
|||||||
test('isMatrixifyEnabled should return true for valid metrics mode configuration', () => {
|
test('isMatrixifyEnabled should return true for valid metrics mode configuration', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_mode_columns: 'metrics',
|
matrixify_mode_columns: 'metrics',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
@@ -65,7 +64,7 @@ test('isMatrixifyEnabled should return true for valid metrics mode configuration
|
|||||||
test('isMatrixifyEnabled should return true for valid dimensions mode configuration', () => {
|
test('isMatrixifyEnabled should return true for valid dimensions mode configuration', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'dimensions',
|
matrixify_mode_rows: 'dimensions',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
||||||
@@ -78,7 +77,7 @@ test('isMatrixifyEnabled should return true for valid dimensions mode configurat
|
|||||||
test('isMatrixifyEnabled should return true for mixed mode configuration', () => {
|
test('isMatrixifyEnabled should return true for mixed mode configuration', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
@@ -91,7 +90,7 @@ test('isMatrixifyEnabled should return true for mixed mode configuration', () =>
|
|||||||
test('isMatrixifyEnabled should return true for topn dimension selection mode', () => {
|
test('isMatrixifyEnabled should return true for topn dimension selection mode', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'dimensions',
|
matrixify_mode_rows: 'dimensions',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_dimension_rows: {
|
matrixify_dimension_rows: {
|
||||||
@@ -110,7 +109,7 @@ test('isMatrixifyEnabled should return true for topn dimension selection mode',
|
|||||||
test('isMatrixifyEnabled should return false when both axes have empty metrics arrays', () => {
|
test('isMatrixifyEnabled should return false when both axes have empty metrics arrays', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_mode_columns: 'metrics',
|
matrixify_mode_columns: 'metrics',
|
||||||
matrixify_rows: [],
|
matrixify_rows: [],
|
||||||
@@ -123,7 +122,7 @@ test('isMatrixifyEnabled should return false when both axes have empty metrics a
|
|||||||
test('isMatrixifyEnabled should return false when both dimensions have empty values and no topn mode', () => {
|
test('isMatrixifyEnabled should return false when both dimensions have empty values and no topn mode', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'dimensions',
|
matrixify_mode_rows: 'dimensions',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_dimension_rows: { dimension: 'country', values: [] },
|
matrixify_dimension_rows: { dimension: 'country', values: [] },
|
||||||
@@ -141,7 +140,7 @@ test('getMatrixifyConfig should return null when no matrixify configuration exis
|
|||||||
test('getMatrixifyConfig should return valid config for metrics mode', () => {
|
test('getMatrixifyConfig should return valid config for metrics mode', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_mode_columns: 'metrics',
|
matrixify_mode_columns: 'metrics',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
@@ -159,7 +158,7 @@ test('getMatrixifyConfig should return valid config for metrics mode', () => {
|
|||||||
test('getMatrixifyConfig should return valid config for dimensions mode', () => {
|
test('getMatrixifyConfig should return valid config for dimensions mode', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'dimensions',
|
matrixify_mode_rows: 'dimensions',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
||||||
@@ -183,7 +182,7 @@ test('getMatrixifyConfig should return valid config for dimensions mode', () =>
|
|||||||
test('getMatrixifyConfig should handle topn selection mode', () => {
|
test('getMatrixifyConfig should handle topn selection mode', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'dimensions',
|
matrixify_mode_rows: 'dimensions',
|
||||||
matrixify_mode_columns: 'dimensions',
|
matrixify_mode_columns: 'dimensions',
|
||||||
matrixify_dimension_rows: {
|
matrixify_dimension_rows: {
|
||||||
@@ -204,8 +203,7 @@ test('getMatrixifyConfig should handle topn selection mode', () => {
|
|||||||
test('getMatrixifyValidationErrors should return empty array when matrixify is not enabled', () => {
|
test('getMatrixifyValidationErrors should return empty array when matrixify is not enabled', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: false,
|
matrixify_enabled: false,
|
||||||
matrixify_enable_horizontal_layout: false,
|
|
||||||
} as MatrixifyFormData;
|
} as MatrixifyFormData;
|
||||||
|
|
||||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||||
@@ -214,7 +212,7 @@ test('getMatrixifyValidationErrors should return empty array when matrixify is n
|
|||||||
test('getMatrixifyValidationErrors should return empty array when properly configured', () => {
|
test('getMatrixifyValidationErrors should return empty array when properly configured', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_mode_columns: 'metrics',
|
matrixify_mode_columns: 'metrics',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
@@ -227,7 +225,7 @@ test('getMatrixifyValidationErrors should return empty array when properly confi
|
|||||||
test('getMatrixifyValidationErrors should return error when enabled but no configuration exists', () => {
|
test('getMatrixifyValidationErrors should return error when enabled but no configuration exists', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
} as MatrixifyFormData;
|
} as MatrixifyFormData;
|
||||||
|
|
||||||
const errors = getMatrixifyValidationErrors(formData);
|
const errors = getMatrixifyValidationErrors(formData);
|
||||||
@@ -237,7 +235,7 @@ test('getMatrixifyValidationErrors should return error when enabled but no confi
|
|||||||
test('getMatrixifyValidationErrors should return error when metrics mode has no metrics', () => {
|
test('getMatrixifyValidationErrors should return error when metrics mode has no metrics', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_rows: [],
|
matrixify_rows: [],
|
||||||
matrixify_columns: [],
|
matrixify_columns: [],
|
||||||
@@ -263,7 +261,7 @@ test('should handle empty form data object', () => {
|
|||||||
test('should handle partial configuration with one axis only', () => {
|
test('should handle partial configuration with one axis only', () => {
|
||||||
const formData = {
|
const formData = {
|
||||||
viz_type: 'table',
|
viz_type: 'table',
|
||||||
matrixify_enable_vertical_layout: true,
|
matrixify_enabled: true,
|
||||||
matrixify_mode_rows: 'metrics',
|
matrixify_mode_rows: 'metrics',
|
||||||
matrixify_rows: [createMetric('Revenue')],
|
matrixify_rows: [createMetric('Revenue')],
|
||||||
// No columns configuration
|
// No columns configuration
|
||||||
|
|||||||
@@ -96,9 +96,8 @@ export interface MatrixifyAxisConfig {
|
|||||||
* Complete Matrixify configuration in form data
|
* Complete Matrixify configuration in form data
|
||||||
*/
|
*/
|
||||||
export interface MatrixifyFormData {
|
export interface MatrixifyFormData {
|
||||||
// Layout enable controls
|
// Enable/disable matrixify functionality
|
||||||
matrixify_enable_vertical_layout?: boolean;
|
matrixify_enabled?: boolean;
|
||||||
matrixify_enable_horizontal_layout?: boolean;
|
|
||||||
|
|
||||||
// Row axis configuration
|
// Row axis configuration
|
||||||
matrixify_mode_rows?: MatrixifyMode;
|
matrixify_mode_rows?: MatrixifyMode;
|
||||||
@@ -178,12 +177,8 @@ export function getMatrixifyConfig(
|
|||||||
* Check if Matrixify is enabled and properly configured
|
* Check if Matrixify is enabled and properly configured
|
||||||
*/
|
*/
|
||||||
export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean {
|
export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean {
|
||||||
// Check if either vertical or horizontal layout is enabled
|
// First check if matrixify is explicitly enabled via checkbox
|
||||||
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
|
if (!formData.matrixify_enabled) {
|
||||||
const hasHorizontalLayout =
|
|
||||||
formData.matrixify_enable_horizontal_layout === true;
|
|
||||||
|
|
||||||
if (!hasVerticalLayout && !hasHorizontalLayout) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,11 +216,7 @@ export function getMatrixifyValidationErrors(
|
|||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Only validate if matrixify is enabled
|
// Only validate if matrixify is enabled
|
||||||
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
|
if (!formData.matrixify_enabled) {
|
||||||
const hasHorizontalLayout =
|
|
||||||
formData.matrixify_enable_horizontal_layout === true;
|
|
||||||
|
|
||||||
if (!hasVerticalLayout && !hasHorizontalLayout) {
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,33 +123,21 @@ export function AsyncAceEditor(
|
|||||||
const cssWorkerUrlPromise = import(
|
const cssWorkerUrlPromise = import(
|
||||||
'ace-builds/src-min-noconflict/worker-css'
|
'ace-builds/src-min-noconflict/worker-css'
|
||||||
);
|
);
|
||||||
const javascriptWorkerUrlPromise = import(
|
|
||||||
'ace-builds/src-min-noconflict/worker-javascript'
|
|
||||||
);
|
|
||||||
const htmlWorkerUrlPromise = import(
|
|
||||||
'ace-builds/src-min-noconflict/worker-html'
|
|
||||||
);
|
|
||||||
const acequirePromise = import('ace-builds/src-min-noconflict/ace');
|
const acequirePromise = import('ace-builds/src-min-noconflict/ace');
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ default: ReactAceEditor },
|
{ default: ReactAceEditor },
|
||||||
{ config },
|
{ config },
|
||||||
{ default: cssWorkerUrl },
|
{ default: cssWorkerUrl },
|
||||||
{ default: javascriptWorkerUrl },
|
|
||||||
{ default: htmlWorkerUrl },
|
|
||||||
{ require: acequire },
|
{ require: acequire },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
reactAcePromise,
|
reactAcePromise,
|
||||||
aceBuildsConfigPromise,
|
aceBuildsConfigPromise,
|
||||||
cssWorkerUrlPromise,
|
cssWorkerUrlPromise,
|
||||||
javascriptWorkerUrlPromise,
|
|
||||||
htmlWorkerUrlPromise,
|
|
||||||
acequirePromise,
|
acequirePromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl);
|
config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl);
|
||||||
config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
|
|
||||||
config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
|
|
||||||
|
|
||||||
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
|
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
/* eslint-disable import/first */
|
|
||||||
/**
|
|
||||||
* 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 AceEditor, { IAceEditorProps } from 'react-ace';
|
|
||||||
import ace from 'ace-builds/src-noconflict/ace';
|
|
||||||
|
|
||||||
// Disable workers to avoid localhost loading issues
|
|
||||||
ace.config.set('useWorker', false);
|
|
||||||
|
|
||||||
// Import required modes and themes after ace is loaded
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-handlebars';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-css';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-json';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-sql';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-markdown';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-javascript';
|
|
||||||
import 'ace-builds/src-min-noconflict/mode-html';
|
|
||||||
import 'ace-builds/src-noconflict/theme-github';
|
|
||||||
import 'ace-builds/src-noconflict/theme-monokai';
|
|
||||||
|
|
||||||
export type CodeEditorMode =
|
|
||||||
| 'handlebars'
|
|
||||||
| 'css'
|
|
||||||
| 'json'
|
|
||||||
| 'sql'
|
|
||||||
| 'markdown'
|
|
||||||
| 'javascript'
|
|
||||||
| 'html';
|
|
||||||
|
|
||||||
export type CodeEditorTheme = 'light' | 'dark';
|
|
||||||
|
|
||||||
export interface CodeEditorProps
|
|
||||||
extends Omit<IAceEditorProps, 'mode' | 'theme'> {
|
|
||||||
mode?: CodeEditorMode;
|
|
||||||
theme?: CodeEditorTheme;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CodeEditor: FC<CodeEditorProps> = ({
|
|
||||||
mode = 'handlebars',
|
|
||||||
theme = 'dark',
|
|
||||||
name,
|
|
||||||
width = '100%',
|
|
||||||
height = '300px',
|
|
||||||
value,
|
|
||||||
fontSize = 14,
|
|
||||||
showPrintMargin = true,
|
|
||||||
focus = true,
|
|
||||||
wrapEnabled = true,
|
|
||||||
highlightActiveLine = true,
|
|
||||||
editorProps = { $blockScrolling: true },
|
|
||||||
setOptions,
|
|
||||||
...rest
|
|
||||||
}: CodeEditorProps) => {
|
|
||||||
const editorName = name || Math.random().toString(36).substring(7);
|
|
||||||
const aceTheme = theme === 'light' ? 'github' : 'monokai';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AceEditor
|
|
||||||
mode={mode}
|
|
||||||
theme={aceTheme}
|
|
||||||
name={editorName}
|
|
||||||
height={height}
|
|
||||||
width={width}
|
|
||||||
value={value}
|
|
||||||
fontSize={fontSize}
|
|
||||||
showPrintMargin={showPrintMargin}
|
|
||||||
focus={focus}
|
|
||||||
editorProps={editorProps}
|
|
||||||
wrapEnabled={wrapEnabled}
|
|
||||||
highlightActiveLine={highlightActiveLine}
|
|
||||||
setOptions={{
|
|
||||||
enableBasicAutocompletion: true,
|
|
||||||
enableLiveAutocompletion: true,
|
|
||||||
enableSnippets: true,
|
|
||||||
showLineNumbers: true,
|
|
||||||
tabSize: 2,
|
|
||||||
showGutter: true,
|
|
||||||
fontFamily:
|
|
||||||
'Menlo, Consolas, Courier New, Ubuntu Mono, source-code-pro, Lucida Console, monospace',
|
|
||||||
...setOptions,
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CodeEditor;
|
|
||||||
@@ -19,10 +19,8 @@
|
|||||||
import { useEffect, useState, FunctionComponent } from 'react';
|
import { useEffect, useState, FunctionComponent } from 'react';
|
||||||
|
|
||||||
import { t, styled, css, useTheme } from '@superset-ui/core';
|
import { t, styled, css, useTheme } from '@superset-ui/core';
|
||||||
import { Dayjs } from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { extendedDayjs } from '../../utils/dates';
|
import { extendedDayjs } from '../../utils/dates';
|
||||||
import 'dayjs/plugin/updateLocale';
|
|
||||||
import 'dayjs/plugin/calendar';
|
|
||||||
import { Icons } from '../Icons';
|
import { Icons } from '../Icons';
|
||||||
import type { LastUpdatedProps } from './types';
|
import type { LastUpdatedProps } from './types';
|
||||||
|
|
||||||
@@ -48,7 +46,9 @@ export const LastUpdated: FunctionComponent<LastUpdatedProps> = ({
|
|||||||
update,
|
update,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [timeSince, setTimeSince] = useState<Dayjs>(extendedDayjs(updatedAt));
|
const [timeSince, setTimeSince] = useState<dayjs.Dayjs>(
|
||||||
|
extendedDayjs(updatedAt),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeSince(() => extendedDayjs(updatedAt));
|
setTimeSince(() => extendedDayjs(updatedAt));
|
||||||
|
|||||||
@@ -127,9 +127,13 @@ const Select = forwardRef(
|
|||||||
const shouldShowSearch = allowNewOptions ? true : showSearch;
|
const shouldShowSearch = allowNewOptions ? true : showSearch;
|
||||||
const [selectValue, setSelectValue] = useState(value);
|
const [selectValue, setSelectValue] = useState(value);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(loading);
|
||||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [visibleOptions, setVisibleOptions] = useState<SelectOptionsType>([]);
|
const [visibleOptions, setVisibleOptions] = useState<SelectOptionsType>([]);
|
||||||
|
const [maxTagCount, setMaxTagCount] = useState(
|
||||||
|
propsMaxTagCount ?? MAX_TAG_COUNT,
|
||||||
|
);
|
||||||
const [onChangeCount, setOnChangeCount] = useState(0);
|
const [onChangeCount, setOnChangeCount] = useState(0);
|
||||||
const previousChangeCount = usePrevious(onChangeCount, 0);
|
const previousChangeCount = usePrevious(onChangeCount, 0);
|
||||||
const fireOnChange = useCallback(
|
const fireOnChange = useCallback(
|
||||||
@@ -137,11 +141,11 @@ const Select = forwardRef(
|
|||||||
[onChangeCount],
|
[onChangeCount],
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxTagCount = oneLine
|
useEffect(() => {
|
||||||
? isDropdownVisible
|
if (oneLine) {
|
||||||
? 0
|
setMaxTagCount(isDropdownVisible ? 0 : 1);
|
||||||
: 1
|
}
|
||||||
: (propsMaxTagCount ?? MAX_TAG_COUNT);
|
}, [isDropdownVisible, oneLine]);
|
||||||
|
|
||||||
const mappedMode = isSingleMode ? undefined : 'multiple';
|
const mappedMode = isSingleMode ? undefined : 'multiple';
|
||||||
|
|
||||||
@@ -506,8 +510,6 @@ const Select = forwardRef(
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = loading ?? false;
|
|
||||||
|
|
||||||
const popupRender = (
|
const popupRender = (
|
||||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||||
) =>
|
) =>
|
||||||
@@ -534,6 +536,12 @@ const Select = forwardRef(
|
|||||||
setVisibleOptions(initialOptions);
|
setVisibleOptions(initialOptions);
|
||||||
}, [initialOptions]);
|
}, [initialOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading !== undefined && loading !== isLoading) {
|
||||||
|
setIsLoading(loading);
|
||||||
|
}
|
||||||
|
}, [isLoading, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectValue(value);
|
setSelectValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|||||||
@@ -181,9 +181,3 @@ export {
|
|||||||
type ThemedAgGridReactProps,
|
type ThemedAgGridReactProps,
|
||||||
setupAGGridModules,
|
setupAGGridModules,
|
||||||
} from './ThemedAgGridReact';
|
} from './ThemedAgGridReact';
|
||||||
export {
|
|
||||||
CodeEditor,
|
|
||||||
type CodeEditorProps,
|
|
||||||
type CodeEditorMode,
|
|
||||||
type CodeEditorTheme,
|
|
||||||
} from './CodeEditor';
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
import { GenericDataType } from './QueryResponse';
|
||||||
import { QueryFormColumn } from './QueryFormData';
|
import { QueryFormColumn } from './QueryFormData';
|
||||||
|
|
||||||
export interface AdhocColumn {
|
export interface AdhocColumn {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { DatasourceType } from './Datasource';
|
import { DatasourceType } from './Datasource';
|
||||||
import { BinaryOperator, SetOperator, UnaryOperator } from './Operator';
|
import { BinaryOperator, SetOperator, UnaryOperator } from './Operator';
|
||||||
import { AppliedTimeExtras, TimeRange } from './Time';
|
import { AppliedTimeExtras, TimeRange } from './Time';
|
||||||
@@ -32,7 +31,7 @@ import { Maybe } from '../../types';
|
|||||||
import { PostProcessingRule } from './PostProcessing';
|
import { PostProcessingRule } from './PostProcessing';
|
||||||
import { JsonObject } from '../../connection';
|
import { JsonObject } from '../../connection';
|
||||||
import { TimeGranularity } from '../../time-format';
|
import { TimeGranularity } from '../../time-format';
|
||||||
import { DataRecordValue } from './QueryResponse';
|
import { GenericDataType, DataRecordValue } from './QueryResponse';
|
||||||
|
|
||||||
export type BaseQueryObjectFilterClause = {
|
export type BaseQueryObjectFilterClause = {
|
||||||
col: QueryFormColumn;
|
col: QueryFormColumn;
|
||||||
|
|||||||
@@ -17,10 +17,19 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
import { TimeseriesDataRecord } from '../../chart';
|
import { TimeseriesDataRecord } from '../../chart';
|
||||||
import { AnnotationData } from './AnnotationLayer';
|
import { AnnotationData } from './AnnotationLayer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic data types, see enum of the same name in superset/utils/core.py.
|
||||||
|
*/
|
||||||
|
export enum GenericDataType {
|
||||||
|
Numeric = 0,
|
||||||
|
String = 1,
|
||||||
|
Temporal = 2,
|
||||||
|
Boolean = 3,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primitive types for data field values.
|
* Primitive types for data field values.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ dayjs.updateLocale('en', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const extendedDayjs = dayjs;
|
export const extendedDayjs = dayjs;
|
||||||
export type { Dayjs };
|
|
||||||
|
|
||||||
export const fDuration = function (
|
export const fDuration = function (
|
||||||
t1: number,
|
t1: number,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export enum FeatureFlag {
|
|||||||
AlertReports = 'ALERT_REPORTS',
|
AlertReports = 'ALERT_REPORTS',
|
||||||
AlertReportTabs = 'ALERT_REPORT_TABS',
|
AlertReportTabs = 'ALERT_REPORT_TABS',
|
||||||
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
|
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
|
||||||
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
|
|
||||||
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
|
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
|
||||||
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
|
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
|
||||||
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
|
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { AdhocMetric } from '@superset-ui/core';
|
import { AdhocMetric, GenericDataType } from '@superset-ui/core';
|
||||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
|
||||||
|
|
||||||
export const NUM_METRIC: AdhocMetric = {
|
export const NUM_METRIC: AdhocMetric = {
|
||||||
expressionType: 'SIMPLE',
|
expressionType: 'SIMPLE',
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
|
"noEmit": true,
|
||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"extends": "../../../tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
|
"include": ["**/*", "../types/**/*", "../../../types/**/*"],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"declarationDir": "lib",
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"src/*": ["./src/*"],
|
||||||
|
"@superset-ui/core": ["src"],
|
||||||
|
"@superset-ui/core/*": ["src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "types/**/*"],
|
"exclude": [
|
||||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"],
|
"lib",
|
||||||
"references": [{ "path": "../superset-core" }]
|
"test"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"spec/**/*",
|
||||||
|
"types/**/*"
|
||||||
|
],
|
||||||
|
"references": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,4 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
declare module 'ace-builds/src-min-noconflict/worker-css';
|
declare module 'ace-builds/src-min-noconflict/worker-css';
|
||||||
declare module 'ace-builds/src-min-noconflict/worker-javascript';
|
|
||||||
declare module 'ace-builds/src-min-noconflict/worker-html';
|
|
||||||
declare module 'ace-builds/src-min-noconflict/ace';
|
declare module 'ace-builds/src-min-noconflict/ace';
|
||||||
|
|||||||
@@ -19,5 +19,3 @@
|
|||||||
declare module '*.gif';
|
declare module '*.gif';
|
||||||
declare module '*.svg';
|
declare module '*.svg';
|
||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
declare module '*.jpg';
|
|
||||||
declare module '*.jpeg';
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"declarationDir": "lib",
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"rootDir": "src",
|
"rootDir": "src"
|
||||||
"declarationDir": "lib"
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "types/**/*"],
|
"exclude": [
|
||||||
"exclude": ["src/**/*.test.*", "src/**/*.stories.*"]
|
"lib",
|
||||||
|
"src/**/*.test.ts"
|
||||||
|
],
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"types/**/*",
|
||||||
|
"../../types/**/*"
|
||||||
|
],
|
||||||
|
"references": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/// <reference types="node" />
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import { defineConfig } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
// Test directory
|
|
||||||
testDir: './playwright/tests',
|
|
||||||
|
|
||||||
// Timeout settings
|
|
||||||
timeout: 30000,
|
|
||||||
expect: { timeout: 8000 },
|
|
||||||
|
|
||||||
// Parallel execution
|
|
||||||
fullyParallel: true,
|
|
||||||
workers: process.env.CI ? 2 : 1,
|
|
||||||
|
|
||||||
// Retry logic - 2 retries in CI, 0 locally
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
|
|
||||||
// Reporter configuration - multiple reporters for better visibility
|
|
||||||
reporter: process.env.CI
|
|
||||||
? [
|
|
||||||
['github'], // GitHub Actions annotations
|
|
||||||
['list'], // Detailed output with summary table
|
|
||||||
['html', { outputFolder: 'playwright-report', open: 'never' }], // Interactive report
|
|
||||||
['json', { outputFile: 'test-results/results.json' }], // Machine-readable
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
['list'], // Shows summary table locally
|
|
||||||
['html', { outputFolder: 'playwright-report', open: 'on-failure' }], // Auto-open on failure
|
|
||||||
],
|
|
||||||
|
|
||||||
// Global test setup
|
|
||||||
use: {
|
|
||||||
// Use environment variable for base URL in CI, default to localhost:8088 for local
|
|
||||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088',
|
|
||||||
|
|
||||||
// Browser settings
|
|
||||||
headless: !!process.env.CI,
|
|
||||||
|
|
||||||
viewport: { width: 1280, height: 1024 },
|
|
||||||
|
|
||||||
// Screenshots and videos on failure
|
|
||||||
screenshot: 'only-on-failure',
|
|
||||||
video: 'retain-on-failure',
|
|
||||||
|
|
||||||
// Trace collection for debugging
|
|
||||||
trace: 'retain-on-failure',
|
|
||||||
},
|
|
||||||
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: {
|
|
||||||
browserName: 'chromium',
|
|
||||||
testIdAttribute: 'data-test',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
// Web server setup - disabled in CI (Flask started separately in workflow)
|
|
||||||
webServer: process.env.CI
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
command: 'curl -f http://localhost:8088/health',
|
|
||||||
url: 'http://localhost:8088/health',
|
|
||||||
reuseExistingServer: true,
|
|
||||||
timeout: 5000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,218 +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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Playwright E2E Tests for Superset
|
|
||||||
|
|
||||||
This directory contains Playwright end-to-end tests for Apache Superset, designed as a replacement for the existing Cypress tests during the migration to Playwright.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
playwright/
|
|
||||||
├── components/core/ # Reusable UI components
|
|
||||||
├── pages/ # Page Object Models
|
|
||||||
├── tests/ # Test files organized by feature
|
|
||||||
├── utils/ # Shared constants and utilities
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
We follow **YAGNI** (You Aren't Gonna Need It), **DRY** (Don't Repeat Yourself), and **KISS** (Keep It Simple, Stupid) principles:
|
|
||||||
|
|
||||||
- Build only what's needed now
|
|
||||||
- Reuse existing patterns and components
|
|
||||||
- Keep solutions simple and maintainable
|
|
||||||
|
|
||||||
## Component Architecture
|
|
||||||
|
|
||||||
### Core Components (`components/core/`)
|
|
||||||
|
|
||||||
Reusable UI interaction classes for common elements:
|
|
||||||
|
|
||||||
- **Form**: Container with properly scoped child element access
|
|
||||||
- **Input**: Supports `fill()`, `type()`, and `pressSequentially()` methods
|
|
||||||
- **Button**: Standard click, hover, focus interactions
|
|
||||||
|
|
||||||
**Usage Example:**
|
|
||||||
```typescript
|
|
||||||
import { Form } from '../components/core';
|
|
||||||
|
|
||||||
const loginForm = new Form(page, '[data-test="login-form"]');
|
|
||||||
const usernameInput = loginForm.getInput('[data-test="username-input"]');
|
|
||||||
await usernameInput.fill('admin');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Page Objects (`pages/`)
|
|
||||||
|
|
||||||
Each page object encapsulates:
|
|
||||||
- **Actions**: What you can do on the page
|
|
||||||
- **Queries**: Information you can get from the page
|
|
||||||
- **Selectors**: Centralized in private static SELECTORS constant
|
|
||||||
- **NO Assertions**: Keep assertions in test files
|
|
||||||
|
|
||||||
**Page Object Pattern:**
|
|
||||||
```typescript
|
|
||||||
export class AuthPage {
|
|
||||||
// Selectors centralized in the page object
|
|
||||||
private static readonly SELECTORS = {
|
|
||||||
LOGIN_FORM: '[data-test="login-form"]',
|
|
||||||
USERNAME_INPUT: '[data-test="username-input"]',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Actions - what you can do
|
|
||||||
async loginWithCredentials(username: string, password: string) { }
|
|
||||||
|
|
||||||
// Queries - information you can get
|
|
||||||
async getCurrentUrl(): Promise<string> { }
|
|
||||||
|
|
||||||
// NO assertions - those belong in tests
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests (`tests/`)
|
|
||||||
|
|
||||||
Organized by feature/area (auth, dashboard, charts, etc.):
|
|
||||||
- Use page objects for actions
|
|
||||||
- Keep assertions in test files
|
|
||||||
- Import shared constants from `utils/`
|
|
||||||
|
|
||||||
**Test Pattern:**
|
|
||||||
```typescript
|
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { AuthPage } from '../../pages/AuthPage';
|
|
||||||
import { LOGIN } from '../../utils/urls';
|
|
||||||
|
|
||||||
test('should login with correct credentials', async ({ page }) => {
|
|
||||||
const authPage = new AuthPage(page);
|
|
||||||
await authPage.goto();
|
|
||||||
await authPage.loginWithCredentials('admin', 'general');
|
|
||||||
|
|
||||||
// Assertions belong in tests, not page objects
|
|
||||||
expect(await authPage.getCurrentUrl()).not.toContain(LOGIN);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Utilities (`utils/`)
|
|
||||||
|
|
||||||
Shared constants and utilities:
|
|
||||||
- **urls.ts**: URL paths and request patterns
|
|
||||||
- Keep flat exports (no premature namespacing)
|
|
||||||
|
|
||||||
## Contributing Guidelines
|
|
||||||
|
|
||||||
### Adding New Tests
|
|
||||||
|
|
||||||
1. **Check existing components** before creating new ones
|
|
||||||
2. **Use page objects** for page interactions
|
|
||||||
3. **Keep assertions in tests**, not page objects
|
|
||||||
4. **Follow naming conventions**: `feature.spec.ts`
|
|
||||||
|
|
||||||
### Adding New Components
|
|
||||||
|
|
||||||
1. **Follow YAGNI**: Only build what's immediately needed
|
|
||||||
2. **Use Locator-based scoping** for proper element isolation
|
|
||||||
3. **Support both string selectors and Locator objects** via constructor overloads
|
|
||||||
4. **Add to `components/core/index.ts`** for easy importing
|
|
||||||
|
|
||||||
### Adding New Page Objects
|
|
||||||
|
|
||||||
1. **Centralize selectors** in private static SELECTORS constant
|
|
||||||
2. **Import shared constants** from `utils/urls.ts`
|
|
||||||
3. **Actions and queries only** - no assertions
|
|
||||||
4. **Use existing components** for DOM interactions
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run all tests
|
|
||||||
npm run playwright:test
|
|
||||||
# or: npx playwright test
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
npx playwright test tests/auth/login.spec.ts
|
|
||||||
|
|
||||||
# Run with UI mode for debugging
|
|
||||||
npm run playwright:ui
|
|
||||||
# or: npx playwright test --ui
|
|
||||||
|
|
||||||
# Run in headed mode (see browser)
|
|
||||||
npm run playwright:headed
|
|
||||||
# or: npx playwright test --headed
|
|
||||||
|
|
||||||
# Debug specific test file
|
|
||||||
npm run playwright:debug tests/auth/login.spec.ts
|
|
||||||
# or: npx playwright test --debug tests/auth/login.spec.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Reports
|
|
||||||
|
|
||||||
Playwright generates multiple reports for better visibility:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View interactive HTML report (opens automatically on failure)
|
|
||||||
npm run playwright:report
|
|
||||||
# or: npx playwright show-report
|
|
||||||
|
|
||||||
# View test trace for debugging failures
|
|
||||||
npx playwright show-trace test-results/[test-name]/trace.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### Report Types
|
|
||||||
|
|
||||||
- **List Reporter**: Shows progress and summary table in terminal
|
|
||||||
- **HTML Report**: Interactive web interface with screenshots, videos, and traces
|
|
||||||
- **JSON Report**: Machine-readable format in `test-results/results.json`
|
|
||||||
- **GitHub Actions**: Annotations in CI for failed tests
|
|
||||||
|
|
||||||
### Debugging Failed Tests
|
|
||||||
|
|
||||||
When tests fail, Playwright automatically captures:
|
|
||||||
- **Screenshots** at the point of failure
|
|
||||||
- **Videos** of the entire test run
|
|
||||||
- **Traces** with timeline and network activity
|
|
||||||
- **Error context** with detailed debugging information
|
|
||||||
|
|
||||||
All debugging artifacts are available in the HTML report for easy analysis.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- **Config**: `playwright.config.ts` - matches Cypress settings
|
|
||||||
- **Base URL**: `http://localhost:8088` (assumes Superset running)
|
|
||||||
- **Browsers**: Chrome only for Phase 1 (YAGNI)
|
|
||||||
- **Retries**: 2 in CI, 0 locally (matches Cypress)
|
|
||||||
|
|
||||||
## Migration from Cypress
|
|
||||||
|
|
||||||
When porting Cypress tests:
|
|
||||||
|
|
||||||
1. **Port the logic**, not the implementation
|
|
||||||
2. **Use page objects** instead of inline selectors
|
|
||||||
3. **Replace `cy.intercept/cy.wait`** with `page.waitForRequest()`
|
|
||||||
4. **Use shared constants** from `utils/urls.ts`
|
|
||||||
5. **Follow the established patterns** shown in `tests/auth/login.spec.ts`
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
- **Centralize selectors** in page objects
|
|
||||||
- **Centralize URLs** in `utils/urls.ts`
|
|
||||||
- **Use meaningful test descriptions**
|
|
||||||
- **Keep page objects action-focused**
|
|
||||||
- **Put assertions in tests, not page objects**
|
|
||||||
- **Follow the existing patterns** for consistency
|
|
||||||
@@ -1,119 +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 { Locator, Page } from '@playwright/test';
|
|
||||||
|
|
||||||
export class Button {
|
|
||||||
private readonly locator: Locator;
|
|
||||||
|
|
||||||
constructor(page: Page, selector: string);
|
|
||||||
|
|
||||||
constructor(page: Page, locator: Locator);
|
|
||||||
|
|
||||||
constructor(page: Page, selectorOrLocator: string | Locator) {
|
|
||||||
if (typeof selectorOrLocator === 'string') {
|
|
||||||
this.locator = page.locator(selectorOrLocator);
|
|
||||||
} else {
|
|
||||||
this.locator = selectorOrLocator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the button element locator
|
|
||||||
*/
|
|
||||||
get element(): Locator {
|
|
||||||
return this.locator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clicks the button
|
|
||||||
* @param options - Optional click options
|
|
||||||
*/
|
|
||||||
async click(options?: {
|
|
||||||
timeout?: number;
|
|
||||||
force?: boolean;
|
|
||||||
delay?: number;
|
|
||||||
button?: 'left' | 'right' | 'middle';
|
|
||||||
}): Promise<void> {
|
|
||||||
await this.element.click(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the button text content
|
|
||||||
*/
|
|
||||||
async getText(): Promise<string> {
|
|
||||||
return (await this.element.textContent()) ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a specific attribute value from the button
|
|
||||||
* @param attribute - The attribute name to retrieve
|
|
||||||
*/
|
|
||||||
async getAttribute(attribute: string): Promise<string | null> {
|
|
||||||
return this.element.getAttribute(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the button is visible
|
|
||||||
*/
|
|
||||||
async isVisible(): Promise<boolean> {
|
|
||||||
return this.element.isVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the button is enabled
|
|
||||||
*/
|
|
||||||
async isEnabled(): Promise<boolean> {
|
|
||||||
return this.element.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the button is disabled
|
|
||||||
*/
|
|
||||||
async isDisabled(): Promise<boolean> {
|
|
||||||
return this.element.isDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hovers over the button
|
|
||||||
* @param options - Optional hover options
|
|
||||||
*/
|
|
||||||
async hover(options?: { timeout?: number; force?: boolean }): Promise<void> {
|
|
||||||
await this.element.hover(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses on the button
|
|
||||||
*/
|
|
||||||
async focus(): Promise<void> {
|
|
||||||
await this.element.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Double clicks the button
|
|
||||||
* @param options - Optional click options
|
|
||||||
*/
|
|
||||||
async doubleClick(options?: {
|
|
||||||
timeout?: number;
|
|
||||||
force?: boolean;
|
|
||||||
delay?: number;
|
|
||||||
}): Promise<void> {
|
|
||||||
await this.element.dblclick(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +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 { Locator, Page } from '@playwright/test';
|
|
||||||
import { Input } from './Input';
|
|
||||||
import { Button } from './Button';
|
|
||||||
|
|
||||||
export class Form {
|
|
||||||
private readonly page: Page;
|
|
||||||
|
|
||||||
private readonly locator: Locator;
|
|
||||||
|
|
||||||
constructor(page: Page, selector: string);
|
|
||||||
|
|
||||||
constructor(page: Page, locator: Locator);
|
|
||||||
|
|
||||||
constructor(page: Page, selectorOrLocator: string | Locator) {
|
|
||||||
this.page = page;
|
|
||||||
if (typeof selectorOrLocator === 'string') {
|
|
||||||
this.locator = page.locator(selectorOrLocator);
|
|
||||||
} else {
|
|
||||||
this.locator = selectorOrLocator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the form element locator
|
|
||||||
*/
|
|
||||||
get element(): Locator {
|
|
||||||
return this.locator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an input field within the form (properly scoped)
|
|
||||||
* @param inputSelector - Selector for the input field
|
|
||||||
*/
|
|
||||||
getInput(inputSelector: string): Input {
|
|
||||||
const scopedLocator = this.locator.locator(inputSelector);
|
|
||||||
return new Input(this.page, scopedLocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a button within the form (properly scoped)
|
|
||||||
* @param buttonSelector - Selector for the button
|
|
||||||
*/
|
|
||||||
getButton(buttonSelector: string): Button {
|
|
||||||
const scopedLocator = this.locator.locator(buttonSelector);
|
|
||||||
return new Button(this.page, scopedLocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the form is visible
|
|
||||||
*/
|
|
||||||
async isVisible(): Promise<boolean> {
|
|
||||||
return this.locator.isVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submits the form (triggers submit event)
|
|
||||||
*/
|
|
||||||
async submit(): Promise<void> {
|
|
||||||
await this.locator.evaluate((form: HTMLElement) => {
|
|
||||||
if (form instanceof HTMLFormElement) {
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for the form to be visible
|
|
||||||
* @param options - Optional wait options
|
|
||||||
*/
|
|
||||||
async waitForVisible(options?: { timeout?: number }): Promise<void> {
|
|
||||||
await this.locator.waitFor({ state: 'visible', ...options });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all form data as key-value pairs
|
|
||||||
* Useful for validation and debugging
|
|
||||||
*/
|
|
||||||
async getFormData(): Promise<Record<string, string>> {
|
|
||||||
return this.locator.evaluate((form: HTMLElement) => {
|
|
||||||
if (form instanceof HTMLFormElement) {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
result[key] = value.toString();
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +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 { Locator, Page } from '@playwright/test';
|
|
||||||
|
|
||||||
export class Input {
|
|
||||||
private readonly locator: Locator;
|
|
||||||
|
|
||||||
constructor(page: Page, selector: string);
|
|
||||||
|
|
||||||
constructor(page: Page, locator: Locator);
|
|
||||||
|
|
||||||
constructor(page: Page, selectorOrLocator: string | Locator) {
|
|
||||||
if (typeof selectorOrLocator === 'string') {
|
|
||||||
this.locator = page.locator(selectorOrLocator);
|
|
||||||
} else {
|
|
||||||
this.locator = selectorOrLocator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the input element locator
|
|
||||||
*/
|
|
||||||
get element(): Locator {
|
|
||||||
return this.locator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fast fill - clears the input and sets the value directly
|
|
||||||
* @param value - The value to fill
|
|
||||||
* @param options - Optional fill options
|
|
||||||
*/
|
|
||||||
async fill(
|
|
||||||
value: string,
|
|
||||||
options?: { timeout?: number; force?: boolean },
|
|
||||||
): Promise<void> {
|
|
||||||
await this.element.fill(value, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Types text character by character (simulates real typing)
|
|
||||||
* @param text - The text to type
|
|
||||||
* @param options - Optional typing options
|
|
||||||
*/
|
|
||||||
async type(text: string, options?: { delay?: number }): Promise<void> {
|
|
||||||
await this.element.type(text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Types text sequentially with more control over timing
|
|
||||||
* @param text - The text to type
|
|
||||||
* @param options - Optional sequential typing options
|
|
||||||
*/
|
|
||||||
async pressSequentially(
|
|
||||||
text: string,
|
|
||||||
options?: { delay?: number },
|
|
||||||
): Promise<void> {
|
|
||||||
await this.element.pressSequentially(text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the current value of the input
|
|
||||||
*/
|
|
||||||
async getValue(): Promise<string> {
|
|
||||||
return this.element.inputValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the input field
|
|
||||||
*/
|
|
||||||
async clear(): Promise<void> {
|
|
||||||
await this.element.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the input is visible
|
|
||||||
*/
|
|
||||||
async isVisible(): Promise<boolean> {
|
|
||||||
return this.element.isVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the input is enabled
|
|
||||||
*/
|
|
||||||
async isEnabled(): Promise<boolean> {
|
|
||||||
return this.element.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focuses on the input field
|
|
||||||
*/
|
|
||||||
async focus(): Promise<void> {
|
|
||||||
await this.element.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +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 { Page, Response } from '@playwright/test';
|
|
||||||
import { Form } from '../components/core';
|
|
||||||
import { URL } from '../utils/urls';
|
|
||||||
|
|
||||||
export class AuthPage {
|
|
||||||
private readonly page: Page;
|
|
||||||
|
|
||||||
private readonly loginForm: Form;
|
|
||||||
|
|
||||||
// Selectors specific to the auth/login page
|
|
||||||
private static readonly SELECTORS = {
|
|
||||||
LOGIN_FORM: '[data-test="login-form"]',
|
|
||||||
USERNAME_INPUT: '[data-test="username-input"]',
|
|
||||||
PASSWORD_INPUT: '[data-test="password-input"]',
|
|
||||||
LOGIN_BUTTON: '[data-test="login-button"]',
|
|
||||||
ERROR_SELECTORS: [
|
|
||||||
'[role="alert"]',
|
|
||||||
'.ant-form-item-explain-error',
|
|
||||||
'.ant-form-item-explain.ant-form-item-explain-error',
|
|
||||||
'.alert-danger',
|
|
||||||
],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
constructor(page: Page) {
|
|
||||||
this.page = page;
|
|
||||||
this.loginForm = new Form(page, AuthPage.SELECTORS.LOGIN_FORM);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the login page
|
|
||||||
*/
|
|
||||||
async goto(): Promise<void> {
|
|
||||||
await this.page.goto(URL.LOGIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for login form to be visible
|
|
||||||
*/
|
|
||||||
async waitForLoginForm(): Promise<void> {
|
|
||||||
await this.loginForm.waitForVisible({ timeout: 5000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login with provided credentials
|
|
||||||
* @param username - Username to enter
|
|
||||||
* @param password - Password to enter
|
|
||||||
*/
|
|
||||||
async loginWithCredentials(
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const usernameInput = this.loginForm.getInput(
|
|
||||||
AuthPage.SELECTORS.USERNAME_INPUT,
|
|
||||||
);
|
|
||||||
const passwordInput = this.loginForm.getInput(
|
|
||||||
AuthPage.SELECTORS.PASSWORD_INPUT,
|
|
||||||
);
|
|
||||||
const loginButton = this.loginForm.getButton(
|
|
||||||
AuthPage.SELECTORS.LOGIN_BUTTON,
|
|
||||||
);
|
|
||||||
|
|
||||||
await usernameInput.fill(username);
|
|
||||||
await passwordInput.fill(password);
|
|
||||||
await loginButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current page URL
|
|
||||||
*/
|
|
||||||
async getCurrentUrl(): Promise<string> {
|
|
||||||
return this.page.url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the session cookie specifically
|
|
||||||
*/
|
|
||||||
async getSessionCookie(): Promise<{ name: string; value: string } | null> {
|
|
||||||
const cookies = await this.page.context().cookies();
|
|
||||||
return cookies.find((c: any) => c.name === 'session') || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if login form has validation errors
|
|
||||||
*/
|
|
||||||
async hasLoginError(): Promise<boolean> {
|
|
||||||
const visibilityPromises = AuthPage.SELECTORS.ERROR_SELECTORS.map(
|
|
||||||
selector => this.page.locator(selector).isVisible(),
|
|
||||||
);
|
|
||||||
const visibilityResults = await Promise.all(visibilityPromises);
|
|
||||||
return visibilityResults.some((isVisible: any) => isVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for a login request to be made and return the response
|
|
||||||
*/
|
|
||||||
async waitForLoginRequest(): Promise<Response> {
|
|
||||||
return this.page.waitForResponse(
|
|
||||||
(response: any) =>
|
|
||||||
response.url().includes('/login/') &&
|
|
||||||
response.request().method() === 'POST',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +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 { test, expect } from '@playwright/test';
|
|
||||||
import { AuthPage } from '../../pages/AuthPage';
|
|
||||||
import { URL } from '../../utils/urls';
|
|
||||||
|
|
||||||
test.describe('Login view', () => {
|
|
||||||
let authPage: AuthPage;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }: any) => {
|
|
||||||
authPage = new AuthPage(page);
|
|
||||||
await authPage.goto();
|
|
||||||
await authPage.waitForLoginForm();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should redirect to login with incorrect username and password', async ({
|
|
||||||
page,
|
|
||||||
}: any) => {
|
|
||||||
// Setup request interception before login attempt
|
|
||||||
const loginRequestPromise = authPage.waitForLoginRequest();
|
|
||||||
|
|
||||||
// Attempt login with incorrect credentials
|
|
||||||
await authPage.loginWithCredentials('admin', 'wrongpassword');
|
|
||||||
|
|
||||||
// Wait for login request and verify response
|
|
||||||
const loginResponse = await loginRequestPromise;
|
|
||||||
// Failed login returns 401 Unauthorized or 302 redirect to login
|
|
||||||
expect([401, 302]).toContain(loginResponse.status());
|
|
||||||
|
|
||||||
// Wait for redirect to complete before checking URL
|
|
||||||
await page.waitForURL((url: any) => url.pathname.endsWith('login/'), {
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify we stay on login page
|
|
||||||
const currentUrl = await authPage.getCurrentUrl();
|
|
||||||
expect(currentUrl).toContain(URL.LOGIN);
|
|
||||||
|
|
||||||
// Verify error message is shown
|
|
||||||
const hasError = await authPage.hasLoginError();
|
|
||||||
expect(hasError).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should login with correct username and password', async ({
|
|
||||||
page,
|
|
||||||
}: any) => {
|
|
||||||
// Setup request interception before login attempt
|
|
||||||
const loginRequestPromise = authPage.waitForLoginRequest();
|
|
||||||
|
|
||||||
// Login with correct credentials
|
|
||||||
await authPage.loginWithCredentials('admin', 'general');
|
|
||||||
|
|
||||||
// Wait for login request and verify response
|
|
||||||
const loginResponse = await loginRequestPromise;
|
|
||||||
// Successful login returns 302 redirect
|
|
||||||
expect(loginResponse.status()).toBe(302);
|
|
||||||
|
|
||||||
// Wait for successful redirect to welcome page
|
|
||||||
await page.waitForURL(
|
|
||||||
(url: any) => url.pathname.endsWith('superset/welcome/'),
|
|
||||||
{
|
|
||||||
timeout: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify specific session cookie exists
|
|
||||||
const sessionCookie = await authPage.getSessionCookie();
|
|
||||||
expect(sessionCookie).not.toBeNull();
|
|
||||||
expect(sessionCookie?.value).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,14 +17,11 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function roundDecimal(
|
export default function roundDecimal(number, precision) {
|
||||||
number: number,
|
let roundedNumber;
|
||||||
precision?: number,
|
let p = precision;
|
||||||
): number {
|
|
||||||
let roundedNumber: number;
|
|
||||||
if (precision) {
|
if (precision) {
|
||||||
const p = 10 ** precision;
|
roundedNumber = Math.round(number * (p = 10 ** p)) / p;
|
||||||
roundedNumber = Math.round(number * p) / p;
|
|
||||||
} else {
|
} else {
|
||||||
roundedNumber = Math.round(number);
|
roundedNumber = Math.round(number);
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": false,
|
"composite": false,
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
|
"noEmit": true,
|
||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"extends": "../../../tsconfig.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"include": ["**/*", "../types/**/*", "../../../types/**/*"]
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"../types/**/*",
|
||||||
|
"../../../types/**/*"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Path Resolution: Override baseUrl to maintain correct path mappings from parent config
|
"composite": true,
|
||||||
// (e.g., "@apache-superset/core" -> "./packages/superset-core/src")
|
|
||||||
"baseUrl": "../..",
|
|
||||||
|
|
||||||
// Directory Overrides: Parent config paths are relative to frontend root,
|
|
||||||
// but packages need paths relative to their own directory
|
|
||||||
"outDir": "lib",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"declarationDir": "lib"
|
"outDir": "lib",
|
||||||
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"],
|
"include": ["src/**/*", "types/**/*"],
|
||||||
"exclude": [
|
"exclude": ["lib", "test"],
|
||||||
"src/**/*.js",
|
|
||||||
"src/**/*.jsx",
|
|
||||||
"src/**/*.test.*",
|
|
||||||
"src/**/*.stories.*"
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../packages/superset-core" },
|
|
||||||
{ "path": "../../packages/superset-ui-core" },
|
{ "path": "../../packages/superset-ui-core" },
|
||||||
{ "path": "../../packages/superset-ui-chart-controls" }
|
{ "path": "../../packages/superset-ui-chart-controls" }
|
||||||
]
|
]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user