Spaces:
Paused
Paused
Download docs/src/components/StorybookWrapper.jsx from Leon4gr45/powerbi: direct link, hf CLI and curl.
- Browser
- Download file 21.7 kB
-
https://huggingface.co/spaces/Leon4gr45/powerbi/resolve/main/docs/src/components/StorybookWrapper.jsx
- Command line
-
hf download hf://spaces/Leon4gr45/powerbi/docs/src/components/StorybookWrapper.jsx
-
curl -L -o StorybookWrapper.jsx https://huggingface.co/spaces/Leon4gr45/powerbi/resolve/main/docs/src/components/StorybookWrapper.jsx
21.7 kB
| /** | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import React from 'react'; | |
| import BrowserOnly from '@docusaurus/BrowserOnly'; | |
| // 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/components'); | |
| // 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/theme'); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const { App, ConfigProvider } = require('antd'); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const { useColorMode } = require('@docusaurus/theme-common'); | |
| // 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; | |
| }; | |
| // `themeObject` is a module-level singleton (superset-core/src/theme | |
| // index.tsx: `Theme.fromConfig()`), created once with no dark/light | |
| // config, so SupersetThemeProvider always rendered whatever that default | |
| // algorithm was -- it had no way to know about Docusaurus's theme toggle. | |
| // Docusaurus tracks the toggle in React context (useColorMode), so | |
| // mirror it onto the singleton via the toggleDarkMode() method Theme | |
| // already exposes for exactly this purpose. | |
| // | |
| // Use useLayoutEffect (not useEffect) so the sync runs before the | |
| // browser paints. This component only ever mounts client-side (it's | |
| // built inside a BrowserOnly callback), so there's no SSR mismatch | |
| // concern -- and running synchronously before paint avoids a brief | |
| // flash of the singleton's previous palette when a page loads directly | |
| // in dark mode or the toggle fires during route navigation. | |
| function ThemeSync({ children }) { | |
| const { colorMode } = useColorMode(); | |
| React.useLayoutEffect(() => { | |
| themeObject.toggleDarkMode(colorMode === 'dark'); | |
| }, [colorMode]); | |
| return children; | |
| } | |
| SupersetProviders = ({ children }) => ( | |
| <ThemeSync> | |
| <themeObject.SupersetThemeProvider> | |
| <ConfigProvider | |
| getPopupContainer={getPopupContainer} | |
| getTargetContainer={() => document.body} | |
| > | |
| <App>{children}</App> | |
| </ConfigProvider> | |
| </themeObject.SupersetThemeProvider> | |
| </ThemeSync> | |
| ); | |
| 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 ( | |
| <div | |
| style={{ | |
| border: '1px solid var(--ifm-color-emphasis-300)', | |
| borderRadius: '4px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| minHeight: '100px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| color: 'var(--ifm-color-emphasis-600)', | |
| }} | |
| > | |
| Loading component... | |
| </div> | |
| ); | |
| } | |
| // 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 var(--ifm-color-emphasis-300)', | |
| borderRadius: '4px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| position: 'relative', // Required for portal positioning | |
| }} | |
| > | |
| {Component ? ( | |
| <Component {...restProps}>{children}</Component> | |
| ) : ( | |
| <div style={{ color: 'var(--ifm-color-emphasis-600)' }}> | |
| 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) => { | |
| setStateProps(prev => ({ | |
| ...prev, | |
| [key]: value, | |
| })); | |
| }; | |
| // 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 ( | |
| <Providers> | |
| <div className="storybook-with-controls"> | |
| <div | |
| className="storybook-example" | |
| style={{ | |
| border: '1px solid var(--ifm-color-emphasis-300)', | |
| borderRadius: '4px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| position: 'relative', // Required for portal positioning | |
| }} | |
| > | |
| {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: 'var(--ifm-color-emphasis-600)' }}> | |
| Component "{String(componentToRender)}" not found | |
| </div> | |
| )} | |
| </div> | |
| {controls.length > 0 && ( | |
| <div | |
| className="storybook-controls" | |
| style={{ | |
| border: '1px solid var(--ifm-color-emphasis-300)', | |
| borderRadius: '4px', | |
| padding: '20px', | |
| marginBottom: '20px', | |
| }} | |
| > | |
| <h4>Controls</h4> | |
| {controls.map(control => ( | |
| <div key={control.name} style={{ marginBottom: '10px' }}> | |
| <label style={{ display: 'block', marginBottom: '5px' }}> | |
| {control.label || control.name}: | |
| </label> | |
| {control.type === 'select' ? ( | |
| <select | |
| value={stateProps[control.name] ?? ''} | |
| onChange={e => | |
| updateProp(control.name, e.target.value || undefined) | |
| } | |
| style={{ width: '100%', padding: '5px' }} | |
| > | |
| <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] ?? ''} | |
| onChange={e => updateProp(control.name, e.target.value)} | |
| style={{ width: '100%', padding: '5px' }} | |
| /> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </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: 'var(--ifm-color-emphasis-600)' }}> | |
| 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: 'var(--ifm-color-emphasis-700)', | |
| }} | |
| > | |
| {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> | |
| ); | |
| } | |