mirror of
https://github.com/apache/superset.git
synced 2026-04-21 00:54:44 +00:00
docs(components): federate Storybook stories into Developer Portal MDX (#37502)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,12 @@ const StyledBlurredSection = styled('section')`
|
||||
|
||||
interface BlurredSectionProps {
|
||||
children: ReactNode;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const BlurredSection = ({ children }: BlurredSectionProps) => {
|
||||
const BlurredSection = ({ children, id }: BlurredSectionProps) => {
|
||||
return (
|
||||
<StyledBlurredSection>
|
||||
<StyledBlurredSection id={id}>
|
||||
{children}
|
||||
<img className="blur" src="/img/community/blur.png" alt="Blur" />
|
||||
</StyledBlurredSection>
|
||||
|
||||
@@ -18,33 +18,245 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
// A simple component to display a story example
|
||||
export function StoryExample({ component: Component, props = {} }) {
|
||||
// Lazy-loaded component registry - populated on first use in browser
|
||||
let componentRegistry = null;
|
||||
let SupersetProviders = null;
|
||||
|
||||
function getComponentRegistry() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {}; // SSR - return empty
|
||||
}
|
||||
|
||||
if (componentRegistry !== null) {
|
||||
return componentRegistry; // Already loaded
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const antd = require('antd');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CoreUI = require('@apache-superset/core/ui');
|
||||
|
||||
// Build component registry with antd as base fallback layer.
|
||||
// Some Superset components (e.g., Typography) use styled-components that may
|
||||
// fail to initialize in the docs build. Antd originals serve as fallbacks.
|
||||
componentRegistry = { ...antd, ...SupersetComponents, ...CoreUI };
|
||||
|
||||
return componentRegistry;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load components:', error);
|
||||
componentRegistry = {};
|
||||
return componentRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
function getProviders() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ({ children }) => children; // SSR
|
||||
}
|
||||
|
||||
if (SupersetProviders !== null) {
|
||||
return SupersetProviders;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App, ConfigProvider } = require('antd');
|
||||
|
||||
// Configure Ant Design to render portals (tooltips, dropdowns, etc.)
|
||||
// inside the closest .storybook-example container instead of document.body
|
||||
// This fixes positioning issues in the docs pages
|
||||
const getPopupContainer = (triggerNode) => {
|
||||
// Find the closest .storybook-example container
|
||||
const container = triggerNode?.closest?.('.storybook-example');
|
||||
return container || document.body;
|
||||
};
|
||||
|
||||
SupersetProviders = ({ children }) => (
|
||||
<themeObject.SupersetThemeProvider>
|
||||
<ConfigProvider
|
||||
getPopupContainer={getPopupContainer}
|
||||
getTargetContainer={() => document.body}
|
||||
>
|
||||
<App>{children}</App>
|
||||
</ConfigProvider>
|
||||
</themeObject.SupersetThemeProvider>
|
||||
);
|
||||
return SupersetProviders;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load providers:', error);
|
||||
return ({ children }) => children;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a value is a valid React component (function, forwardRef, memo, etc.)
|
||||
function isReactComponent(value) {
|
||||
if (!value) return false;
|
||||
// Function/class components
|
||||
if (typeof value === 'function') return true;
|
||||
// forwardRef, memo, lazy — React wraps these as objects with $$typeof
|
||||
if (typeof value === 'object' && value.$$typeof) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve component from string name or React component
|
||||
// Supports dot notation for nested components (e.g., 'Icons.InfoCircleOutlined')
|
||||
function resolveComponent(component) {
|
||||
if (!component) return null;
|
||||
// If already a component (function/class/forwardRef), return as-is
|
||||
if (isReactComponent(component)) return component;
|
||||
// If string, look up in registry
|
||||
if (typeof component === 'string') {
|
||||
const registry = getComponentRegistry();
|
||||
// Handle dot notation (e.g., 'Icons.InfoCircleOutlined')
|
||||
if (component.includes('.')) {
|
||||
const parts = component.split('.');
|
||||
let current = registry[parts[0]];
|
||||
for (let i = 1; i < parts.length && current; i++) {
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return isReactComponent(current) ? current : null;
|
||||
}
|
||||
return registry[component] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loading placeholder for SSR
|
||||
function LoadingPlaceholder() {
|
||||
return (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
{Component && <Component {...props} />}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
minHeight: '100px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#999',
|
||||
}}
|
||||
>
|
||||
Loading component...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story with controls
|
||||
export function StoryWithControls({
|
||||
component: Component,
|
||||
props = {},
|
||||
controls = [],
|
||||
}) {
|
||||
// A simple component to display a story example
|
||||
export function StoryExample({ component, props = {} }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
const { children, restProps } = extractChildren(props);
|
||||
return (
|
||||
<Providers>
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component ? (
|
||||
<Component {...restProps}>{children}</Component>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Props that should be rendered as children rather than passed as props
|
||||
const CHILDREN_PROP_NAMES = ['label', 'children', 'text', 'content'];
|
||||
|
||||
// Extract children from props based on common conventions
|
||||
function extractChildren(props) {
|
||||
for (const propName of CHILDREN_PROP_NAMES) {
|
||||
if (props[propName] !== undefined && props[propName] !== null && props[propName] !== '') {
|
||||
const { [propName]: childContent, ...restProps } = props;
|
||||
return { children: childContent, restProps };
|
||||
}
|
||||
}
|
||||
return { children: null, restProps: props };
|
||||
}
|
||||
|
||||
// Generate sample children for layout components
|
||||
// Supports:
|
||||
// - Array of strings: ['Item 1', 'Item 2'] - renders as styled divs
|
||||
// - Array of component descriptors: [{ component: 'Button', props: { children: 'Click' } }]
|
||||
// - Number: 3 - generates that many sample items
|
||||
// - String: 'content' - renders as literal content
|
||||
function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
|
||||
if (!sampleChildren) return null;
|
||||
|
||||
// Default style if none provided (minimal, just enough to see items)
|
||||
const itemStyle = sampleChildrenStyle || {};
|
||||
|
||||
// If it's an array, check if items are component descriptors or strings
|
||||
if (Array.isArray(sampleChildren)) {
|
||||
return sampleChildren.map((item, i) => {
|
||||
// Component descriptor: { component: 'Button', props: { ... } }
|
||||
if (item && typeof item === 'object' && item.component) {
|
||||
const ChildComponent = resolveComponent(item.component);
|
||||
if (ChildComponent) {
|
||||
return <ChildComponent key={i} {...item.props} />;
|
||||
}
|
||||
// Fallback if component not found
|
||||
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
|
||||
}
|
||||
// Simple string
|
||||
return (
|
||||
<div key={i} style={itemStyle}>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a number, generate that many sample items
|
||||
if (typeof sampleChildren === 'number') {
|
||||
return new Array(sampleChildren).fill(null).map((_, i) => (
|
||||
<div key={i} style={itemStyle}>
|
||||
Item {i + 1}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
// If it's a string, treat as literal content
|
||||
if (typeof sampleChildren === 'string') {
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
// Inner component for StoryWithControls (browser-only)
|
||||
// renderComponent allows overriding which component to actually render (useful when the named
|
||||
// component is a namespace object like Icons, not a React component)
|
||||
// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility
|
||||
function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
// Use renderComponent if provided, otherwise use the main component name
|
||||
const componentToRender = renderComponent || component;
|
||||
const Component = resolveComponent(componentToRender);
|
||||
const Providers = getProviders();
|
||||
const [stateProps, setStateProps] = React.useState(props);
|
||||
|
||||
const updateProp = (key, value) => {
|
||||
@@ -54,8 +266,77 @@ export function StoryWithControls({
|
||||
}));
|
||||
};
|
||||
|
||||
// Extract children from props (label, children, text, content)
|
||||
// When sampleChildren is explicitly provided, skip extraction so all props
|
||||
// (like 'content') stay as component props rather than becoming children
|
||||
const { children: propsChildren, restProps } = sampleChildren
|
||||
? { children: null, restProps: stateProps }
|
||||
: extractChildren(stateProps);
|
||||
// Filter out undefined values so they don't override component defaults
|
||||
const filteredProps = Object.fromEntries(
|
||||
Object.entries(restProps).filter(([, v]) => v !== undefined)
|
||||
);
|
||||
|
||||
// Resolve any prop values that are component descriptors
|
||||
// e.g., { component: 'Button', props: { children: 'Click' } }
|
||||
// Also resolves descriptors nested inside array items:
|
||||
// e.g., items: [{ id: 'x', element: { component: 'div', props: { children: 'text' } } }]
|
||||
Object.keys(filteredProps).forEach(key => {
|
||||
const value = filteredProps[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value) && value.component) {
|
||||
const PropComponent = resolveComponent(value.component);
|
||||
if (PropComponent) {
|
||||
filteredProps[key] = <PropComponent {...value.props} />;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
filteredProps[key] = value.map((item, idx) => {
|
||||
if (item && typeof item === 'object') {
|
||||
const resolved = { ...item };
|
||||
Object.keys(resolved).forEach(field => {
|
||||
const fieldValue = resolved[field];
|
||||
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) {
|
||||
const FieldComponent = resolveComponent(fieldValue.component);
|
||||
if (FieldComponent) {
|
||||
resolved[field] = React.createElement(FieldComponent, { key: `${key}-${idx}`, ...fieldValue.props });
|
||||
}
|
||||
}
|
||||
});
|
||||
return resolved;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// For List-like components with dataSource but no renderItem, provide a default
|
||||
if (filteredProps.dataSource && !filteredProps.renderItem) {
|
||||
const ListItem = resolveComponent('List')?.Item;
|
||||
filteredProps.renderItem = (item) =>
|
||||
ListItem
|
||||
? React.createElement(ListItem, null, String(item))
|
||||
: React.createElement('div', null, String(item));
|
||||
}
|
||||
|
||||
// Use sample children if provided, otherwise use props children
|
||||
const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren;
|
||||
|
||||
// For components with a trigger (like Modal with show/onHide), add handlers.
|
||||
// onHideProp supports comma-separated names for components with multiple close
|
||||
// callbacks (e.g., "onHide,handleSave,onConfirmNavigation").
|
||||
const triggerProps = {};
|
||||
if (triggerProp && onHideProp) {
|
||||
const closeHandler = () => updateProp(triggerProp, false);
|
||||
onHideProp.split(',').forEach(prop => {
|
||||
triggerProps[prop.trim()] = closeHandler;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the Button component for trigger buttons
|
||||
const ButtonComponent = resolveComponent('Button');
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Providers>
|
||||
<div className="storybook-with-controls">
|
||||
<div
|
||||
className="storybook-example"
|
||||
@@ -64,9 +345,24 @@ export function StoryWithControls({
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component && <Component {...stateProps} />}
|
||||
{Component ? (
|
||||
<>
|
||||
{/* Show a trigger button for components like Modal */}
|
||||
{triggerProp && ButtonComponent && (
|
||||
<ButtonComponent onClick={() => updateProp(triggerProp, true)}>
|
||||
Open {component}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
<Component {...filteredProps} {...triggerProps}>{children}</Component>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(componentToRender)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{controls.length > 0 && (
|
||||
@@ -87,26 +383,64 @@ export function StoryWithControls({
|
||||
</label>
|
||||
{control.type === 'select' ? (
|
||||
<select
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value || undefined)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
>
|
||||
{control.options.map(option => (
|
||||
<option value="">— None —</option>
|
||||
{control.options?.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : control.type === 'inline-radio' || control.type === 'radio' ? (
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{control.options?.map(option => (
|
||||
<label
|
||||
key={option}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={control.name}
|
||||
value={option}
|
||||
checked={stateProps[control.name] === option}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : control.type === 'boolean' ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, e.target.checked)}
|
||||
/>
|
||||
) : control.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, Number(e.target.value))}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
) : control.type === 'color' ? (
|
||||
<input
|
||||
type="color"
|
||||
value={stateProps[control.name] || '#000000'}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{
|
||||
width: '50px',
|
||||
height: '30px',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={stateProps[control.name]}
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
@@ -116,6 +450,81 @@ export function StoryWithControls({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story with controls
|
||||
// renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons')
|
||||
// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide")
|
||||
export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<StoryWithControlsInner
|
||||
component={Component}
|
||||
renderComponent={renderComponent}
|
||||
props={props}
|
||||
controls={controls}
|
||||
sampleChildren={sampleChildren}
|
||||
sampleChildrenStyle={sampleChildrenStyle}
|
||||
triggerProp={triggerProp}
|
||||
onHideProp={onHideProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Inner component for ComponentGallery (browser-only)
|
||||
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<div className="component-gallery">
|
||||
{sizes.map(size => (
|
||||
<div key={size} style={{ marginBottom: 40 }}>
|
||||
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
|
||||
{styles.map(style => (
|
||||
<Component
|
||||
key={`${style}_${size}`}
|
||||
{...{ [sizeProp]: size, [styleProp]: style }}
|
||||
>
|
||||
{style}
|
||||
</Component>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A component to display a gallery of all variants (sizes x styles)
|
||||
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<ComponentGalleryInner
|
||||
component={component}
|
||||
sizes={sizes}
|
||||
styles={styles}
|
||||
sizeProp={sizeProp}
|
||||
styleProp={styleProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ const Community = () => {
|
||||
)}
|
||||
</ul>
|
||||
</StyledJoinCommunity>
|
||||
<BlurredSection>
|
||||
<BlurredSection id="superset-community-calendar">
|
||||
<SectionHeader
|
||||
level="h2"
|
||||
title="Superset Community Calendar"
|
||||
|
||||
118
docs/src/shims/null-module.js
Normal file
118
docs/src/shims/null-module.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Null module shim for packages not available in the docs build.
|
||||
// These are transitive dependencies of superset-frontend components that exist
|
||||
// in the barrel file but are never rendered on the docs site.
|
||||
// webpack needs these to resolve at build time even though the code paths
|
||||
// that use them are never executed at runtime.
|
||||
//
|
||||
// This shim uses a recursive Proxy to handle nested property access chains:
|
||||
// import ace from 'ace-builds'; ace.config.set(...) → works (returns proxy)
|
||||
// import { useResizeDetector } from 'react-resize-detector' → returns noop hook
|
||||
// import ReactAce from 'react-ace' → returns NullComponent
|
||||
|
||||
const NullComponent = () => null;
|
||||
|
||||
// For hooks that return objects/arrays
|
||||
const useNoop = () => ({});
|
||||
|
||||
// Mock for useResizeDetector - returns { ref, width, height } where ref.current exists
|
||||
const useResizeDetectorMock = () => ({
|
||||
ref: { current: null },
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a recursive proxy that handles any depth of property access.
|
||||
* This allows patterns like ace.config.set() or ace.config.setModuleUrl() to work.
|
||||
*
|
||||
* The proxy is both callable (returns undefined) and accessible (returns another proxy).
|
||||
*/
|
||||
function createDeepProxy() {
|
||||
const handler = {
|
||||
// Handle property access - return another proxy for chaining
|
||||
get(target, prop) {
|
||||
// Standard module properties
|
||||
if (prop === 'default') return createDeepProxy();
|
||||
if (prop === '__esModule') return true;
|
||||
|
||||
// Symbol properties (used by JS internals)
|
||||
if (typeof prop === 'symbol') {
|
||||
if (prop === Symbol.toPrimitive) return () => '';
|
||||
if (prop === Symbol.toStringTag) return 'NullModule';
|
||||
if (prop === Symbol.iterator) return undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// React-specific properties
|
||||
if (prop === '$$typeof') return undefined;
|
||||
if (prop === 'propTypes') return undefined;
|
||||
if (prop === 'displayName') return 'NullComponent';
|
||||
|
||||
// Specific hook mocks for known hooks that need proper return values
|
||||
if (prop === 'useResizeDetector') {
|
||||
return useResizeDetectorMock;
|
||||
}
|
||||
|
||||
// Common hook names return useNoop for better compatibility
|
||||
if (typeof prop === 'string' && prop.startsWith('use')) {
|
||||
return useNoop;
|
||||
}
|
||||
|
||||
// Return another proxy to allow further chaining (ace.config.set)
|
||||
return createDeepProxy();
|
||||
},
|
||||
|
||||
// Handle function calls - return undefined (safe default)
|
||||
apply() {
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// Handle new ClassName() - return an empty object
|
||||
construct() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
// Create a proxy over a function so it's both callable and has properties
|
||||
return new Proxy(function NullModule() {}, handler);
|
||||
}
|
||||
|
||||
// Create the main module export as a deep proxy
|
||||
const nullModule = createDeepProxy();
|
||||
|
||||
// Support both CommonJS and ES module patterns
|
||||
module.exports = nullModule;
|
||||
module.exports.default = createDeepProxy();
|
||||
module.exports.__esModule = true;
|
||||
|
||||
// Named exports for common patterns (webpack may inline these)
|
||||
module.exports.useResizeDetector = useResizeDetectorMock;
|
||||
module.exports.withResizeDetector = createDeepProxy();
|
||||
module.exports.Resizable = NullComponent;
|
||||
module.exports.ResizableBox = NullComponent;
|
||||
module.exports.FixedSizeList = NullComponent;
|
||||
module.exports.VariableSizeList = NullComponent;
|
||||
|
||||
// ace-builds specific exports that CodeEditor uses
|
||||
module.exports.config = createDeepProxy();
|
||||
module.exports.require = createDeepProxy();
|
||||
module.exports.edit = createDeepProxy();
|
||||
54
docs/src/shims/react-table.js
vendored
Normal file
54
docs/src/shims/react-table.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Shim for react-table to handle CommonJS to ES module interop
|
||||
// react-table v7 is CommonJS, but Superset components import it with ES module syntax
|
||||
// Use relative path to avoid circular dependency since webpack aliases 'react-table' to this file
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS interop shim for react-table v7
|
||||
const reactTable = require('../../node_modules/react-table');
|
||||
|
||||
// Re-export all named exports
|
||||
export const {
|
||||
useTable,
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useGlobalFilter,
|
||||
useRowSelect,
|
||||
useRowState,
|
||||
useColumnOrder,
|
||||
useExpanded,
|
||||
useGroupBy,
|
||||
useResizeColumns,
|
||||
useBlockLayout,
|
||||
useAbsoluteLayout,
|
||||
useFlexLayout,
|
||||
actions,
|
||||
defaultColumn,
|
||||
makePropGetter,
|
||||
reduceHooks,
|
||||
loopHooks,
|
||||
ensurePluginOrder,
|
||||
functionalUpdate,
|
||||
useGetLatest,
|
||||
safeUseLayoutEffect,
|
||||
} = reactTable;
|
||||
|
||||
// Default export
|
||||
export default reactTable;
|
||||
@@ -264,3 +264,193 @@ ul.dropdown__menu svg {
|
||||
.menu__list-item.delete.api-method > .menu__link::before {
|
||||
background-color: #f93e3e;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Component Example Isolation
|
||||
Prevents Docusaurus/Infima styles from bleeding into Superset components
|
||||
============================================ */
|
||||
|
||||
/* Reset link styles inside component examples */
|
||||
.storybook-example a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
.storybook-example a:hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Reset list styles */
|
||||
.storybook-example ul,
|
||||
.storybook-example ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Override Infima's .markdown li + li margin */
|
||||
.storybook-example li + li,
|
||||
.markdown .storybook-example li + li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Reset heading styles */
|
||||
.storybook-example h1,
|
||||
.storybook-example h2,
|
||||
.storybook-example h3,
|
||||
.storybook-example h4,
|
||||
.storybook-example h5,
|
||||
.storybook-example h6 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/* Reset paragraph margins */
|
||||
.storybook-example p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Reset table margins - Infima applies margin-bottom via --ifm-spacing-vertical */
|
||||
.storybook-example table {
|
||||
margin: 0;
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* Ensure Ant Design components render correctly */
|
||||
.storybook-example .ant-breadcrumb {
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
.storybook-example .ant-breadcrumb a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.storybook-example .ant-breadcrumb a:hover {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Ant Design Popup/Portal Isolation
|
||||
These components render outside .storybook-example via portals
|
||||
============================================ */
|
||||
|
||||
/* DatePicker, TimePicker dropdown panels - reset Infima table styles
|
||||
Using doubled selectors for higher specificity than Infima's defaults */
|
||||
.ant-picker-dropdown.ant-picker-dropdown table,
|
||||
.ant-picker-dropdown.ant-picker-dropdown thead,
|
||||
.ant-picker-dropdown.ant-picker-dropdown tbody,
|
||||
.ant-picker-dropdown.ant-picker-dropdown tr,
|
||||
.ant-picker-dropdown.ant-picker-dropdown th,
|
||||
.ant-picker-dropdown.ant-picker-dropdown td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-picker-dropdown.ant-picker-dropdown table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
|
||||
/* Override Infima's zebra striping with higher specificity */
|
||||
.ant-picker-dropdown.ant-picker-dropdown tr:nth-child(2n),
|
||||
.ant-picker-dropdown.ant-picker-dropdown tbody tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-picker-dropdown.ant-picker-dropdown th,
|
||||
.ant-picker-dropdown.ant-picker-dropdown td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Select, Dropdown, Popover portals */
|
||||
.ant-select-dropdown.ant-select-dropdown table,
|
||||
.ant-select-dropdown.ant-select-dropdown thead,
|
||||
.ant-select-dropdown.ant-select-dropdown tbody,
|
||||
.ant-select-dropdown.ant-select-dropdown tr,
|
||||
.ant-select-dropdown.ant-select-dropdown th,
|
||||
.ant-select-dropdown.ant-select-dropdown td,
|
||||
.ant-dropdown.ant-dropdown table,
|
||||
.ant-dropdown.ant-dropdown thead,
|
||||
.ant-dropdown.ant-dropdown tbody,
|
||||
.ant-dropdown.ant-dropdown tr,
|
||||
.ant-dropdown.ant-dropdown th,
|
||||
.ant-dropdown.ant-dropdown td,
|
||||
.ant-popover.ant-popover table,
|
||||
.ant-popover.ant-popover thead,
|
||||
.ant-popover.ant-popover tbody,
|
||||
.ant-popover.ant-popover tr,
|
||||
.ant-popover.ant-popover th,
|
||||
.ant-popover.ant-popover td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-select-dropdown.ant-select-dropdown tr:nth-child(2n),
|
||||
.ant-dropdown.ant-dropdown tr:nth-child(2n),
|
||||
.ant-popover.ant-popover tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Modal portals */
|
||||
.ant-modal.ant-modal table,
|
||||
.ant-modal.ant-modal thead,
|
||||
.ant-modal.ant-modal tbody,
|
||||
.ant-modal.ant-modal tr,
|
||||
.ant-modal.ant-modal th,
|
||||
.ant-modal.ant-modal td {
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-modal.ant-modal tr:nth-child(2n) {
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Live Code Editor Height Limits
|
||||
Prevents tall code blocks from dominating the page
|
||||
============================================ */
|
||||
|
||||
/* Limit the code editor height and make it scrollable */
|
||||
/* Target multiple possible class names used by Docusaurus/react-live */
|
||||
.playgroundEditor,
|
||||
[class*="playgroundEditor"],
|
||||
.live-editor,
|
||||
[class*="liveEditor"] {
|
||||
max-height: 350px !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* The actual textarea/code area inside the editor */
|
||||
.playgroundEditor textarea,
|
||||
.playgroundEditor pre,
|
||||
[class*="playgroundEditor"] textarea,
|
||||
[class*="playgroundEditor"] pre {
|
||||
max-height: 350px !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
/* Also limit the preview area for consistency */
|
||||
.playgroundPreview,
|
||||
[class*="playgroundPreview"] {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Hide sidebar items with sidebar_class_name: hidden in frontmatter */
|
||||
.menu__list-item.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
10
docs/src/theme.d.ts
vendored
10
docs/src/theme.d.ts
vendored
@@ -30,3 +30,13 @@ declare module '@theme/Layout' {
|
||||
|
||||
export default function Layout(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Header' {
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children?: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundHeader(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
107
docs/src/theme/Playground/Preview/index.tsx
Normal file
107
docs/src/theme/Playground/Preview/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from 'react';
|
||||
import { LiveError, LivePreview } from 'react-live';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import { ErrorBoundaryErrorMessageFallback } from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Get the theme wrapper for Superset components
|
||||
function getThemeWrapper() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App } = require('antd');
|
||||
|
||||
if (!themeObject?.SupersetThemeProvider) {
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<themeObject.SupersetThemeProvider>
|
||||
<App>{children}</App>
|
||||
</themeObject.SupersetThemeProvider>
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('[PlaygroundPreview] Failed to load theme provider:', e);
|
||||
return ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
function Loader() {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function ThemedLivePreview(): ReactNode {
|
||||
const ThemeWrapper = getThemeWrapper();
|
||||
return (
|
||||
<ThemeWrapper>
|
||||
<LivePreview />
|
||||
</ThemeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function PlaygroundLivePreview(): ReactNode {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<Loader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}
|
||||
>
|
||||
<ThemedLivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlaygroundPreview(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks"
|
||||
>
|
||||
Result
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<div className={styles.playgroundPreview}>
|
||||
<PlaygroundLivePreview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
docs/src/theme/Playground/Preview/styles.module.css
Normal file
23
docs/src/theme/Playground/Preview/styles.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
||||
@@ -18,36 +18,49 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Card, Input, Space, Tag, Tooltip } from 'antd';
|
||||
|
||||
// Import extension components from @apache-superset/core/ui
|
||||
// This matches the established pattern used throughout the Superset codebase
|
||||
// Resolved via webpack alias to superset-frontend/packages/superset-core/src/ui/components
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
// Browser-only check for SSR safety
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
/**
|
||||
* ReactLiveScope provides the scope for live code blocks.
|
||||
* Any component added here will be available in ```tsx live blocks.
|
||||
*
|
||||
* To add more components:
|
||||
* 1. Import the component from @apache-superset/core above
|
||||
* 2. Add it to the scope object below
|
||||
* Components are conditionally loaded only in the browser to avoid
|
||||
* SSG issues with Emotion CSS-in-JS jsx runtime.
|
||||
*
|
||||
* Components are available by name, e.g.:
|
||||
* <Button>Click me</Button>
|
||||
* <Avatar size="large" />
|
||||
* <Badge count={5} />
|
||||
*/
|
||||
const ReactLiveScope = {
|
||||
|
||||
// Base scope with React (always available)
|
||||
const ReactLiveScope: Record<string, unknown> = {
|
||||
// React core
|
||||
React,
|
||||
...React,
|
||||
|
||||
// Extension components from @apache-superset/core
|
||||
Alert,
|
||||
|
||||
// Common Ant Design components (for demos)
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
};
|
||||
|
||||
// Only load Superset components in browser context
|
||||
// This prevents SSG errors from Emotion CSS-in-JS
|
||||
if (isBrowser) {
|
||||
try {
|
||||
// Dynamic require for browser-only execution
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Alert } = require('@apache-superset/core/ui');
|
||||
|
||||
console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10));
|
||||
console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {}));
|
||||
|
||||
Object.assign(ReactLiveScope, SupersetComponents, { Alert });
|
||||
|
||||
console.log('[ReactLiveScope] Final scope keys:', Object.keys(ReactLiveScope).slice(0, 20));
|
||||
} catch (e) {
|
||||
console.error('[ReactLiveScope] Failed to load Superset components:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactLiveScope;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import type { Plugin } from '@docusaurus/types';
|
||||
|
||||
export default function webpackExtendPlugin(): Plugin<void> {
|
||||
@@ -26,12 +27,73 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
configureWebpack(config) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Use NormalModuleReplacementPlugin to forcefully replace react-table
|
||||
// This is necessary because regular aliases don't work for modules in nested node_modules
|
||||
const reactTableShim = path.resolve(__dirname, './shims/react-table.js');
|
||||
config.plugins?.push(
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
/^react-table$/,
|
||||
reactTableShim,
|
||||
),
|
||||
);
|
||||
|
||||
// Stub out heavy third-party packages that are transitive dependencies of
|
||||
// superset-frontend components. The barrel file (components/index.ts)
|
||||
// re-exports all components, so webpack must resolve their imports even
|
||||
// though these components are never rendered on the docs site.
|
||||
const nullModuleShim = path.resolve(__dirname, './shims/null-module.js');
|
||||
const heavyDepsPatterns = [
|
||||
/^brace(\/|$)/, // ACE editor modes/themes
|
||||
/^react-ace(\/|$)/,
|
||||
/^ace-builds(\/|$)/,
|
||||
/^react-js-cron(\/|$)/, // Cron picker + CSS
|
||||
// react-resize-detector: NOT shimmed — DropdownContainer needs it at runtime
|
||||
// for overflow detection. Resolves from superset-frontend/node_modules.
|
||||
/^react-window(\/|$)/,
|
||||
/^re-resizable(\/|$)/,
|
||||
/^react-draggable(\/|$)/,
|
||||
/^ag-grid-react(\/|$)/,
|
||||
/^ag-grid-community(\/|$)/,
|
||||
];
|
||||
heavyDepsPatterns.forEach(pattern => {
|
||||
config.plugins?.push(
|
||||
new webpack.NormalModuleReplacementPlugin(pattern, nullModuleShim),
|
||||
);
|
||||
});
|
||||
|
||||
// Add YAML loader rule directly to existing rules
|
||||
config.module?.rules?.push({
|
||||
test: /\.ya?ml$/,
|
||||
use: 'js-yaml-loader',
|
||||
});
|
||||
|
||||
// Add babel-loader rule for superset-frontend files
|
||||
// This ensures Emotion CSS-in-JS is processed correctly for SSG
|
||||
const supersetFrontendPath = path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend',
|
||||
);
|
||||
config.module?.rules?.push({
|
||||
test: /\.(tsx?|jsx?)$/,
|
||||
include: supersetFrontendPath,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic',
|
||||
importSource: '@emotion/react',
|
||||
},
|
||||
],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['@emotion/babel-plugin'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
devtool: isDev ? 'eval-source-map' : config.devtool,
|
||||
...(isDev && {
|
||||
@@ -44,8 +106,16 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
},
|
||||
}),
|
||||
resolve: {
|
||||
// Add superset-frontend node_modules to module resolution
|
||||
modules: [
|
||||
...(config.resolve?.modules || []),
|
||||
path.resolve(__dirname, '../../superset-frontend/node_modules'),
|
||||
],
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
// Ensure single React instance across all modules (critical for hooks to work)
|
||||
react: path.resolve(__dirname, '../node_modules/react'),
|
||||
'react-dom': path.resolve(__dirname, '../node_modules/react-dom'),
|
||||
// Allow importing from superset-frontend
|
||||
src: path.resolve(__dirname, '../../superset-frontend/src'),
|
||||
// '@superset-ui/core': path.resolve(
|
||||
@@ -58,14 +128,29 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-ui-core/src/components',
|
||||
),
|
||||
// Extension API package - allows docs to import from @apache-superset/core/ui
|
||||
// This matches the established pattern used throughout the Superset codebase
|
||||
// Point directly to components to avoid importing theme (which has font dependencies)
|
||||
// Note: TypeScript types come from docs/src/types/apache-superset-core (see tsconfig.json)
|
||||
// This split is intentional: webpack resolves actual source, tsconfig provides simplified types
|
||||
// Also alias the full package path for internal imports within components
|
||||
'@superset-ui/core/components': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-ui-core/src/components',
|
||||
),
|
||||
// Use a shim for react-table to handle CommonJS to ES module interop
|
||||
// react-table v7 is CommonJS, but Superset components import it with ES module syntax
|
||||
'react-table': path.resolve(__dirname, './shims/react-table.js'),
|
||||
// Extension API package - resolve @apache-superset/core and its sub-paths
|
||||
// to source so the docs build doesn't depend on pre-built lib/ artifacts.
|
||||
// More specific sub-path aliases must come first; webpack matches the
|
||||
// longest prefix.
|
||||
'@apache-superset/core/ui': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src/ui/components',
|
||||
'../../superset-frontend/packages/superset-core/src/ui',
|
||||
),
|
||||
'@apache-superset/core/api/core': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src/api/core',
|
||||
),
|
||||
'@apache-superset/core': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src',
|
||||
),
|
||||
// Add proper Storybook aliases
|
||||
'@storybook/blocks': path.resolve(
|
||||
|
||||
Reference in New Issue
Block a user