From 042229bf80a79271491e7f2ae30e35d0a0d7d4a9 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Wed, 28 Jan 2026 17:54:38 -0800 Subject: [PATCH 1/4] fix(docs): add consistent dev-mode logging for Matomo page views (#37526) Co-authored-by: Claude Opus 4.5 --- docs/src/theme/Root.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/src/theme/Root.js b/docs/src/theme/Root.js index 210d99e8ed8b..10538df69704 100644 --- a/docs/src/theme/Root.js +++ b/docs/src/theme/Root.js @@ -74,6 +74,14 @@ export default function Root({ children }) { window._paq.push(['trackSiteSearch', keyword, category, resultsCount]); }; + // Helper to track page views + const trackPageView = (url, title) => { + if (devMode) { + console.log('Matomo trackPageView:', { url, title }); + } + window._paq.push(['trackPageView']); + }; + // Track external link clicks using domain as category (vendor-agnostic) const handleLinkClick = (event) => { @@ -221,7 +229,6 @@ export default function Root({ children }) { trackDocsVersion(); if (devMode) { - console.log('Tracking page view:', currentPath, currentTitle); window._paq.push(['setDomains', ['superset.apache.org']]); window._paq.push([ 'setCustomUrl', @@ -233,7 +240,7 @@ export default function Root({ children }) { window._paq.push(['setReferrerUrl', window.location.href]); window._paq.push(['setDocumentTitle', currentTitle]); - window._paq.push(['trackPageView']); + trackPageView(currentPath, currentTitle); // Check for 404 after page renders setTimeout(track404, 500); From b3526fc4caa4c4168d175e69337682b3905746ba Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Wed, 28 Jan 2026 17:58:05 -0800 Subject: [PATCH 2/4] docs(community): redesign community page with card grid layout (#37536) Co-authored-by: Claude Opus 4.5 --- docs/src/pages/community.tsx | 251 +++++++++++------- docs/static/img/community/bluesky-symbol.svg | 21 ++ docs/static/img/community/globe-symbol.svg | 23 ++ docs/static/img/community/linkedin-symbol.svg | 21 ++ docs/static/img/community/x-symbol.svg | 21 ++ 5 files changed, 241 insertions(+), 96 deletions(-) create mode 100644 docs/static/img/community/bluesky-symbol.svg create mode 100644 docs/static/img/community/globe-symbol.svg create mode 100644 docs/static/img/community/linkedin-symbol.svg create mode 100644 docs/static/img/community/x-symbol.svg diff --git a/docs/src/pages/community.tsx b/docs/src/pages/community.tsx index 09c7621c6982..d89c7f28fa9f 100644 --- a/docs/src/pages/community.tsx +++ b/docs/src/pages/community.tsx @@ -18,20 +18,24 @@ */ import { useState } from 'react'; import styled from '@emotion/styled'; -import { List } from 'antd'; import Layout from '@theme/Layout'; import { mq } from '../utils'; import SectionHeader from '../components/SectionHeader'; import BlurredSection from '../components/BlurredSection'; -const communityLinks = [ +interface CommunityLink { + url: string; + title: string; + description: string; + image: string; +} + +const communityLinks: CommunityLink[] = [ { url: 'http://bit.ly/join-superset-slack', title: 'Slack', description: 'Interact with other Superset users and community members.', image: 'slack-symbol.jpg', - ariaLabel: - 'Interact with other Superset users and community members on Slack', }, { url: 'https://github.com/apache/superset', @@ -39,96 +43,148 @@ const communityLinks = [ description: 'Create tickets to report issues, report bugs, and suggest new features.', image: 'github-symbol.jpg', - ariaLabel: - 'Create tickets to report issues, report bugs, and suggest new features on Superset GitHub repo', }, { url: 'https://lists.apache.org/list.html?dev@superset.apache.org', title: 'dev@ Mailing List', description: - 'Participate in conversations with committers and contributors.', + 'Participate in conversations with committers and contributors. Subscribe by emailing dev-subscribe@superset.apache.org.', image: 'email-symbol.png', - ariaLabel: - 'Participate in conversations with committers and contributors on Superset mailing list', - }, - { - url: 'https://stackoverflow.com/questions/tagged/apache-superset', - title: 'Stack Overflow', - description: 'Our growing knowledge base.', - image: 'stackoverflow-symbol.jpg', - ariaLabel: 'See Superset issues on Stack Overflow', - }, - { - url: 'https://www.meetup.com/Global-Apache-Superset-Community-Meetup/', - title: 'Superset Meetup Group', - description: - 'Join our monthly virtual meetups and register for any upcoming events.', - image: 'coffee-symbol.png', - ariaLabel: - 'Join our monthly virtual meetups and register for any upcoming events on Meetup', }, { - url: 'https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md', + url: 'https://superset.apache.org/inTheWild', title: 'Organizations', description: 'A list of some of the organizations using Superset in production.', - image: 'note-symbol.png', - ariaLabel: 'See a list of the organizations using Superset in production', + image: 'globe-symbol.svg', }, { - url: 'https://github.com/apache-superset/awesome-apache-superset', + url: 'https://superset.apache.org/developer_portal/contributing/overview', title: 'Contributors Guide', description: 'Interested in contributing? Learn how to contribute and best practices.', image: 'writing-symbol.png', - ariaLabel: 'Learn how to contribute and best practices on Superset GitHub', }, ]; -const StyledJoinCommunity = styled('section')` - background-color: var(--ifm-background-color); - border-bottom: 1px solid var(--ifm-border-color); - .list { - max-width: 540px; - margin: 0 auto; - padding: 40px 20px 20px 35px; +interface SocialLink { + url: string; + title: string; + image: string; +} + +const socialLinks: SocialLink[] = [ + { + url: 'https://x.com/ApacheSuperset', + title: 'X (Twitter)', + image: 'x-symbol.svg', + }, + { + url: 'https://www.linkedin.com/company/apache-superset/', + title: 'LinkedIn', + image: 'linkedin-symbol.svg', + }, + { + url: 'https://bsky.app/profile/apachesuperset.bsky.social', + title: 'Bluesky', + image: 'bluesky-symbol.svg', + }, +]; + +const StyledCardGrid = styled('div')` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + max-width: 1000px; + margin: 0 auto; + padding: 30px 20px; + ${mq[2]} { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .item { - padding: 0; - border: 0; + ${mq[1]} { + grid-template-columns: 1fr; + } + .card { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 20px; + border: 1px solid var(--ifm-border-color); + border-radius: 10px; + text-decoration: none; + color: inherit; + transition: + border-color 0.2s, + box-shadow 0.2s; + &:hover { + border-color: var(--ifm-color-primary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + text-decoration: none; + color: inherit; + } } .icon { width: 40px; - margin-top: 5px; - ${mq[1]} { - width: 40px; - margin-top: 0; - } + height: 40px; + flex-shrink: 0; + } + .card-body { + min-width: 0; } .title { - font-size: 20px; - line-height: 36px; + font-size: 18px; font-weight: 700; color: var(--ifm-font-base-color); - ${mq[1]} { - font-size: 23px; - line-height: 26px; - } + margin-bottom: 4px; } .description { font-size: 14px; - line-height: 20px; - color: var(--ifm-font-base-color); - margin-top: -8px; - margin-bottom: 23px; - ${mq[1]} { - font-size: 17px; - line-height: 22px; - color: var(--ifm-primary-text); - margin-bottom: 35px; - margin-top: 0; + line-height: 1.4; + color: var(--ifm-secondary-text); + } +`; + +const StyledSocialGrid = styled('div')` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + max-width: 600px; + margin: 0 auto; + padding: 30px 20px; + ${mq[1]} { + grid-template-columns: 1fr; + max-width: 300px; + } + .card { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 16px 20px; + border: 1px solid var(--ifm-border-color); + border-radius: 10px; + text-decoration: none; + color: inherit; + transition: + border-color 0.2s, + box-shadow 0.2s; + &:hover { + border-color: var(--ifm-color-primary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + text-decoration: none; + color: inherit; } } + .icon { + width: 28px; + height: 28px; + flex-shrink: 0; + } + .title { + font-size: 16px; + font-weight: 700; + color: var(--ifm-font-base-color); + } `; const StyledCalendarIframe = styled('iframe')` @@ -169,10 +225,10 @@ const FinePrint = styled('div')` `; const Community = () => { - const [showCalendar, setShowCalendar] = useState(false); // State to control calendar visibility + const [showCalendar, setShowCalendar] = useState(false); const toggleCalendar = () => { - setShowCalendar(!showCalendar); // Toggle calendar visibility + setShowCalendar(!showCalendar); }; return ( @@ -188,39 +244,42 @@ const Community = () => { subtitle="Get involved in our welcoming, fast growing community!" /> - - ( - - - - - } - title={ - -

- {title} -

-
- } - description={

{description}

} - aria-label="Community link" - /> -
- )} - /> -
+
+ + {communityLinks.map(({ url, title, description, image }) => ( + + {title} +
+
{title}
+
{description}
+
+
+ ))} +
+
+ + + + {socialLinks.map(({ url, title, image }) => ( + + {title} + {title} + + ))} + + + + + diff --git a/docs/static/img/community/globe-symbol.svg b/docs/static/img/community/globe-symbol.svg new file mode 100644 index 000000000000..5ee7fd386b40 --- /dev/null +++ b/docs/static/img/community/globe-symbol.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/docs/static/img/community/linkedin-symbol.svg b/docs/static/img/community/linkedin-symbol.svg new file mode 100644 index 000000000000..09a9e2202809 --- /dev/null +++ b/docs/static/img/community/linkedin-symbol.svg @@ -0,0 +1,21 @@ + + + + diff --git a/docs/static/img/community/x-symbol.svg b/docs/static/img/community/x-symbol.svg new file mode 100644 index 000000000000..d4e5a2fd3496 --- /dev/null +++ b/docs/static/img/community/x-symbol.svg @@ -0,0 +1,21 @@ + + + + From 5fedb65bc05d79d1aa7771740055c59648baec07 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Wed, 28 Jan 2026 18:47:20 -0800 Subject: [PATCH 3/4] fix(docs): migrate deprecated antd v6 APIs to items prop pattern (#37530) Co-authored-by: Claude Opus 4.5 --- .../src/components/databases/DatabasePage.tsx | 454 +++++++++--------- docs/src/pages/community.tsx | 244 ++++------ docs/src/pages/index.tsx | 7 +- 3 files changed, 332 insertions(+), 373 deletions(-) diff --git a/docs/src/components/databases/DatabasePage.tsx b/docs/src/components/databases/DatabasePage.tsx index c02d4a44a1ca..bcd2e1ad1d3b 100644 --- a/docs/src/components/databases/DatabasePage.tsx +++ b/docs/src/components/databases/DatabasePage.tsx @@ -60,8 +60,6 @@ const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => ( ); const { Title, Paragraph, Text } = Typography; -const { Panel } = Collapse; -const { TabPane } = Tabs; interface DatabasePageProps { database: DatabaseInfo; @@ -112,21 +110,20 @@ const DatabasePage: React.FC = ({ database, name }) => { return ( - - {docs.drivers.map((driver, idx) => ( - - {driver.name} - {driver.is_recommended && ( - - Recommended - - )} - - } - key={idx} - > + ({ + key: String(idx), + label: ( + + {driver.name} + {driver.is_recommended && ( + + Recommended + + )} + + ), + children: ( {driver.pypi_package && (
@@ -145,9 +142,9 @@ const DatabasePage: React.FC = ({ database, name }) => { )} - - ))} - + ), + }))} + /> ); }; @@ -165,46 +162,51 @@ const DatabasePage: React.FC = ({ database, name }) => { } style={{ marginBottom: 16 }} > - - {docs.authentication_methods.map((auth, idx) => ( - - {auth.description && {auth.description}} - {auth.requirements && ( - - )} - {auth.connection_string && - renderConnectionString( - auth.connection_string, - 'Connection String' + ({ + key: String(idx), + label: auth.name, + children: ( + <> + {auth.description && {auth.description}} + {auth.requirements && ( + )} - {auth.secure_extra && ( -
- Secure Extra Configuration: - - {JSON.stringify(auth.secure_extra, null, 2)} - -
- )} - {auth.engine_parameters && ( -
- Engine Parameters: - - {JSON.stringify(auth.engine_parameters, null, 2)} - -
- )} - {auth.notes && ( - - )} -
- ))} -
+ {auth.connection_string && + renderConnectionString( + auth.connection_string, + 'Connection String', + )} + {auth.secure_extra && ( +
+ Secure Extra Configuration: + + {JSON.stringify(auth.secure_extra, null, 2)} + +
+ )} + {auth.engine_parameters && ( +
+ Engine Parameters: + + {JSON.stringify(auth.engine_parameters, null, 2)} + +
+ )} + {auth.notes && ( + + )} + + ), + }))} + /> ); }; @@ -222,23 +224,27 @@ const DatabasePage: React.FC = ({ database, name }) => { } style={{ marginBottom: 16 }} > - - {docs.engine_parameters.map((param, idx) => ( - - {param.description && {param.description}} - {param.json && ( - - {JSON.stringify(param.json, null, 2)} - - )} - {param.docs_url && ( - - Learn more - - )} - - ))} - + ({ + key: String(idx), + label: param.name, + children: ( + <> + {param.description && ( + {param.description} + )} + {param.json && ( + {JSON.stringify(param.json, null, 2)} + )} + {param.docs_url && ( + + Learn more + + )} + + ), + }))} + /> ); }; @@ -247,75 +253,81 @@ const DatabasePage: React.FC = ({ database, name }) => { const renderCompatibleDatabases = () => { if (!docs?.compatible_databases?.length) return null; - // Create array of all panel keys to expand by default - const allPanelKeys = docs.compatible_databases.map((_, idx) => idx); + // Create array of all item keys to expand by default + const allItemKeys = docs.compatible_databases.map((_, idx) => String(idx)); return ( The following databases are compatible with the {name} driver: - - {docs.compatible_databases.map((compat, idx) => ( - - {compat.logo && ( - {compat.name} ({ + key: String(idx), + label: ( +
+ {compat.logo && ( + {compat.name} + )} + {compat.name} +
+ ), + children: ( + <> + {compat.description && ( + {compat.description} + )} + {compat.connection_string && + renderConnectionString(compat.connection_string)} + {compat.parameters && ( +
+ Parameters: + ({ + key, + parameter: key, + description: value, + }), + )} + columns={[ + { + title: 'Parameter', + dataIndex: 'parameter', + key: 'p', + }, + { + title: 'Description', + dataIndex: 'description', + key: 'd', + }, + ]} + pagination={false} + size="small" /> - )} - {compat.name} - - } - key={idx} - > - {compat.description && ( - {compat.description} - )} - {compat.connection_string && - renderConnectionString(compat.connection_string)} - {compat.parameters && ( -
- Parameters: -
({ - key, - parameter: key, - description: value, - }) - )} - columns={[ - { title: 'Parameter', dataIndex: 'parameter', key: 'p' }, - { - title: 'Description', - dataIndex: 'description', - key: 'd', - }, - ]} - pagination={false} - size="small" + + )} + {compat.notes && ( + - - )} - {compat.notes && ( - - )} - - ))} - + )} + + ), + }))} + /> ); }; @@ -376,7 +388,7 @@ const DatabasePage: React.FC = ({ database, name }) => { 'YEAR', ]; const extendedGrains = Object.keys(database.time_grains).filter( - (g) => !commonGrains.includes(g) + g => !commonGrains.includes(g), ); return ( @@ -384,12 +396,14 @@ const DatabasePage: React.FC = ({ database, name }) => {
Common Time Grains:
- {commonGrains.map((grain) => ( + {commonGrains.map(grain => ( ))} @@ -399,12 +413,14 @@ const DatabasePage: React.FC = ({ database, name }) => {
Extended Time Grains:
- {extendedGrains.map((grain) => ( + {extendedGrains.map(grain => ( ))} @@ -471,81 +487,83 @@ const DatabasePage: React.FC = ({ database, name }) => { Common error messages you may encounter when connecting to or querying{' '} {name}, along with their causes and solutions. - - {sortedCategories.map((category) => ( - - - {category} - - {errorsByCategory[category].length} error - {errorsByCategory[category].length !== 1 ? 's' : ''} - - } - key={category} - > - {errorsByCategory[category].map((error, idx) => ( -
-
- {error.description || error.error_type} -
- - {error.invalid_fields && error.invalid_fields.length > 0 && ( + ({ + key: category, + label: ( + + + {category} + + {errorsByCategory[category].length} error + {errorsByCategory[category].length !== 1 ? 's' : ''} + + ), + children: ( + <> + {errorsByCategory[category].map((error, idx) => ( +
- Check these fields: - {error.invalid_fields.map((field) => ( - - {field} - - ))} -
- )} - {error.issue_codes && error.issue_codes.length > 0 && ( -
- Related issue codes: - {error.issue_codes.map((code) => ( - - - Issue {code} - - - ))} + + {error.description || error.error_type} +
- )} -
- ))} - - ))} -
+ + {error.invalid_fields && + error.invalid_fields.length > 0 && ( +
+ Check these fields: + {error.invalid_fields.map(field => ( + + {field} + + ))} +
+ )} + {error.issue_codes && error.issue_codes.length > 0 && ( +
+ Related issue codes: + {error.issue_codes.map(code => ( + + + Issue {code} + + + ))} +
+ )} +
+ ))} + + ), + }))} + /> ); }; return ( -
+
{docs?.logo && ( = ({ database, name }) => { }} /> )} - {name} + + {name} + {docs?.homepage_url && ( = ({ database, name }) => { {docs.pypi_packages?.length > 0 && (
Required packages: - {docs.pypi_packages.map((pkg) => ( + {docs.pypi_packages.map(pkg => ( {pkg} @@ -638,7 +658,7 @@ const DatabasePage: React.FC = ({ database, name }) => { key, parameter: key, description: value, - }) + }), )} columns={[ { title: 'Parameter', dataIndex: 'parameter', key: 'p' }, @@ -664,7 +684,7 @@ const DatabasePage: React.FC = ({ database, name }) => {
{renderConnectionString( example.connection_string, - example.description + example.description, )}
))} @@ -717,7 +737,11 @@ const DatabasePage: React.FC = ({ database, name }) => {
)} {docs.sqlalchemy_docs_url && ( - + SQLAlchemy Dialect Documentation )} diff --git a/docs/src/pages/community.tsx b/docs/src/pages/community.tsx index d89c7f28fa9f..0f06d08ee3b7 100644 --- a/docs/src/pages/community.tsx +++ b/docs/src/pages/community.tsx @@ -23,19 +23,14 @@ import { mq } from '../utils'; import SectionHeader from '../components/SectionHeader'; import BlurredSection from '../components/BlurredSection'; -interface CommunityLink { - url: string; - title: string; - description: string; - image: string; -} - -const communityLinks: CommunityLink[] = [ +const communityLinks = [ { url: 'http://bit.ly/join-superset-slack', title: 'Slack', description: 'Interact with other Superset users and community members.', image: 'slack-symbol.jpg', + ariaLabel: + 'Interact with other Superset users and community members on Slack', }, { url: 'https://github.com/apache/superset', @@ -43,147 +38,99 @@ const communityLinks: CommunityLink[] = [ description: 'Create tickets to report issues, report bugs, and suggest new features.', image: 'github-symbol.jpg', + ariaLabel: + 'Create tickets to report issues, report bugs, and suggest new features on Superset GitHub repo', }, { url: 'https://lists.apache.org/list.html?dev@superset.apache.org', title: 'dev@ Mailing List', description: - 'Participate in conversations with committers and contributors. Subscribe by emailing dev-subscribe@superset.apache.org.', + 'Participate in conversations with committers and contributors.', image: 'email-symbol.png', + ariaLabel: + 'Participate in conversations with committers and contributors on Superset mailing list', + }, + { + url: 'https://stackoverflow.com/questions/tagged/apache-superset', + title: 'Stack Overflow', + description: 'Our growing knowledge base.', + image: 'stackoverflow-symbol.jpg', + ariaLabel: 'See Superset issues on Stack Overflow', + }, + { + url: 'https://www.meetup.com/Global-Apache-Superset-Community-Meetup/', + title: 'Superset Meetup Group', + description: + 'Join our monthly virtual meetups and register for any upcoming events.', + image: 'coffee-symbol.png', + ariaLabel: + 'Join our monthly virtual meetups and register for any upcoming events on Meetup', }, { - url: 'https://superset.apache.org/inTheWild', + url: 'https://github.com/apache/superset/blob/master/RESOURCES/INTHEWILD.md', title: 'Organizations', description: 'A list of some of the organizations using Superset in production.', - image: 'globe-symbol.svg', + image: 'note-symbol.png', + ariaLabel: 'See a list of the organizations using Superset in production', }, { - url: 'https://superset.apache.org/developer_portal/contributing/overview', + url: 'https://github.com/apache-superset/awesome-apache-superset', title: 'Contributors Guide', description: 'Interested in contributing? Learn how to contribute and best practices.', image: 'writing-symbol.png', + ariaLabel: 'Learn how to contribute and best practices on Superset GitHub', }, ]; -interface SocialLink { - url: string; - title: string; - image: string; -} - -const socialLinks: SocialLink[] = [ - { - url: 'https://x.com/ApacheSuperset', - title: 'X (Twitter)', - image: 'x-symbol.svg', - }, - { - url: 'https://www.linkedin.com/company/apache-superset/', - title: 'LinkedIn', - image: 'linkedin-symbol.svg', - }, - { - url: 'https://bsky.app/profile/apachesuperset.bsky.social', - title: 'Bluesky', - image: 'bluesky-symbol.svg', - }, -]; - -const StyledCardGrid = styled('div')` - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - max-width: 1000px; - margin: 0 auto; - padding: 30px 20px; - ${mq[2]} { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - ${mq[1]} { - grid-template-columns: 1fr; +const StyledJoinCommunity = styled('section')` + background-color: var(--ifm-background-color); + border-bottom: 1px solid var(--ifm-border-color); + .list { + max-width: 540px; + margin: 0 auto; + padding: 40px 20px 20px 35px; + list-style: none; } - .card { + .item { display: flex; align-items: flex-start; - gap: 16px; - padding: 20px; - border: 1px solid var(--ifm-border-color); - border-radius: 10px; - text-decoration: none; - color: inherit; - transition: - border-color 0.2s, - box-shadow 0.2s; - &:hover { - border-color: var(--ifm-color-primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - text-decoration: none; - color: inherit; - } + gap: 12px; + padding: 0; + border: 0; } .icon { width: 40px; - height: 40px; - flex-shrink: 0; - } - .card-body { - min-width: 0; + margin-top: 5px; + ${mq[1]} { + width: 40px; + margin-top: 0; + } } .title { - font-size: 18px; + font-size: 20px; + line-height: 36px; font-weight: 700; color: var(--ifm-font-base-color); - margin-bottom: 4px; + ${mq[1]} { + font-size: 23px; + line-height: 26px; + } } .description { font-size: 14px; - line-height: 1.4; - color: var(--ifm-secondary-text); - } -`; - -const StyledSocialGrid = styled('div')` - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - max-width: 600px; - margin: 0 auto; - padding: 30px 20px; - ${mq[1]} { - grid-template-columns: 1fr; - max-width: 300px; - } - .card { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - padding: 16px 20px; - border: 1px solid var(--ifm-border-color); - border-radius: 10px; - text-decoration: none; - color: inherit; - transition: - border-color 0.2s, - box-shadow 0.2s; - &:hover { - border-color: var(--ifm-color-primary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - text-decoration: none; - color: inherit; - } - } - .icon { - width: 28px; - height: 28px; - flex-shrink: 0; - } - .title { - font-size: 16px; - font-weight: 700; + line-height: 20px; color: var(--ifm-font-base-color); + margin-top: -8px; + margin-bottom: 23px; + ${mq[1]} { + font-size: 17px; + line-height: 22px; + color: var(--ifm-primary-text); + margin-bottom: 35px; + margin-top: 0; + } } `; @@ -225,10 +172,10 @@ const FinePrint = styled('div')` `; const Community = () => { - const [showCalendar, setShowCalendar] = useState(false); + const [showCalendar, setShowCalendar] = useState(false); // State to control calendar visibility const toggleCalendar = () => { - setShowCalendar(!showCalendar); + setShowCalendar(!showCalendar); // Toggle calendar visibility }; return ( @@ -244,42 +191,33 @@ const Community = () => { subtitle="Get involved in our welcoming, fast growing community!" /> -
- - {communityLinks.map(({ url, title, description, image }) => ( - - {title} -
-
{title}
-
{description}
-
-
- ))} -
-
- - - - {socialLinks.map(({ url, title, image }) => ( - - {title} - {title} - - ))} - - + +
    + {communityLinks.map( + ({ url, title, description, image, ariaLabel }) => ( +
  • + + + +
    + +

    + {title} +

    +
    +

    {description}

    +
    +
  • + ), + )} +
+
Date: Wed, 28 Jan 2026 21:33:01 -0800 Subject: [PATCH 4/4] docs(components): federate Storybook stories into Developer Portal MDX (#37502) Co-authored-by: Claude Opus 4.5 --- .rat-excludes | 1 + AGENTS.md | 24 + docs/.claude/instructions.md | 115 ++ docs/.gitignore | 7 + docs/babel.config.js | 11 +- docs/developer_portal/contributing/howtos.md | 14 +- .../extensions/components/alert.mdx | 131 -- .../extensions/components/index.mdx | 93 -- docs/developer_portal/sidebars.js | 18 + docs/developer_portal/testing/storybook.md | 114 ++ docs/docusaurus.config.ts | 16 +- docs/netlify.toml | 2 + docs/package.json | 17 +- docs/scripts/generate-superset-components.mjs | 1415 +++++++++++++++++ docs/sidebarTutorials.js | 12 + docs/src/components/BlurredSection.tsx | 5 +- docs/src/components/StorybookWrapper.jsx | 465 +++++- docs/src/pages/community.tsx | 2 +- docs/src/shims/null-module.js | 118 ++ docs/src/shims/react-table.js | 54 + docs/src/styles/custom.css | 190 +++ docs/src/theme.d.ts | 10 + docs/src/theme/Playground/Preview/index.tsx | 107 ++ .../Playground/Preview/styles.module.css | 23 + docs/src/theme/ReactLiveScope/index.tsx | 51 +- docs/src/webpack.extend.ts | 97 +- docs/static/img/atomic-design.png | Bin 0 -> 163100 bytes docs/yarn.lock | 172 +- .../AutoComplete/AutoComplete.stories.tsx | 59 +- .../src/components/Avatar/Avatar.stories.tsx | 19 + .../src/components/Badge/Badge.stories.tsx | 149 +- .../Breadcrumb/Breadcrumb.stories.tsx | 65 +- .../src/components/Button/Button.stories.tsx | 44 +- .../ButtonGroup/ButtonGroup.stories.tsx | 98 +- .../src/components/Card/Card.stories.tsx | 81 +- .../components/Checkbox/Checkbox.stories.tsx | 69 + .../DatePicker/DatePicker.stories.tsx | 44 +- .../components/Divider/Divider.stories.tsx | 41 + .../DropdownContainer.stories.tsx | 139 ++ .../EditableTitle/EditableTitle.stories.tsx | 61 + .../EmptyState/EmptyState.stories.tsx | 122 +- .../components/FaveStar/FaveStar.stories.tsx | 43 + .../src/components/Flex/Flex.stories.tsx | 94 +- .../src/components/Grid/Grid.stories.tsx | 356 +++-- .../IconButton/IconButton.stories.tsx | 53 +- .../IconTooltip/IconTooltip.stories.tsx | 24 + .../src/components/Icons/Icons.stories.tsx | 179 +++ .../src/components/Input/Input.stories.tsx | 16 +- .../src/components/Label/Label.stories.tsx | 33 + .../src/components/Layout/Layout.stories.tsx | 250 ++- .../src/components/List/List.stories.tsx | 31 +- .../ListViewCard/ListViewCard.stories.tsx | 102 +- .../components/Loading/Loading.stories.tsx | 105 +- .../src/components/Menu/Menu.stories.tsx | 99 +- .../MetadataBar/MetadataBar.stories.tsx | 119 +- .../src/components/Modal/Modal.stories.tsx | 123 +- .../ModalTrigger/ModalTrigger.stories.tsx | 119 +- .../components/Popover/Popover.stories.tsx | 139 +- .../ProgressBar/ProgressBar.stories.tsx | 171 +- .../src/components/Radio/Radio.stories.tsx | 75 +- .../src/components/Select/Select.stories.tsx | 385 +++-- .../components/Skeleton/Skeleton.stories.tsx | 149 +- .../src/components/Slider/Slider.stories.tsx | 175 +- .../src/components/Space/Space.stories.tsx | 89 +- .../src/components/Steps/Steps.stories.tsx | 143 +- .../src/components/Switch/Switch.stories.tsx | 120 +- .../src/components/Table/Table.stories.tsx | 192 ++- .../Table/TableOverview.stories.tsx | 2 +- .../TableView/TableView.stories.tsx | 161 +- .../src/components/Tabs/Tabs.stories.tsx | 148 +- .../src/components/Timer/Timer.stories.tsx | 121 +- .../components/Tooltip/Tooltip.stories.tsx | 111 +- .../src/components/Tree/Tree.stories.tsx | 320 ++-- .../TreeSelect/TreeSelect.stories.tsx | 386 ++--- .../Typography/Typography.stories.tsx | 476 ++++-- .../UnsavedChangesModal.stories.tsx | 64 +- .../src/components/Upload/Upload.stories.tsx | 166 +- .../superset-ui-core/src/components/index.ts | 16 + .../packages/superset-ui-demo/README.md | 55 +- 79 files changed, 8029 insertions(+), 1656 deletions(-) create mode 100644 docs/.claude/instructions.md delete mode 100644 docs/developer_portal/extensions/components/alert.mdx delete mode 100644 docs/developer_portal/extensions/components/index.mdx create mode 100644 docs/developer_portal/testing/storybook.md create mode 100644 docs/scripts/generate-superset-components.mjs create mode 100644 docs/src/shims/null-module.js create mode 100644 docs/src/shims/react-table.js create mode 100644 docs/src/theme/Playground/Preview/index.tsx create mode 100644 docs/src/theme/Playground/Preview/styles.module.css create mode 100644 docs/static/img/atomic-design.png diff --git a/.rat-excludes b/.rat-excludes index 135f96cb3ba3..44cf26ac6a3a 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -75,6 +75,7 @@ logos/* erd.puml erd.svg intro_header.txt +TODO.md # for LLMs llm-context.md diff --git a/AGENTS.md b/AGENTS.md index 6e1efb4a1bd8..16f085e4e86f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,30 @@ superset/ - **UPDATING.md**: Add breaking changes here - **Docstrings**: Required for new functions/classes +## Developer Portal: Storybook-to-MDX Documentation + +The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.** + +### Core Philosophy +- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first +- **Generator should be lightweight** - It extracts and passes through data; avoid special cases +- **Stories define everything** - Props, controls, galleries, examples all come from story metadata + +### Story Requirements for Docs Generation +- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;` +- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`) +- Define `args` for default prop values +- Define `argTypes` at the story level (not meta level) with control types and descriptions +- Use `parameters.docs.gallery` for size×style variant grids +- Use `parameters.docs.sampleChildren` for components that need children +- Use `parameters.docs.liveExample` for custom live code blocks +- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline + +### Generator Location +- Script: `docs/scripts/generate-superset-components.mjs` +- Wrapper: `docs/src/components/StorybookWrapper.jsx` +- Output: `docs/developer_portal/components/` + ## Architecture Patterns ### Security & Features diff --git a/docs/.claude/instructions.md b/docs/.claude/instructions.md new file mode 100644 index 000000000000..54db2f6f121c --- /dev/null +++ b/docs/.claude/instructions.md @@ -0,0 +1,115 @@ +# Developer Portal Documentation Instructions + +## Core Principle: Stories Are the Single Source of Truth + +When working on the Storybook-to-MDX documentation system: + +**ALWAYS fix the story first. NEVER add workarounds to the generator.** + +## Why This Matters + +The generator (`scripts/generate-superset-components.mjs`) should be lightweight - it extracts data from stories and passes it through. When you add special cases to the generator: +- It becomes harder to maintain +- Stories diverge from their docs representation +- Future stories need to know about generator quirks + +When you fix stories to match the expected patterns: +- Stories work identically in Storybook and Docs +- The generator stays simple and predictable +- Patterns are consistent and learnable + +## Story Patterns for Docs Generation + +### Required Structure +```tsx +// Use inline export default (NOT const meta = ...; export default meta) +export default { + title: 'Components/MyComponent', + component: MyComponent, +}; + +// Name interactive stories with Interactive prefix +export const InteractiveMyComponent: Story = { + args: { + // Default prop values + }, + argTypes: { + // Control definitions - MUST be at story level, not meta level + propName: { + control: { type: 'select' }, + options: ['a', 'b', 'c'], + description: 'What this prop does', + }, + }, +}; +``` + +### For Components with Variants (size × style grids) +```tsx +const sizes = ['small', 'medium', 'large']; +const variants = ['primary', 'secondary', 'danger']; + +InteractiveButton.parameters = { + docs: { + gallery: { + component: 'Button', + sizes, + styles: variants, + sizeProp: 'size', + styleProp: 'variant', + }, + }, +}; +``` + +### For Components Requiring Children +```tsx +InteractiveIconTooltip.parameters = { + docs: { + // Component descriptors with dot notation for nested components + sampleChildren: [{ component: 'Icons.InfoCircleOutlined', props: { iconSize: 'l' } }], + }, +}; +``` + +### For Custom Live Code Examples +```tsx +InteractiveMyComponent.parameters = { + docs: { + liveExample: `function Demo() { + return Content; +}`, + }, +}; +``` + +### For Complex Props (objects, arrays) +```tsx +InteractiveMenu.parameters = { + docs: { + staticProps: { + items: [ + { key: '1', label: 'Item 1' }, + { key: '2', label: 'Item 2' }, + ], + }, + }, +}; +``` + +## Common Issues and How to Fix Them (in the Story) + +| Issue | Wrong Approach | Right Approach | +|-------|---------------|----------------| +| Component not generated | Add pattern to generator | Change story to use inline `export default` | +| Control shows as text instead of select | Add special case in generator | Add `argTypes` with `control: { type: 'select' }` | +| Missing children/content | Modify StorybookWrapper | Add `parameters.docs.sampleChildren` | +| Gallery not showing | Add to generator output | Add `parameters.docs.gallery` config | +| Wrong live example | Hardcode in generator | Add `parameters.docs.liveExample` | + +## Files + +- **Generator**: `docs/scripts/generate-superset-components.mjs` +- **Wrapper**: `docs/src/components/StorybookWrapper.jsx` +- **Output**: `docs/developer_portal/components/` +- **Stories**: `superset-frontend/packages/superset-ui-core/src/components/*/` diff --git a/docs/.gitignore b/docs/.gitignore index 80249d7797be..37df51ce524a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -35,5 +35,12 @@ docs/databases/ # Source of truth is static/resources/openapi.json docs/api/ +# Generated component documentation MDX files (regenerated at build time) +# Source of truth is Storybook stories in superset-frontend/packages/superset-ui-core/src/components/ +developer_portal/components/ + +# Generated extension component documentation (regenerated at build time) +developer_portal/extensions/components/ + # Note: src/data/databases.json is COMMITTED (not ignored) to preserve feature diagnostics # that require Flask context to generate. Update it locally with: npm run gen-db-docs diff --git a/docs/babel.config.js b/docs/babel.config.js index e1e4c0bc50c2..61032e678a4d 100644 --- a/docs/babel.config.js +++ b/docs/babel.config.js @@ -19,5 +19,14 @@ */ module.exports = { - presets: [require.resolve('@docusaurus/core/lib/babel/preset')], + presets: [ + [ + require.resolve('@docusaurus/core/lib/babel/preset'), + { + runtime: 'automatic', + importSource: '@emotion/react', + }, + ], + ], + plugins: ['@emotion/babel-plugin'], }; diff --git a/docs/developer_portal/contributing/howtos.md b/docs/developer_portal/contributing/howtos.md index 8468b8ca2054..cb52bd5b4c99 100644 --- a/docs/developer_portal/contributing/howtos.md +++ b/docs/developer_portal/contributing/howtos.md @@ -258,19 +258,7 @@ For debugging the Flask backend: ### Storybook -Storybook is used for developing and testing UI components in isolation: - -```bash -cd superset-frontend - -# Start Storybook -npm run storybook - -# Build static Storybook -npm run build-storybook -``` - -Access Storybook at http://localhost:6006 +See the dedicated [Storybook documentation](../testing/storybook) for information on running Storybook locally and adding new stories. ## Contributing Translations diff --git a/docs/developer_portal/extensions/components/alert.mdx b/docs/developer_portal/extensions/components/alert.mdx deleted file mode 100644 index f83234a01b53..000000000000 --- a/docs/developer_portal/extensions/components/alert.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Alert -sidebar_label: Alert ---- - - - -import { StoryWithControls } from '../../../src/components/StorybookWrapper'; -import { Alert } from '@apache-superset/core/ui'; - -# Alert - -Alert component for displaying important messages to users. Wraps Ant Design Alert with sensible defaults and improved accessibility. - -## Live Example - - - -## Try It - -Edit the code below to experiment with the component: - -```tsx live -function Demo() { - return ( - - ); -} -``` - -## Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `closable` | `boolean` | `true` | Whether the Alert can be closed with a close button. | -| `type` | `string` | `"info"` | Type of the alert (e.g., info, error, warning, success). | -| `message` | `string` | `"This is a sample alert message."` | Message | -| `description` | `string` | `"Sample description for additional context."` | Description | -| `showIcon` | `boolean` | `true` | Whether to display an icon in the Alert. | - -## Usage in Extensions - -This component is available in the `@apache-superset/core/ui` package, which is automatically available to Superset extensions. - -```tsx -import { Alert } from '@apache-superset/core/ui'; - -function MyExtension() { - return ( - - ); -} -``` - -## Source Links - -- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx) -- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx) - ---- - -*This page was auto-generated from the component's Storybook story.* diff --git a/docs/developer_portal/extensions/components/index.mdx b/docs/developer_portal/extensions/components/index.mdx deleted file mode 100644 index e40b4126f7f7..000000000000 --- a/docs/developer_portal/extensions/components/index.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: Extension Components -sidebar_label: Overview -sidebar_position: 1 ---- - - - -# Extension Components - -These UI components are available to Superset extension developers through the `@apache-superset/core/ui` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements. - -## Available Components - -- [Alert](./alert) - -## Usage - -All components are exported from the `@apache-superset/core/ui` package: - -```tsx -import { Alert } from '@apache-superset/core/ui'; - -export function MyExtensionPanel() { - return ( - - Welcome to my extension! - - ); -} -``` - -## Adding New Components - -Components in `@apache-superset/core/ui` are automatically documented here. To add a new extension component: - -1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/` -2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts` -3. Create a Storybook story with an `Interactive` export: - -```tsx -export default { - title: 'Extension Components/MyComponent', - component: MyComponent, - parameters: { - docs: { - description: { - component: 'Description of the component...', - }, - }, - }, -}; - -export const InteractiveMyComponent = (args) => ; - -InteractiveMyComponent.args = { - variant: 'primary', - disabled: false, -}; - -InteractiveMyComponent.argTypes = { - variant: { - control: { type: 'select' }, - options: ['primary', 'secondary'], - }, - disabled: { - control: { type: 'boolean' }, - }, -}; -``` - -4. Run `yarn start` in `docs/` - the page generates automatically! - -## Interactive Documentation - -For interactive examples with controls, visit the [Storybook](/storybook/?path=/docs/extension-components--docs). diff --git a/docs/developer_portal/sidebars.js b/docs/developer_portal/sidebars.js index 3e81c7e5cc59..7c376be945e7 100644 --- a/docs/developer_portal/sidebars.js +++ b/docs/developer_portal/sidebars.js @@ -26,6 +26,9 @@ module.exports = { collapsed: true, items: [ 'contributing/overview', + 'guidelines/design-guidelines', + 'guidelines/frontend-style-guidelines', + 'guidelines/backend-style-guidelines', ], }, { @@ -61,5 +64,20 @@ module.exports = { 'testing/overview', ], }, + { + type: 'category', + label: 'UI Components', + collapsed: true, + link: { + type: 'doc', + id: 'components/index', + }, + items: [ + { + type: 'autogenerated', + dirName: 'components', + }, + ], + }, ], }; diff --git a/docs/developer_portal/testing/storybook.md b/docs/developer_portal/testing/storybook.md new file mode 100644 index 000000000000..0e190220f159 --- /dev/null +++ b/docs/developer_portal/testing/storybook.md @@ -0,0 +1,114 @@ +--- +title: Storybook +sidebar_position: 5 +--- + + + +# Storybook + +Superset uses [Storybook](https://storybook.js.org/) for developing and testing UI components in isolation. Storybook provides a sandbox to build components independently, outside of the main application. + +## Public Storybook + +A public Storybook with components from the `master` branch is available at: + +**[apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui/?path=/story/*)** + +## Running Locally + +### Main Superset Storybook + +To run the main Superset Storybook locally: + +```bash +cd superset-frontend + +# Start Storybook (opens at http://localhost:6006) +npm run storybook + +# Build static Storybook +npm run build-storybook +``` + +### @superset-ui Package Storybook + +The `@superset-ui` packages have a separate Storybook for component library development: + +```bash +cd superset-frontend + +# Install dependencies and bootstrap packages +npm ci && npm run bootstrap + +# Start the @superset-ui Storybook (opens at http://localhost:9001) +cd packages/superset-ui-demo +npm run storybook +``` + +## Adding Stories + +### To an Existing Package + +If stories already exist for the package, extend the `examples` array in the package's story file: + +``` +storybook/stories//index.js +``` + +### To a New Package + +1. Add package dependencies: + + ```bash + npm install + ``` + +2. Create a story folder matching the package name: + + ```bash + mkdir storybook/stories/superset-ui-/ + ``` + +3. Create an `index.js` file with the story configuration: + + ```javascript + export default { + examples: [ + { + storyPath: '@superset-ui/package', + storyName: 'My Story', + renderStory: () => , + }, + ], + }; + ``` + + Use the `|` separator for nested stories: + ```javascript + storyPath: '@superset-ui/package|Category|Subcategory' + ``` + +## Best Practices + +- **Isolate components**: Stories should render components in isolation, without application context +- **Show variations**: Create stories for different states, sizes, and configurations +- **Document props**: Use Storybook's controls to expose configurable props +- **Test edge cases**: Include stories for loading states, error states, and empty states diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 85a2cc9b5d37..7552c5917401 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -135,26 +135,26 @@ if (!versionsConfig.developer_portal.disabled && !versionsConfig.developer_porta { type: 'doc', docsPluginId: 'developer_portal', - docId: 'extensions/overview', - label: 'Extensions', + docId: 'contributing/overview', + label: 'Contributing', }, { type: 'doc', docsPluginId: 'developer_portal', - docId: 'testing/overview', - label: 'Testing', + docId: 'extensions/overview', + label: 'Extensions', }, { type: 'doc', docsPluginId: 'developer_portal', - docId: 'guidelines/design-guidelines', - label: 'Guidelines', + docId: 'testing/overview', + label: 'Testing', }, { type: 'doc', docsPluginId: 'developer_portal', - docId: 'contributing/overview', - label: 'Contributing', + docId: 'components/index', + label: 'UI Components', }, { label: 'API Reference', diff --git a/docs/netlify.toml b/docs/netlify.toml index 2ec4fa197837..fde7b6d83923 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -34,6 +34,8 @@ NODE_VERSION = "20" # Yarn version YARN_VERSION = "1.22.22" + # Increase heap size for webpack bundling of Superset UI components + NODE_OPTIONS = "--max-old-space-size=4096" # Deploy preview settings [context.deploy-preview] diff --git a/docs/package.json b/docs/package.json index a511a7f81542..7f5ef41ffcf0 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,9 +6,9 @@ "scripts": { "docusaurus": "docusaurus", "_init": "cat src/intro_header.txt ../README.md > docs/intro.md", - "start": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && yarn run generate:api-docs && NODE_ENV=development docusaurus start", + "start": "yarn run _init && yarn run generate:all && NODE_ENV=development docusaurus start", "stop": "pkill -f 'docusaurus start' || pkill -f 'docusaurus serve' || echo 'No docusaurus server running'", - "build": "yarn run _init && yarn run generate:extension-components && yarn run generate:database-docs && yarn run generate:api-docs && DEBUG=docusaurus:* docusaurus build", + "build": "yarn run _init && yarn run generate:all && DEBUG=docusaurus:* docusaurus build", "generate:api-docs": "python3 scripts/fix-openapi-spec.py && docusaurus gen-api-docs superset && node scripts/convert-api-sidebar.mjs && node scripts/generate-api-index.mjs && node scripts/generate-api-tag-pages.mjs", "clean:api-docs": "docusaurus clean-api-docs superset", "swizzle": "docusaurus swizzle", @@ -17,10 +17,12 @@ "serve": "yarn run _init && docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "yarn run generate:extension-components && yarn run generate:database-docs && tsc", + "typecheck": "yarn run generate:all && tsc", "generate:extension-components": "node scripts/generate-extension-components.mjs", + "generate:superset-components": "node scripts/generate-superset-components.mjs", "generate:database-docs": "node scripts/generate-database-docs.mjs", "gen-db-docs": "node scripts/generate-database-docs.mjs", + "generate:all": "yarn run generate:extension-components && yarn run generate:superset-components && yarn run generate:database-docs && yarn run generate:api-docs", "lint:db-metadata": "python3 ../superset/db_engine_specs/lint_metadata.py", "lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md", "update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme", @@ -36,14 +38,20 @@ }, "dependencies": { "@ant-design/icons": "^6.1.0", + "@babel/core": "^7.26.0", + "@babel/preset-react": "^7.26.3", + "@babel/preset-typescript": "^7.26.0", "@docusaurus/core": "3.9.2", "@docusaurus/plugin-client-redirects": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-live-codeblock": "^3.9.2", "@docusaurus/theme-mermaid": "^3.9.2", + "@emotion/babel-plugin": "^11.13.5", "@emotion/core": "^11.0.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.14.1", + "@fontsource/fira-code": "^5.2.7", + "@fontsource/inter": "^5.2.8", "@mdx-js/react": "^3.1.1", "@saucelabs/theme-github-codeblock": "^0.3.0", "@storybook/addon-docs": "^8.6.15", @@ -59,6 +67,7 @@ "@storybook/theming": "^8.6.11", "@superset-ui/core": "^0.20.4", "antd": "^6.2.2", + "babel-loader": "^9.2.1", "caniuse-lite": "^1.0.30001766", "docusaurus-plugin-less": "^2.0.2", "docusaurus-plugin-openapi-docs": "^4.6.0", @@ -72,7 +81,9 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-github-btn": "^1.4.0", + "react-resize-detector": "7.1.2", "react-svg-pan-zoom": "^3.13.1", + "react-table": "^7.8.0", "remark-import-partial": "^0.0.2", "reselect": "^5.1.1", "storybook": "^8.6.15", diff --git a/docs/scripts/generate-superset-components.mjs b/docs/scripts/generate-superset-components.mjs new file mode 100644 index 000000000000..a85a49bef0a8 --- /dev/null +++ b/docs/scripts/generate-superset-components.mjs @@ -0,0 +1,1415 @@ +/** + * 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. + */ + +/** + * ============================================================================ + * PHILOSOPHY: STORIES ARE THE SINGLE SOURCE OF TRUTH + * ============================================================================ + * + * When something doesn't render correctly in the docs, FIX THE STORY FIRST. + * Do NOT add special cases or workarounds to this generator. + * + * This generator should be as lightweight as possible - it extracts data from + * stories and passes it through to MDX. All configuration belongs in stories: + * + * - Use `export default { title: '...' }` (inline export, not variable) + * - Name stories `Interactive${ComponentName}` for docs generation + * - Define `args` and `argTypes` at the story level (not meta level) + * - Use `parameters.docs.gallery` for variant grids + * - Use `parameters.docs.sampleChildren` for components needing children + * - Use `parameters.docs.liveExample` for custom code examples + * - Use `parameters.docs.staticProps` for complex props + * + * If a story doesn't work with this generator, fix the story to match the + * expected patterns rather than adding complexity here. + * ============================================================================ + */ + +/** + * This script scans for ALL Storybook stories and generates MDX documentation + * pages for the "Superset Components" section of the developer portal. + * + * Supports multiple source directories with different import paths and categories. + * + * Usage: node scripts/generate-superset-components.mjs + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.resolve(__dirname, '../..'); +const DOCS_DIR = path.resolve(__dirname, '..'); +const OUTPUT_DIR = path.join(DOCS_DIR, 'developer_portal/components'); +const FRONTEND_DIR = path.join(ROOT_DIR, 'superset-frontend'); + +// Source configurations with import paths and categories +const SOURCES = [ + { + name: 'UI Core Components', + path: 'packages/superset-ui-core/src/components', + importPrefix: '@superset/components', + docImportPrefix: '@superset-ui/core/components', + category: 'ui', + enabled: true, + // Components that require complex function props or aren't exported properly + skipComponents: new Set([ + // Complex function props (require callbacks, async data, or render props) + 'AsyncSelect', 'ConfirmStatusChange', 'CronPicker', 'LabeledErrorBoundInput', + 'AsyncAceEditor', 'AsyncEsmComponent', 'TimezoneSelector', + // Not exported from @superset/components index or have export mismatches + 'ActionCell', 'BooleanCell', 'ButtonCell', 'NullCell', 'NumericCell', 'TimeCell', + 'CertifiedBadgeWithTooltip', 'CodeSyntaxHighlighter', 'DynamicTooltip', + 'PopoverDropdown', 'PopoverSection', 'WarningIconWithTooltip', 'RefreshLabel', + // Components with complex nested props (JSX children, overlay, items arrays) + 'Dropdown', 'DropdownButton', + ]), + }, + { + name: 'App Components', + path: 'src/components', + importPrefix: 'src/components', + docImportPrefix: 'src/components', + category: 'app', + enabled: false, // Requires app context (Redux, routing, etc.) + skipComponents: new Set([]), + }, + { + name: 'Dashboard Components', + path: 'src/dashboard/components', + importPrefix: 'src/dashboard/components', + docImportPrefix: 'src/dashboard/components', + category: 'dashboard', + enabled: false, // Requires app context + skipComponents: new Set([]), + }, + { + name: 'Explore Components', + path: 'src/explore/components', + importPrefix: 'src/explore/components', + docImportPrefix: 'src/explore/components', + category: 'explore', + enabled: false, // Requires app context + skipComponents: new Set([]), + }, + { + name: 'Feature Components', + path: 'src/features', + importPrefix: 'src/features', + docImportPrefix: 'src/features', + category: 'features', + enabled: false, // Requires app context + skipComponents: new Set([]), + }, + { + name: 'Filter Components', + path: 'src/filters/components', + importPrefix: 'src/filters/components', + docImportPrefix: 'src/filters/components', + category: 'filters', + enabled: false, // Requires app context + skipComponents: new Set([]), + }, + { + name: 'Chart Plugins', + path: 'packages/superset-ui-demo/storybook/stories/plugins', + importPrefix: '@superset-ui/demo', + docImportPrefix: '@superset-ui/demo', + category: 'chart-plugins', + enabled: false, // Requires chart infrastructure + skipComponents: new Set([]), + }, + { + name: 'Core Packages', + path: 'packages/superset-ui-demo/storybook/stories/superset-ui-chart', + importPrefix: '@superset-ui/core', + docImportPrefix: '@superset-ui/core', + category: 'core-packages', + enabled: false, // Requires specific setup + skipComponents: new Set([]), + }, +]; + +// Category mapping from story title prefixes to output directories +const CATEGORY_MAP = { + 'Components/': 'ui', + 'Design System/': 'design-system', + 'Chart Plugins/': 'chart-plugins', + 'Legacy Chart Plugins/': 'legacy-charts', + 'Core Packages/': 'core-packages', + 'Others/': 'utilities', + 'Extension Components/': 'extension', // Skip - handled by other script + 'Superset App/': 'app', +}; + +// Documentation-only stories to skip (not actual components) +const SKIP_STORIES = [ + 'Introduction', // Design System intro page + 'Overview', // Category overview pages + 'Examples', // Example collections + 'DesignSystem', // Meta design system page + 'MetadataBarOverview', // Overview page + 'TableOverview', // Overview page + 'Filter Plugins', // Collection story, not a component +]; + + +/** + * Recursively find all story files in a directory + */ +function walkDir(dir, files = []) { + if (!fs.existsSync(dir)) return files; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkDir(fullPath, files); + } else if (entry.name.endsWith('.stories.tsx') || entry.name.endsWith('.stories.ts')) { + files.push(fullPath); + } + } + return files; +} + +/** + * Find all story files from enabled sources + */ +function findEnabledStoryFiles() { + const files = []; + for (const source of SOURCES.filter(s => s.enabled)) { + const dir = path.join(FRONTEND_DIR, source.path); + const sourceFiles = walkDir(dir, []); + // Attach source config to each file + for (const file of sourceFiles) { + files.push({ file, source }); + } + } + return files; +} + +/** + * Find all story files from disabled sources (for tracking) + */ +function findDisabledStoryFiles() { + const files = []; + for (const source of SOURCES.filter(s => !s.enabled)) { + const dir = path.join(FRONTEND_DIR, source.path); + walkDir(dir, files); + } + return files; +} + +/** + * Parse a story file and extract metadata + */ +function parseStoryFile(filePath, sourceConfig) { + const content = fs.readFileSync(filePath, 'utf-8'); + + // Extract title from story meta (in export default block, not from data objects) + // Look for title in the export default section, which typically starts with "export default {" + const metaMatch = content.match(/export\s+default\s*\{[\s\S]*?title:\s*['"]([^'"]+)['"]/); + const title = metaMatch ? metaMatch[1] : null; + + if (!title) return null; + + // Extract component name (last part of title path) + const titleParts = title.split('/'); + const componentName = titleParts.pop(); + + // Skip documentation-only stories + if (SKIP_STORIES.includes(componentName)) { + return null; + } + + // Skip components in the source's skip list + if (sourceConfig.skipComponents.has(componentName)) { + return null; + } + + // Determine category - use source's default category unless title has a specific prefix + let category = sourceConfig.category; + for (const [prefix, cat] of Object.entries(CATEGORY_MAP)) { + if (title.startsWith(prefix)) { + category = cat; + break; + } + } + + // Extract description from parameters + let description = ''; + const descBlockMatch = content.match( + /description:\s*{\s*component:\s*([\s\S]*?)\s*},?\s*}/ + ); + if (descBlockMatch) { + const descBlock = descBlockMatch[1]; + const stringParts = []; + const stringMatches = descBlock.matchAll(/['"]([^'"]*)['"]/g); + for (const match of stringMatches) { + stringParts.push(match[1]); + } + description = stringParts.join('').trim(); + } + + // Extract story exports + const storyExports = []; + const exportMatches = content.matchAll(/export\s+(?:const|function)\s+(\w+)/g); + for (const match of exportMatches) { + if (match[1] !== 'default') { + storyExports.push(match[1]); + } + } + + // Extract component import path from the story file + // Look for: import ComponentName from './path' (default export) + // or: import { ComponentName } from './path' (named export) + let componentImportPath = null; + let isDefaultExport = true; + + // Try to find default import matching the component name + // Handles: import Component from 'path' + // and: import Component, { OtherExport } from 'path' + const defaultImportMatch = content.match( + new RegExp(`import\\s+${componentName}(?:\\s*,\\s*{[^}]*})?\\s+from\\s+['"]([^'"]+)['"]`) + ); + if (defaultImportMatch) { + componentImportPath = defaultImportMatch[1]; + isDefaultExport = true; + } else { + // Try named import + const namedImportMatch = content.match( + new RegExp(`import\\s*{[^}]*\\b${componentName}\\b[^}]*}\\s*from\\s+['"]([^'"]+)['"]`) + ); + if (namedImportMatch) { + componentImportPath = namedImportMatch[1]; + isDefaultExport = false; + } + } + + // Calculate full import path if we found a relative import + // For UI core components with aliases, keep using the alias + let resolvedImportPath = sourceConfig.importPrefix; + const useAlias = sourceConfig.importPrefix.startsWith('@superset/'); + + if (componentImportPath && componentImportPath.startsWith('.') && !useAlias) { + const storyDir = path.dirname(filePath); + const resolvedPath = path.resolve(storyDir, componentImportPath); + // Get path relative to frontend root, then convert to import path + const frontendRelative = path.relative(FRONTEND_DIR, resolvedPath); + resolvedImportPath = frontendRelative.replace(/\\/g, '/'); + } else if (!componentImportPath && !useAlias) { + // Fallback: assume component is in same dir as story, named same as component + const storyDir = path.dirname(filePath); + const possibleComponentPath = path.join(storyDir, componentName); + const frontendRelative = path.relative(FRONTEND_DIR, possibleComponentPath); + resolvedImportPath = frontendRelative.replace(/\\/g, '/'); + } + + return { + filePath, + title, + titleParts, + componentName, + category, + description, + storyExports, + relativePath: path.relative(ROOT_DIR, filePath), + sourceConfig, + resolvedImportPath, + isDefaultExport, + }; +} + +/** + * Parse args content and extract key-value pairs + * Handles strings with apostrophes correctly + */ +function parseArgsContent(argsContent, args) { + // Split into lines and process each line for simple key-value pairs + const lines = argsContent.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (!trimmed || trimmed.startsWith('//')) continue; + + // Match: key: value pattern at start of line + const propMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):\s*(.+?)[\s,]*$/); + // Also match key with value on the next line (e.g., prettier wrapping long strings) + const keyOnlyMatch = !propMatch && trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):$/); + if (!propMatch && !keyOnlyMatch) continue; + + let key, valueStr; + if (propMatch) { + key = propMatch[1]; + valueStr = propMatch[2]; + } else { + // Value is on the next line + key = keyOnlyMatch[1]; + const nextLine = i + 1 < lines.length ? lines[i + 1].trim().replace(/,\s*$/, '') : ''; + if (!nextLine) continue; + valueStr = nextLine; + i++; // Skip the next line since we consumed it + } + + // Parse the value + // Double-quoted string (handles apostrophes inside) + const doubleQuoteMatch = valueStr.match(/^"([^"]*)"$/); + if (doubleQuoteMatch) { + args[key] = doubleQuoteMatch[1]; + continue; + } + + // Single-quoted string + const singleQuoteMatch = valueStr.match(/^'([^']*)'$/); + if (singleQuoteMatch) { + args[key] = singleQuoteMatch[1]; + continue; + } + + // Template literal + const templateMatch = valueStr.match(/^`([^`]*)`$/); + if (templateMatch) { + args[key] = templateMatch[1].replace(/\s+/g, ' ').trim(); + continue; + } + + // Boolean + if (valueStr === 'true' || valueStr === 'true,') { + args[key] = true; + continue; + } + if (valueStr === 'false' || valueStr === 'false,') { + args[key] = false; + continue; + } + + // Number (including decimals and negative) + const numMatch = valueStr.match(/^(-?\d+\.?\d*),?$/); + if (numMatch) { + args[key] = Number(numMatch[1]); + continue; + } + + // Skip complex values (objects, arrays, function calls, expressions) + } +} + +/** + * Extract variable arrays from file content (for options references) + */ +function extractVariableArrays(content) { + const variableArrays = {}; + + // Pattern 1: const varName = ['a', 'b', 'c']; + // Also handles: export const varName: Type[] = ['a', 'b', 'c']; + const varMatches = content.matchAll(/(?:export\s+)?(?:const|let)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?::\s*[^=]+)?\s*=\s*\[([^\]]+)\]/g); + for (const varMatch of varMatches) { + const varName = varMatch[1]; + const arrayContent = varMatch[2]; + const values = []; + const valMatches = arrayContent.matchAll(/['"]([^'"]+)['"]/g); + for (const val of valMatches) { + values.push(val[1]); + } + if (values.length > 0) { + variableArrays[varName] = values; + } + } + + // Pattern 2: const VAR = { options: [...] } - for SIZES.options, COLORS.options patterns + const objWithOptionsMatches = content.matchAll(/(?:const|let)\s+([A-Z][A-Z_0-9]*)\s*=\s*\{[^}]*options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/g); + for (const match of objWithOptionsMatches) { + const objName = match[1]; + const optionsVarName = match[2]; + // Link the object's options to the underlying array + if (variableArrays[optionsVarName]) { + variableArrays[objName] = variableArrays[optionsVarName]; + } + } + + return variableArrays; +} + +/** + * Extract a string value from content, handling quotes properly + */ +function extractStringValue(content, startIndex) { + const remaining = content.slice(startIndex).trim(); + + // Single-quoted string + if (remaining.startsWith("'")) { + let i = 1; + while (i < remaining.length) { + if (remaining[i] === "'" && remaining[i - 1] !== '\\') { + return remaining.slice(1, i); + } + i++; + } + } + + // Double-quoted string + if (remaining.startsWith('"')) { + let i = 1; + while (i < remaining.length) { + if (remaining[i] === '"' && remaining[i - 1] !== '\\') { + return remaining.slice(1, i); + } + i++; + } + } + + // Template literal + if (remaining.startsWith('`')) { + let i = 1; + while (i < remaining.length) { + if (remaining[i] === '`' && remaining[i - 1] !== '\\') { + return remaining.slice(1, i).replace(/\s+/g, ' ').trim(); + } + i++; + } + } + + return null; +} + +/** + * Parse argTypes content and populate the argTypes object + */ +function parseArgTypes(argTypesContent, argTypes, fullContent) { + const variableArrays = extractVariableArrays(fullContent); + + // Match argType definitions - find each property block + // Use balanced brace extraction for each property + const propPattern = /([a-zA-Z_$][a-zA-Z0-9_$]*):\s*\{/g; + let propMatch; + + while ((propMatch = propPattern.exec(argTypesContent)) !== null) { + const propName = propMatch[1]; + const propStartIndex = propMatch.index + propMatch[0].length - 1; + const propConfig = extractBalancedBraces(argTypesContent, propStartIndex); + + if (!propConfig) continue; + + // Initialize argTypes entry if not exists + if (!argTypes[propName]) { + argTypes[propName] = {}; + } + + // Extract description - find the position and extract properly + const descIndex = propConfig.indexOf('description:'); + if (descIndex !== -1) { + const descValue = extractStringValue(propConfig, descIndex + 'description:'.length); + if (descValue) { + argTypes[propName].description = descValue; + } + } + + // Check for inline options array + const optionsMatch = propConfig.match(/options:\s*\[([^\]]+)\]/); + if (optionsMatch) { + const optionsStr = optionsMatch[1]; + const options = []; + const optionMatches = optionsStr.matchAll(/['"]([^'"]+)['"]/g); + for (const opt of optionMatches) { + options.push(opt[1]); + } + if (options.length > 0) { + argTypes[propName].type = 'select'; + argTypes[propName].options = options; + } + } else { + // Check for variable reference: options: variableName or options: VAR.options + const varRefMatch = propConfig.match(/options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)?)/); + if (varRefMatch) { + const varRef = varRefMatch[1]; + // Handle VAR.options pattern + const dotMatch = varRef.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.options$/); + if (dotMatch && variableArrays[dotMatch[1]]) { + argTypes[propName].type = 'select'; + argTypes[propName].options = variableArrays[dotMatch[1]]; + } else if (variableArrays[varRef]) { + argTypes[propName].type = 'select'; + argTypes[propName].options = variableArrays[varRef]; + } + } else { + // Check for ES6 shorthand: options, (same as options: options) + const shorthandMatch = propConfig.match(/(?:^|[,\s])options(?:[,\s]|$)/); + if (shorthandMatch && variableArrays['options']) { + argTypes[propName].type = 'select'; + argTypes[propName].options = variableArrays['options']; + } + } + } + + // Check for control type (radio, select, boolean, etc.) + // Supports both: control: 'boolean' (shorthand) and control: { type: 'boolean' } (object) + const controlShorthandMatch = propConfig.match(/control:\s*['"]([^'"]+)['"]/); + const controlObjectMatch = propConfig.match(/control:\s*\{[^}]*type:\s*['"]([^'"]+)['"]/); + if (controlShorthandMatch) { + argTypes[propName].type = controlShorthandMatch[1]; + } else if (controlObjectMatch) { + argTypes[propName].type = controlObjectMatch[1]; + } + + // Clear options for non-select/radio types (the shorthand "options" detection + // can false-positive when the word "options" appears in description text) + const finalType = argTypes[propName].type; + if (finalType && !['select', 'radio', 'inline-radio'].includes(finalType)) { + delete argTypes[propName].options; + } + } +} + +/** + * Helper to find balanced braces content + */ +function extractBalancedBraces(content, startIndex) { + let depth = 0; + let start = -1; + for (let i = startIndex; i < content.length; i++) { + if (content[i] === '{') { + if (depth === 0) start = i + 1; + depth++; + } else if (content[i] === '}') { + depth--; + if (depth === 0) { + return content.slice(start, i); + } + } + } + return null; +} + +/** + * Helper to find balanced brackets content (for arrays) + */ +function extractBalancedBrackets(content, startIndex) { + let depth = 0; + let start = -1; + for (let i = startIndex; i < content.length; i++) { + if (content[i] === '[') { + if (depth === 0) start = i + 1; + depth++; + } else if (content[i] === ']') { + depth--; + if (depth === 0) { + return content.slice(start, i); + } + } + } + return null; +} + +/** + * Convert camelCase prop name to human-readable label + * Handles acronyms properly: imgURL -> "Image URL", coverLeft -> "Cover Left" + */ +function propNameToLabel(name) { + return name + // Insert space before uppercase letters that follow lowercase (camelCase boundary) + .replace(/([a-z])([A-Z])/g, '$1 $2') + // Handle common acronyms - keep them together + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') + // Capitalize first letter + .replace(/^./, s => s.toUpperCase()) + // Fix common acronyms display + .replace(/\bUrl\b/g, 'URL') + .replace(/\bImg\b/g, 'Image') + .replace(/\bId\b/g, 'ID'); +} + +/** + * Convert JS object literal syntax to JSON + * Handles: single quotes, unquoted keys, trailing commas + */ +function jsToJson(jsStr) { + try { + // Remove comments + let str = jsStr.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''); + + // Replace single quotes with double quotes (but not inside already double-quoted strings) + str = str.replace(/'/g, '"'); + + // Add quotes around unquoted keys: { foo: -> { "foo": + str = str.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3'); + + // Remove trailing commas before } or ] + str = str.replace(/,(\s*[}\]])/g, '$1'); + + return JSON.parse(str); + } catch { + return null; + } +} + +/** + * Extract docs config from story parameters + * Looks for: StoryName.parameters = { docs: { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample } } + * Uses generic JSON parsing for inline data + */ +function extractDocsConfig(content, storyNames) { + // Extract variable arrays for gallery config (sizes, styles) + const variableArrays = extractVariableArrays(content); + + let sampleChildren = null; + let sampleChildrenStyle = null; + let gallery = null; + let staticProps = null; + let liveExample = null; + let examples = null; + let renderComponent = null; + let triggerProp = null; + let onHideProp = null; + + for (const storyName of storyNames) { + // Look for parameters block + const parametersPattern = new RegExp(`${storyName}\\.parameters\\s*=\\s*\\{`, 's'); + const parametersMatch = content.match(parametersPattern); + + if (parametersMatch) { + const parametersContent = extractBalancedBraces(content, parametersMatch.index + parametersMatch[0].length - 1); + if (parametersContent) { + // Extract sampleChildren - inline array using generic JSON parser + const sampleChildrenArrayMatch = parametersContent.match(/sampleChildren:\s*\[/); + if (sampleChildrenArrayMatch) { + const arrayStartIndex = sampleChildrenArrayMatch.index + sampleChildrenArrayMatch[0].length - 1; + const arrayContent = extractBalancedBrackets(parametersContent, arrayStartIndex); + if (arrayContent) { + const parsed = jsToJson('[' + arrayContent + ']'); + if (parsed && parsed.length > 0) { + sampleChildren = parsed; + } + } + } + + // Extract sampleChildrenStyle - inline object using generic JSON parser + const sampleChildrenStyleMatch = parametersContent.match(/sampleChildrenStyle:\s*\{/); + if (sampleChildrenStyleMatch) { + const styleContent = extractBalancedBraces(parametersContent, sampleChildrenStyleMatch.index + sampleChildrenStyleMatch[0].length - 1); + if (styleContent) { + const parsed = jsToJson('{' + styleContent + '}'); + if (parsed) { + sampleChildrenStyle = parsed; + } + } + } + + // Extract staticProps - generic JSON-like object extraction + const staticPropsMatch = parametersContent.match(/staticProps:\s*\{/); + if (staticPropsMatch) { + const staticPropsContent = extractBalancedBraces(parametersContent, staticPropsMatch.index + staticPropsMatch[0].length - 1); + if (staticPropsContent) { + // Try to parse as JSON (handles inline data) + const parsed = jsToJson('{' + staticPropsContent + '}'); + if (parsed) { + staticProps = parsed; + } + } + } + + // Extract gallery config + const galleryMatch = parametersContent.match(/gallery:\s*\{/); + if (galleryMatch) { + const galleryContent = extractBalancedBraces(parametersContent, galleryMatch.index + galleryMatch[0].length - 1); + if (galleryContent) { + gallery = {}; + + // Extract component name + const compMatch = galleryContent.match(/component:\s*['"]([^'"]+)['"]/); + if (compMatch) gallery.component = compMatch[1]; + + // Extract sizes - variable reference + const sizesVarMatch = galleryContent.match(/sizes:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (sizesVarMatch && variableArrays[sizesVarMatch[1]]) { + gallery.sizes = variableArrays[sizesVarMatch[1]]; + } + + // Extract styles - variable reference + const stylesVarMatch = galleryContent.match(/styles:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/); + if (stylesVarMatch && variableArrays[stylesVarMatch[1]]) { + gallery.styles = variableArrays[stylesVarMatch[1]]; + } + + // Extract sizeProp + const sizePropMatch = galleryContent.match(/sizeProp:\s*['"]([^'"]+)['"]/); + if (sizePropMatch) gallery.sizeProp = sizePropMatch[1]; + + // Extract styleProp + const stylePropMatch = galleryContent.match(/styleProp:\s*['"]([^'"]+)['"]/); + if (stylePropMatch) gallery.styleProp = stylePropMatch[1]; + } + } + + // Extract liveExample - template literal for custom live code block + const liveExampleMatch = parametersContent.match(/liveExample:\s*`/); + if (liveExampleMatch) { + // Find the closing backtick + const startIndex = liveExampleMatch.index + liveExampleMatch[0].length; + let endIndex = startIndex; + while (endIndex < parametersContent.length && parametersContent[endIndex] !== '`') { + // Handle escaped backticks + if (parametersContent[endIndex] === '\\' && parametersContent[endIndex + 1] === '`') { + endIndex += 2; + } else { + endIndex++; + } + } + if (endIndex < parametersContent.length) { + // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars) + liveExample = parametersContent.slice(startIndex, endIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$'); + } + } + + // Extract renderComponent - allows overriding which component to render + // Useful when the title-derived component (e.g., 'Icons') is a namespace, not a component + const renderComponentMatch = parametersContent.match(/renderComponent:\s*['"]([^'"]+)['"]/); + if (renderComponentMatch) { + renderComponent = renderComponentMatch[1]; + } + + // Extract triggerProp/onHideProp - for components like Modal that need a trigger button + const triggerPropMatch = parametersContent.match(/triggerProp:\s*['"]([^'"]+)['"]/); + if (triggerPropMatch) { + triggerProp = triggerPropMatch[1]; + } + const onHidePropMatch = parametersContent.match(/onHideProp:\s*['"]([^'"]+)['"]/); + if (onHidePropMatch) { + onHideProp = onHidePropMatch[1]; + } + + // Extract examples array - for multiple code examples + // Format: examples: [{ title: 'Title', code: `...` }, ...] + const examplesMatch = parametersContent.match(/examples:\s*\[/); + if (examplesMatch) { + const examplesStartIndex = examplesMatch.index + examplesMatch[0].length - 1; + const examplesArrayContent = extractBalancedBrackets(parametersContent, examplesStartIndex); + if (examplesArrayContent) { + examples = []; + // Find each example object { title: '...', code: `...` } + const exampleObjPattern = /\{\s*title:\s*['"]([^'"]+)['"]\s*,\s*code:\s*`/g; + let exampleMatch; + while ((exampleMatch = exampleObjPattern.exec(examplesArrayContent)) !== null) { + const title = exampleMatch[1]; + const codeStartIndex = exampleMatch.index + exampleMatch[0].length; + // Find closing backtick for code + let codeEndIndex = codeStartIndex; + while (codeEndIndex < examplesArrayContent.length && examplesArrayContent[codeEndIndex] !== '`') { + if (examplesArrayContent[codeEndIndex] === '\\' && examplesArrayContent[codeEndIndex + 1] === '`') { + codeEndIndex += 2; + } else { + codeEndIndex++; + } + } + // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars) + const code = examplesArrayContent.slice(codeStartIndex, codeEndIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$'); + examples.push({ title, code }); + } + } + } + } + } + + if (sampleChildren || gallery || staticProps || liveExample || examples || renderComponent || triggerProp) break; + } + + return { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; +} + +/** + * Extract args and controls from story content + */ +function extractArgsAndControls(content, componentName) { + const args = {}; + const argTypes = {}; + + // First, extract argTypes from the default export meta (shared across all stories) + // Pattern: export default { argTypes: {...} } + const defaultExportMatch = content.match(/export\s+default\s*\{/); + if (defaultExportMatch) { + const metaContent = extractBalancedBraces(content, defaultExportMatch.index + defaultExportMatch[0].length - 1); + if (metaContent) { + const metaArgTypesMatch = metaContent.match(/\bargTypes:\s*\{/); + if (metaArgTypesMatch) { + const metaArgTypesContent = extractBalancedBraces(metaContent, metaArgTypesMatch.index + metaArgTypesMatch[0].length - 1); + if (metaArgTypesContent) { + parseArgTypes(metaArgTypesContent, argTypes, content); + } + } + } + } + + // Then, try to find the Interactive story block (CSF 3.0 or CSF 2.0) + // Support multiple naming conventions: + // - InteractiveComponentName (CSF 2.0 convention) + // - ComponentNameStory (CSF 3.0 convention) + // - ComponentName (fallback) + const storyNames = [`Interactive${componentName}`, `${componentName}Story`, componentName]; + + // Extract docs config (sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample) from parameters.docs + const { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractDocsConfig(content, storyNames); + + for (const storyName of storyNames) { + // Try CSF 3.0 format: export const StoryName: StoryObj = { args: {...}, argTypes: {...} } + const csf3Pattern = new RegExp(`export\\s+const\\s+${storyName}[^=]*=[^{]*\\{`, 's'); + const csf3Match = content.match(csf3Pattern); + + if (csf3Match) { + const storyStartIndex = csf3Match.index + csf3Match[0].length - 1; + const storyContent = extractBalancedBraces(content, storyStartIndex); + + if (storyContent) { + // Extract args from story content + const argsMatch = storyContent.match(/\bargs:\s*\{/); + if (argsMatch) { + const argsContent = extractBalancedBraces(storyContent, argsMatch.index + argsMatch[0].length - 1); + if (argsContent) { + parseArgsContent(argsContent, args); + } + } + + // Extract argTypes from story content + const argTypesMatch = storyContent.match(/\bargTypes:\s*\{/); + if (argTypesMatch) { + const argTypesContent = extractBalancedBraces(storyContent, argTypesMatch.index + argTypesMatch[0].length - 1); + if (argTypesContent) { + parseArgTypes(argTypesContent, argTypes, content); + } + } + + if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) { + break; // Found a matching story + } + } + } + + // Try CSF 2.0 format: StoryName.args = {...} + const csf2ArgsPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`, 's'); + const csf2ArgsMatch = content.match(csf2ArgsPattern); + if (csf2ArgsMatch) { + const argsContent = extractBalancedBraces(content, csf2ArgsMatch.index + csf2ArgsMatch[0].length - 1); + if (argsContent) { + parseArgsContent(argsContent, args); + } + } + + // Try CSF 2.0 argTypes: StoryName.argTypes = {...} + const csf2ArgTypesPattern = new RegExp(`${storyName}\\.argTypes\\s*=\\s*\\{`, 's'); + const csf2ArgTypesMatch = content.match(csf2ArgTypesPattern); + if (csf2ArgTypesMatch) { + const argTypesContent = extractBalancedBraces(content, csf2ArgTypesMatch.index + csf2ArgTypesMatch[0].length - 1); + if (argTypesContent) { + parseArgTypes(argTypesContent, argTypes, content); + } + } + + if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) { + break; // Found a matching story + } + } + + // Generate controls from args first, then add any argTypes-only props + const controls = []; + const processedProps = new Set(); + + // First pass: props that have default values in args + for (const [key, value] of Object.entries(args)) { + processedProps.add(key); + const label = propNameToLabel(key); + const argType = argTypes[key] || {}; + + if (argType.type) { + // Use argTypes override (select, radio with options) + controls.push({ + name: key, + label, + type: argType.type, + options: argType.options, + description: argType.description + }); + } else if (typeof value === 'boolean') { + controls.push({ name: key, label, type: 'boolean', description: argType.description }); + } else if (typeof value === 'string') { + controls.push({ name: key, label, type: 'text', description: argType.description }); + } else if (typeof value === 'number') { + controls.push({ name: key, label, type: 'number', description: argType.description }); + } + } + + // Second pass: props defined only in argTypes (no explicit value in args) + // Add controls for these, but don't set default values on the component + // (setting defaults like open: false or status: 'error' breaks component behavior) + for (const [key, argType] of Object.entries(argTypes)) { + if (processedProps.has(key)) continue; + if (!argType.type) continue; // Skip if no control type defined + + const label = propNameToLabel(key); + + // Don't add to args - let the component use its own defaults + + controls.push({ + name: key, + label, + type: argType.type, + options: argType.options, + description: argType.description + }); + } + + return { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; +} + +/** + * Generate MDX content for a component + */ +function generateMDX(component, storyContent) { + const { componentName, description, relativePath, category, sourceConfig, resolvedImportPath, isDefaultExport } = component; + + const { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractArgsAndControls(storyContent, componentName); + + // Merge staticProps into args for complex values (arrays, objects) that can't be parsed from inline args + const mergedArgs = { ...args, ...staticProps }; + + // Format JSON: unquote property names but keep double quotes for string values + // This avoids issues with single quotes in strings breaking MDX parsing + const controlsJson = JSON.stringify(controls, null, 2) + .replace(/"(\w+)":/g, '$1:'); + + const propsJson = JSON.stringify(mergedArgs, null, 2) + .replace(/"(\w+)":/g, '$1:'); + + // Format sampleChildren if present (from story's parameters.docs.sampleChildren) + const sampleChildrenJson = sampleChildren + ? JSON.stringify(sampleChildren) + : null; + + // Format sampleChildrenStyle if present (from story's parameters.docs.sampleChildrenStyle) + const sampleChildrenStyleJson = sampleChildrenStyle + ? JSON.stringify(sampleChildrenStyle).replace(/"(\w+)":/g, '$1:') + : null; + + // Format gallery config if present + const hasGallery = gallery && gallery.sizes && gallery.styles; + + // Extract children for proper JSX rendering + const childrenValue = mergedArgs.children; + + const liveExampleProps = Object.entries(mergedArgs) + .filter(([key]) => key !== 'children') + .map(([key, value]) => { + if (typeof value === 'string') return `${key}="${value}"`; + if (typeof value === 'boolean') return value ? key : null; + return `${key}={${JSON.stringify(value)}}`; + }) + .filter(Boolean) + .join('\n '); + + // Generate props table with descriptions from argTypes + const propsTable = Object.entries(mergedArgs).map(([key, value]) => { + const type = typeof value === 'boolean' ? 'boolean' : typeof value === 'string' ? 'string' : typeof value === 'number' ? 'number' : 'any'; + const desc = argTypes[key]?.description || '-'; + return `| \`${key}\` | \`${type}\` | \`${JSON.stringify(value)}\` | ${desc} |`; + }).join('\n'); + + // Calculate relative import path based on category depth + const importDepth = category.includes('/') ? 4 : 3; + const wrapperImportPrefix = '../'.repeat(importDepth); + + // Use resolved import path if available, otherwise fall back to source config + const componentImportPath = resolvedImportPath || sourceConfig.importPrefix; + + // Determine component description based on source + const defaultDesc = sourceConfig.category === 'ui' + ? `The ${componentName} component from Superset's UI library.` + : `The ${componentName} component from Superset.`; + + return `--- +title: ${componentName} +sidebar_label: ${componentName} +--- + + + +import { StoryWithControls${hasGallery ? ', ComponentGallery' : ''} } from '${wrapperImportPrefix}src/components/StorybookWrapper'; + +# ${componentName} + +${description || defaultDesc} +${hasGallery ? ` +## All Variants + + +` : ''} +## Live Example + + + +## Try It + +Edit the code below to experiment with the component: + +\`\`\`tsx live +${liveExample || `function Demo() { + return ( + <${componentName} + ${liveExampleProps || '// Add props here'} + ${childrenValue ? `> + ${childrenValue} + ` : '/>'} + ); +}`} +\`\`\` +${examples && examples.length > 0 ? examples.map(ex => ` +## ${ex.title} + +\`\`\`tsx live +${ex.code} +\`\`\` +`).join('') : ''} +${Object.keys(args).length > 0 ? `## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +${propsTable}` : ''} + +## Import + +\`\`\`tsx +${isDefaultExport ? `import ${componentName} from '${componentImportPath}';` : `import { ${componentName} } from '${componentImportPath}';`} +\`\`\` + +--- + +:::tip[Improve this page] +This documentation is auto-generated from the component's Storybook story. +Help improve it by [editing the story file](https://github.com/apache/superset/edit/master/${relativePath}). +::: +`; +} + +/** + * Category display names for sidebar + */ +const CATEGORY_LABELS = { + ui: { title: 'Core Components', sidebarLabel: 'Core Components', description: 'Buttons, inputs, modals, selects, and other fundamental UI elements.' }, + 'design-system': { title: 'Layout Components', sidebarLabel: 'Layout Components', description: 'Grid, Layout, Table, Flex, Space, and container components for page structure.' }, +}; + +/** + * Generate category index page + */ +function generateCategoryIndex(category, components) { + const labels = CATEGORY_LABELS[category] || { + title: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '), + sidebarLabel: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '), + }; + const componentList = components + .sort((a, b) => a.componentName.localeCompare(b.componentName)) + .map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`) + .join('\n'); + + return `--- +title: ${labels.title} +sidebar_label: ${labels.sidebarLabel} +sidebar_position: 1 +--- + + + +# ${labels.title} + +${components.length} components available in this category. + +## Components + +${componentList} +`; +} + +/** + * Generate main overview page + */ +function generateOverviewIndex(categories) { + const categoryList = Object.entries(categories) + .filter(([, components]) => components.length > 0) + .map(([cat, components]) => { + const labels = CATEGORY_LABELS[cat] || { + title: cat.charAt(0).toUpperCase() + cat.slice(1).replace(/-/g, ' '), + }; + const desc = labels.description ? ` ${labels.description}` : ''; + return `### [${labels.title}](./${cat}/)\n${components.length} components —${desc}\n`; + }) + .join('\n'); + + const totalComponents = Object.values(categories).reduce((sum, c) => sum + c.length, 0); + + return `--- +title: UI Components Overview +sidebar_label: Overview +sidebar_position: 0 +--- + + + +# Superset Design System + +A design system is a complete set of standards intended to manage design at scale using reusable components and patterns. + +The Superset Design System uses [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) principles with adapted terminology: + +| Atomic Design | Atoms | Molecules | Organisms | Templates | Pages / Screens | +|---|:---:|:---:|:---:|:---:|:---:| +| **Superset Design** | Foundations | Components | Patterns | Templates | Features | + +Atoms = Foundations, Molecules = Components, Organisms = Patterns, Templates = Templates, Pages / Screens = Features + +--- + +## Component Library + +Interactive documentation for Superset's UI component library. **${totalComponents} components** documented across ${Object.keys(categories).filter(k => categories[k].length > 0).length} categories. + +${categoryList} + +## Usage + +All components are exported from \`@superset-ui/core/components\`: + +\`\`\`tsx +import { Button, Modal, Select } from '@superset-ui/core/components'; +\`\`\` + +## Contributing + +This documentation is auto-generated from Storybook stories. To add or update component documentation: + +1. Create or update the component's \`.stories.tsx\` file +2. Add a descriptive \`title\` and \`description\` in the story meta +3. Export an interactive story with \`args\` for configurable props +4. Run \`yarn generate:superset-components\` in the \`docs/\` directory + +:::info Work in Progress +This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation. +::: + +--- + +*Auto-generated from Storybook stories in the [Design System/Introduction](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-ui-core/src/components/DesignSystem.stories.tsx) story.* +`; +} + +/** + * Generate TODO.md tracking skipped components + */ +function generateTodoMd(skippedFiles) { + const disabledSources = SOURCES.filter(s => !s.enabled); + const grouped = {}; + for (const file of skippedFiles) { + const source = disabledSources.find(s => file.includes(s.path)); + const sourceName = source ? source.name : 'unknown'; + if (!grouped[sourceName]) grouped[sourceName] = []; + grouped[sourceName].push(file); + } + + const sections = Object.entries(grouped) + .map(([source, files]) => { + const fileList = files.map(f => `- [ ] \`${path.relative(ROOT_DIR, f)}\``).join('\n'); + return `### ${source}\n\n${files.length} components\n\n${fileList}`; + }) + .join('\n\n'); + + return `--- +title: Components TODO +sidebar_class_name: hidden +--- + +# Components TODO + +These components were found but not yet supported for documentation generation. +Future phases will add support for these sources. + +## Summary + +- **Total skipped:** ${skippedFiles.length} story files +- **Reason:** Import path resolution not yet implemented + +## Skipped by Source + +${sections} + +## How to Add Support + +1. Determine the correct import path for the source +2. Update \`generate-superset-components.mjs\` to handle the source +3. Add source to \`SUPPORTED_SOURCES\` array +4. Re-run the generator + +--- + +*Auto-generated by generate-superset-components.mjs* +`; +} + +/** + * Main function + */ +async function main() { + console.log('Generating Superset Components documentation...\n'); + + // Find enabled story files + const enabledFiles = findEnabledStoryFiles(); + console.log(`Found ${enabledFiles.length} story files from enabled sources\n`); + + // Find disabled story files (for tracking) + const disabledFiles = findDisabledStoryFiles(); + console.log(`Found ${disabledFiles.length} story files from disabled sources (tracking only)\n`); + + // Parse enabled files + const components = []; + for (const { file, source } of enabledFiles) { + const parsed = parseStoryFile(file, source); + if (parsed && parsed.componentName) { + components.push(parsed); + } + } + + console.log(`Parsed ${components.length} components\n`); + + // Group by category + const categories = {}; + for (const component of components) { + if (!categories[component.category]) { + categories[component.category] = []; + } + categories[component.category].push(component); + } + + // Ensure output directory exists + if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + } + + // Generate MDX files by category + let generatedCount = 0; + for (const [category, categoryComponents] of Object.entries(categories)) { + const categoryDir = path.join(OUTPUT_DIR, category); + if (!fs.existsSync(categoryDir)) { + fs.mkdirSync(categoryDir, { recursive: true }); + } + + // Generate component pages + for (const component of categoryComponents) { + const storyContent = fs.readFileSync(component.filePath, 'utf-8'); + const mdxContent = generateMDX(component, storyContent); + const outputPath = path.join(categoryDir, `${component.componentName.toLowerCase()}.mdx`); + fs.writeFileSync(outputPath, mdxContent); + console.log(` ✓ ${category}/${component.componentName}`); + generatedCount++; + } + + // Generate category index + const indexContent = generateCategoryIndex(category, categoryComponents); + const indexPath = path.join(categoryDir, 'index.mdx'); + fs.writeFileSync(indexPath, indexContent); + console.log(` ✓ ${category}/index`); + } + + // Generate main overview + const overviewContent = generateOverviewIndex(categories); + const overviewPath = path.join(OUTPUT_DIR, 'index.mdx'); + fs.writeFileSync(overviewPath, overviewContent); + console.log(` ✓ index (overview)`); + + // Generate TODO.md + const todoContent = generateTodoMd(disabledFiles); + const todoPath = path.join(OUTPUT_DIR, 'TODO.md'); + fs.writeFileSync(todoPath, todoContent); + console.log(` ✓ TODO.md`); + + console.log(`\nDone! Generated ${generatedCount} component pages.`); + console.log(`Tracked ${disabledFiles.length} components for future implementation.`); +} + +main().catch(console.error); diff --git a/docs/sidebarTutorials.js b/docs/sidebarTutorials.js index 6e0eff12247d..b527517a9540 100644 --- a/docs/sidebarTutorials.js +++ b/docs/sidebarTutorials.js @@ -110,9 +110,21 @@ const sidebars = { 'testing/frontend-testing', 'testing/backend-testing', 'testing/e2e-testing', + 'testing/storybook', 'testing/ci-cd', ], }, + { + type: 'category', + label: 'UI Components', + collapsed: true, + items: [ + { + type: 'autogenerated', + dirName: 'components', + }, + ], + }, { type: 'link', label: 'API Reference', diff --git a/docs/src/components/BlurredSection.tsx b/docs/src/components/BlurredSection.tsx index d502cadcb3db..712a36fa8c51 100644 --- a/docs/src/components/BlurredSection.tsx +++ b/docs/src/components/BlurredSection.tsx @@ -39,11 +39,12 @@ const StyledBlurredSection = styled('section')` interface BlurredSectionProps { children: ReactNode; + id?: string; } -const BlurredSection = ({ children }: BlurredSectionProps) => { +const BlurredSection = ({ children, id }: BlurredSectionProps) => { return ( - + {children} Blur diff --git a/docs/src/components/StorybookWrapper.jsx b/docs/src/components/StorybookWrapper.jsx index ddb1d899ba12..86b1569a872d 100644 --- a/docs/src/components/StorybookWrapper.jsx +++ b/docs/src/components/StorybookWrapper.jsx @@ -18,33 +18,245 @@ */ import React from 'react'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +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/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 }) => ( + + document.body} + > + {children} + + + ); + 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 ( +
+ Loading component... +
+ ); +} // A simple component to display a story example -export function StoryExample({ component: Component, props = {} }) { +export function StoryExample({ component, props = {} }) { return ( - -
- {Component && } -
-
+ }> + {() => { + const Component = resolveComponent(component); + const Providers = getProviders(); + const { children, restProps } = extractChildren(props); + return ( + +
+ {Component ? ( + {children} + ) : ( +
+ Component "{String(component)}" not found +
+ )} +
+
+ ); + }} +
); } -// A simple component to display a story with controls -export function StoryWithControls({ - component: Component, - props = {}, - controls = [], -}) { +// 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 ; + } + // Fallback if component not found + return
{item.props?.children || `Unknown: ${item.component}`}
; + } + // Simple string + return ( +
+ {item} +
+ ); + }); + } + + // If it's a number, generate that many sample items + if (typeof sampleChildren === 'number') { + return new Array(sampleChildren).fill(null).map((_, i) => ( +
+ Item {i + 1} +
+ )); + } + + // 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] = ; + } + } + 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 ( - +
- {Component && } + {Component ? ( + <> + {/* Show a trigger button for components like Modal */} + {triggerProp && ButtonComponent && ( + updateProp(triggerProp, true)}> + Open {component} + + )} + {children} + + ) : ( +
+ Component "{String(componentToRender)}" not found +
+ )}
{controls.length > 0 && ( @@ -87,26 +383,64 @@ export function StoryWithControls({ {control.type === 'select' ? ( + ) : control.type === 'inline-radio' || control.type === 'radio' ? ( +
+ {control.options?.map(option => ( + + ))} +
) : control.type === 'boolean' ? ( updateProp(control.name, e.target.checked)} /> + ) : control.type === 'number' ? ( + updateProp(control.name, Number(e.target.value))} + style={{ width: '100%', padding: '5px' }} + /> + ) : control.type === 'color' ? ( + updateProp(control.name, e.target.value)} + style={{ + width: '50px', + height: '30px', + padding: '2px', + cursor: 'pointer', + }} + /> ) : ( updateProp(control.name, e.target.value)} style={{ width: '100%', padding: '5px' }} /> @@ -116,6 +450,81 @@ export function StoryWithControls({
)}
- + + ); +} + +// 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 ( + }> + {() => ( + + )} + + ); +} + +// Inner component for ComponentGallery (browser-only) +function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) { + const Component = resolveComponent(component); + const Providers = getProviders(); + + if (!Component) { + return ( +
+ Component "{String(component)}" not found +
+ ); + } + + return ( + +
+ {sizes.map(size => ( +
+

{size}

+
+ {styles.map(style => ( + + {style} + + ))} +
+
+ ))} +
+
+ ); +} + +// A component to display a gallery of all variants (sizes x styles) +export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) { + return ( + }> + {() => ( + + )} + ); } diff --git a/docs/src/pages/community.tsx b/docs/src/pages/community.tsx index 0f06d08ee3b7..109720755983 100644 --- a/docs/src/pages/community.tsx +++ b/docs/src/pages/community.tsx @@ -218,7 +218,7 @@ const Community = () => { )} - + 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(); diff --git a/docs/src/shims/react-table.js b/docs/src/shims/react-table.js new file mode 100644 index 000000000000..6c19b1537b3f --- /dev/null +++ b/docs/src/shims/react-table.js @@ -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; diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css index df73738ffc70..bdf74091ff16 100644 --- a/docs/src/styles/custom.css +++ b/docs/src/styles/custom.css @@ -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; +} diff --git a/docs/src/theme.d.ts b/docs/src/theme.d.ts index 04683c86865d..4619708d06d1 100644 --- a/docs/src/theme.d.ts +++ b/docs/src/theme.d.ts @@ -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; +} diff --git a/docs/src/theme/Playground/Preview/index.tsx b/docs/src/theme/Playground/Preview/index.tsx new file mode 100644 index 000000000000..42552c185b32 --- /dev/null +++ b/docs/src/theme/Playground/Preview/index.tsx @@ -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 }) => ( + + {children} + + ); + } catch (e) { + console.error('[PlaygroundPreview] Failed to load theme provider:', e); + return ({ children }: { children: React.ReactNode }) => <>{children}; + } +} + +function Loader() { + return
Loading...
; +} + +function ThemedLivePreview(): ReactNode { + const ThemeWrapper = getThemeWrapper(); + return ( + + + + ); +} + +function PlaygroundLivePreview(): ReactNode { + // No SSR for the live preview + // See https://github.com/facebook/docusaurus/issues/5747 + return ( + }> + {() => ( + <> + ( + + )} + > + + + + + )} + + ); +} + +export default function PlaygroundPreview(): ReactNode { + return ( + <> + + + Result + + +
+ +
+ + ); +} diff --git a/docs/src/theme/Playground/Preview/styles.module.css b/docs/src/theme/Playground/Preview/styles.module.css new file mode 100644 index 000000000000..178361045f70 --- /dev/null +++ b/docs/src/theme/Playground/Preview/styles.module.css @@ -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); +} diff --git a/docs/src/theme/ReactLiveScope/index.tsx b/docs/src/theme/ReactLiveScope/index.tsx index 9675a65dd486..27d35cd60520 100644 --- a/docs/src/theme/ReactLiveScope/index.tsx +++ b/docs/src/theme/ReactLiveScope/index.tsx @@ -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.: + * + * + * */ -const ReactLiveScope = { + +// Base scope with React (always available) +const ReactLiveScope: Record = { // React core React, ...React, +}; - // Extension components from @apache-superset/core - Alert, +// 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'); - // Common Ant Design components (for demos) - Button, - Card, - Input, - Space, - Tag, - Tooltip, -}; + 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; diff --git a/docs/src/webpack.extend.ts b/docs/src/webpack.extend.ts index 5d8e0556f2ce..87bc854d2d4c 100644 --- a/docs/src/webpack.extend.ts +++ b/docs/src/webpack.extend.ts @@ -18,6 +18,7 @@ */ import path from 'path'; +import webpack from 'webpack'; import type { Plugin } from '@docusaurus/types'; export default function webpackExtendPlugin(): Plugin { @@ -26,12 +27,73 @@ export default function webpackExtendPlugin(): Plugin { 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 { }, }), 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 { __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( diff --git a/docs/static/img/atomic-design.png b/docs/static/img/atomic-design.png new file mode 100644 index 0000000000000000000000000000000000000000..e44c5f34a54ebbdd26a30adc43339e40b954914c GIT binary patch literal 163100 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%0DCrlvHPL7w{ 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 Zw1XC2;iH1K&DRJYZB2vwW$HFz{{!AVv(Nwl literal 0 HcmV?d00001 diff --git a/docs/yarn.lock b/docs/yarn.lock index 5a171ac1ca04..27691d1687c2 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -312,6 +312,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.26.0": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.25.9", "@babel/generator@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" @@ -323,6 +344,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -354,6 +386,19 @@ "@babel/traverse" "^7.28.3" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.5" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" @@ -387,6 +432,14 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" @@ -452,6 +505,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -474,6 +532,14 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.2" +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" @@ -481,6 +547,13 @@ dependencies: "@babel/types" "^7.28.2" +"@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" @@ -883,7 +956,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-display-name@^7.27.1": +"@babel/plugin-transform-react-display-name@^7.27.1", "@babel/plugin-transform-react-display-name@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== @@ -997,6 +1070,17 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" +"@babel/plugin-transform-typescript@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72" + integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" + "@babel/plugin-transform-unicode-escapes@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" @@ -1125,6 +1209,18 @@ "@babel/plugin-transform-react-jsx-development" "^7.27.1" "@babel/plugin-transform-react-pure-annotations" "^7.27.1" +"@babel/preset-react@^7.26.3": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.28.5.tgz#6fcc0400fa79698433d653092c3919bb4b0878d9" + integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.28.0" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" + "@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" @@ -1136,6 +1232,17 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.27.1" +"@babel/preset-typescript@^7.26.0": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz#540359efa3028236958466342967522fd8f2a60c" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" + "@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10", "@babel/runtime-corejs3@^7.27.1": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.3.tgz#8a993bea33c4f03b02b95ca9164dad26aaca125d" @@ -1170,6 +1277,19 @@ "@babel/types" "^7.28.2" debug "^4.3.1" +"@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4": version "7.28.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" @@ -1178,6 +1298,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@babel/types@^7.28.4", "@babel/types@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@braintree/sanitize-url@^7.0.4": version "7.1.1" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" @@ -2431,6 +2559,16 @@ resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe" integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw== +"@fontsource/fira-code@^5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.2.7.tgz#9ecbd909d53e7196a5d895b601747fe34491fc6a" + integrity sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ== + +"@fontsource/inter@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.2.8.tgz#10c95d877d972c7de5bd4592309d42fb6a5e1a5b" + integrity sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg== + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -2522,6 +2660,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -6885,9 +7031,9 @@ docusaurus-plugin-less@^2.0.2: integrity sha512-ez6WSSvGS8HoJslYHeG5SflyShWvHFXeTTHXPBd3H1T3zgq9wp6wD7scXm+rXyyfhFhP5VNiIqhYB78z4OLjwg== docusaurus-plugin-openapi-docs@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.6.0.tgz#2b89a3d23f1836a3691f076860dd67013b4800ab" - integrity sha512-wcRUnZca9hRiuAcw2Iz+YUVO4dh01mV2FoAtomRMVlWZIEgw6TA5SqsfHWRd6on/ibvvVS9Lq6GjZTcSjwLcWQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-4.7.1.tgz#fb1cf0d30bb49dc7ceb643ea623209bba054cb2a" + integrity sha512-RpqvTEnhIfdSuTn/Fa/8bmxeufijLL9HCRb//ELD33AKqEbCw147SKR/CqWu4H4gwi50FZLUbiHKZJbPtXLt9Q== dependencies: "@apidevtools/json-schema-ref-parser" "^11.5.4" "@redocly/openapi-core" "^1.34.3" @@ -6906,9 +7052,9 @@ docusaurus-plugin-openapi-docs@^4.6.0: xml-formatter "^3.6.6" docusaurus-theme-openapi-docs@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.6.0.tgz#d01965cef49764c861b4c4920c363ac4bf88cb82" - integrity sha512-YCgYReVMcrKDTNvM4dh9+i+ies+sGbCwv12TRCPZZbeif7RqTc/5w4rhxEIfp/v0uOAQGL4iXfTSBAMExotbMQ== + version "4.7.1" + resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-4.7.1.tgz#bcdb59a76852ed5f9dc77280b38e62de0745d699" + integrity sha512-OPydf11LoEY3fdxaoqCVO+qCk7LBo6l6s28UvHJ5mIN/2xu+dOOio9+xnKZ5FIPOlD+dx0gVSKzaVCi/UFTxlg== dependencies: "@hookform/error-message" "^2.0.1" "@reduxjs/toolkit" "^2.8.2" @@ -12475,6 +12621,13 @@ react-redux@^9.2.0: "@types/use-sync-external-store" "^0.0.6" use-sync-external-store "^1.4.0" +react-resize-detector@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-7.1.2.tgz#8ef975dd8c3d56f9a5160ac382ef7136dcd2d86c" + integrity sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw== + dependencies: + lodash "^4.17.21" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -12530,6 +12683,11 @@ react-syntax-highlighter@^16.0.0: prismjs "^1.30.0" refractor "^5.0.0" +react-table@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2" + integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== + "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0": version "19.1.1" resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" diff --git a/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx index 1b465ff15277..3039d8bdfa3e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/AutoComplete/AutoComplete.stories.tsx @@ -217,6 +217,15 @@ export default { }, } as Meta; +// Static options for docs and simple demos +const staticOptions = [ + { value: 'Dashboard', label: 'Dashboard' }, + { value: 'Chart', label: 'Chart' }, + { value: 'Dataset', label: 'Dataset' }, + { value: 'Database', label: 'Database' }, + { value: 'Query', label: 'Query' }, +]; + const getRandomInt = (max: number, min = 0) => Math.floor(Math.random() * (max - min + 1)) + min; @@ -243,7 +252,7 @@ const searchResult = (query: string) => }; }); -const AutoCompleteWithOptions = (args: AutoCompleteProps) => { +const AutoCompleteWithDynamicSearch = (args: AutoCompleteProps) => { const [options, setOptions] = useState([]); const handleSearch = (value: string) => { @@ -252,16 +261,60 @@ const AutoCompleteWithOptions = (args: AutoCompleteProps) => { return ; }; + type Story = StoryObj; -export const AutoCompleteStory: Story = { +// Interactive story with static options - works in both Storybook and Docs +export const InteractiveAutoComplete: Story = { + args: { + style: { width: 300 }, + placeholder: 'Type to search...', + options: staticOptions, + filterOption: true, // Enable built-in filtering for static options + }, + argTypes: { + options: { + control: false, + description: 'The dropdown options', + }, + filterOption: { + control: 'boolean', + description: 'Enable filtering of options based on input', + }, + }, +}; + +// Docs configuration - provides static options for documentation rendering +InteractiveAutoComplete.parameters = { + docs: { + staticProps: { + options: [ + { value: 'Dashboard', label: 'Dashboard' }, + { value: 'Chart', label: 'Chart' }, + { value: 'Dataset', label: 'Dataset' }, + { value: 'Database', label: 'Database' }, + { value: 'Query', label: 'Query' }, + ], + style: { width: 300 }, + filterOption: true, + }, + }, +}; + +// Dynamic search demo - Storybook only (uses render function) +export const DynamicSearchDemo: Story = { args: { style: { width: 300 }, placeholder: 'Type to search...', }, render: (args: AutoCompleteProps) => (
- +
), + parameters: { + docs: { + disable: true, // Hide from docs, it's a Storybook-specific demo + }, + }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx index 5122cd8b51ac..43ba70c755e9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Avatar/Avatar.stories.tsx @@ -27,6 +27,7 @@ export default { export const InteractiveAvatar = (args: AvatarProps) => ; InteractiveAvatar.args = { + children: 'AB', alt: '', gap: 4, shape: 'circle', @@ -36,8 +37,26 @@ InteractiveAvatar.args = { }; InteractiveAvatar.argTypes = { + children: { + description: 'Text or initials to display inside the avatar.', + control: { type: 'text' }, + }, shape: { + description: 'The shape of the avatar.', options: ['circle', 'square'], control: { type: 'select' }, }, + size: { + description: 'The size of the avatar.', + options: ['small', 'default', 'large'], + control: { type: 'select' }, + }, + src: { + description: 'Image URL for the avatar. If provided, overrides children.', + control: { type: 'text' }, + }, + gap: { + description: 'Letter spacing inside the avatar.', + control: { type: 'number', min: 0, max: 10 }, + }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx index 9a5ff53d7375..497b202e352e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Badge/Badge.stories.tsx @@ -45,7 +45,6 @@ const badgeColors: BadgeColorValue[] = [ 'lime', ]; const badgeSizes: BadgeSizeValue[] = ['default', 'small']; -const STATUSES = ['default', 'error', 'warning', 'success', 'processing']; const COLORS = { label: 'colors', @@ -59,55 +58,119 @@ const SIZES = { defaultValue: undefined, }; -export const InteractiveBadge = (args: BadgeProps) => ; +// Count Badge - shows a number +export const InteractiveBadge = (args: BadgeProps) => ( + +
+ +); InteractiveBadge.args = { - count: undefined, - color: undefined, - text: 'Text', - status: 'success', + count: 5, size: 'default', showZero: false, overflowCount: 99, }; InteractiveBadge.argTypes = { - status: { - control: { - type: 'select', - }, - options: [undefined, ...STATUSES], - description: - 'only works if `count` is `undefined` (or is set to 0) and `color` is set to `undefined`', + count: { + description: 'Number to show in the badge.', + control: { type: 'number' }, }, size: { - control: { - type: 'select', - }, - options: SIZES.options, + description: 'Size of the badge.', + control: { type: 'select' }, + options: ['default', 'small'], }, color: { - control: { - type: 'select', - }, - options: [undefined, ...COLORS.options], - }, - count: { - control: { - type: 'select', - defaultValue: undefined, - }, - options: [undefined, ...Array(100).keys()], - defaultValue: undefined, + description: 'Custom background color for the badge.', + control: { type: 'select' }, + options: [ + 'pink', + 'red', + 'yellow', + 'orange', + 'cyan', + 'green', + 'blue', + 'purple', + 'geekblue', + 'magenta', + 'volcano', + 'gold', + 'lime', + ], }, showZero: { - control: 'boolean', - defaultValue: false, + description: 'Whether to show badge when count is zero.', + control: { type: 'boolean' }, }, overflowCount: { - control: 'number', - description: - 'The threshold at which the number overflows with a `+` e.g if you set this to 10, and the value is 11, you get `11+`', + description: 'Max count to show. Shows count+ when exceeded (e.g., 99+).', + control: { type: 'number' }, + }, +}; + +InteractiveBadge.parameters = { + docs: { + description: { + story: 'Badge can show a count number or a status indicator dot.', + }, + examples: [ + { + title: 'Status Badge', + code: `function StatusBadgeDemo() { + const statuses = ['default', 'success', 'processing', 'warning', 'error']; + return ( +
+ {statuses.map(status => ( + + ))} +
+ ); +}`, + }, + { + title: 'Color Gallery', + code: `function ColorGallery() { + const colors = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple']; + return ( +
+ {colors.map(color => ( + + ))} +
+ ); +}`, + }, + ], + }, +}; + +// Status Badge - shows a status dot with text +export const StatusBadge = (args: BadgeProps) => ; + +StatusBadge.args = { + status: 'success', + text: 'Completed', +}; + +StatusBadge.argTypes = { + status: { + description: 'Status type for the badge dot.', + control: { type: 'select' }, + options: ['default', 'error', 'warning', 'success', 'processing'], + }, + text: { + description: 'Text to display next to the status dot.', + control: { type: 'text' }, }, }; @@ -116,14 +179,16 @@ export const BadgeGallery = () => ( {SIZES.options.map(size => (

{size}

- {COLORS.options.map(color => ( - - ))} +
+ {COLORS.options.map(color => ( + + ))} +
))} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx index 3cd070d34ca1..52b11b4b4d1e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -18,6 +18,13 @@ */ import type { Meta, StoryObj } from '@storybook/react'; import { Breadcrumb } from '.'; +import type { BreadcrumbProps } from './types'; + +const sampleItems = [ + { title: 'Home', href: '/' }, + { title: 'Library', href: '/library' }, + { title: 'Data' }, +]; export default { title: 'Components/Breadcrumb', @@ -33,16 +40,16 @@ export default { }, items: { control: false, - description: 'List of breadcrumb items', + description: 'Array of breadcrumb items with title and optional href', table: { - type: { summary: 'object' }, + type: { summary: '{ title: string, href?: string }[]' }, }, }, }, parameters: { docs: { description: { - component: 'Breadcrumb component for displaying navigation paths', + component: 'Breadcrumb component for displaying navigation paths.', }, }, }, @@ -50,13 +57,55 @@ export default { type Story = StoryObj; +export const InteractiveBreadcrumb = (args: BreadcrumbProps) => ( + +); + +InteractiveBreadcrumb.args = { + items: sampleItems, + separator: '/', +}; + +InteractiveBreadcrumb.argTypes = { + separator: { + description: 'Custom separator between items.', + control: 'text', + }, + items: { + description: 'Array of breadcrumb items with title and optional href.', + control: false, + }, +}; + +InteractiveBreadcrumb.parameters = { + docs: { + staticProps: { + items: [ + { title: 'Home', href: '/' }, + { title: 'Library', href: '/library' }, + { title: 'Data' }, + ], + separator: '/', + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, +}; + +// Keep original for backwards compatibility export const Default: Story = { args: { - items: [ - { title: 'Home', href: '/' }, - { title: 'Library', href: '/library' }, - { title: 'Data' }, - ], + items: sampleItems, }, render: args => , }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx index 4f3b7275cca9..ede9d87cbb88 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Button/Button.stories.tsx @@ -100,18 +100,31 @@ ButtonGallery.parameters = { }, }; -export const InteractiveButton = (args: ButtonProps & { label: string }) => { - const { label, ...btnArgs } = args; - return ; -}; +export const InteractiveButton = (args: ButtonProps & { children: string }) => ( + + + + +); + +InteractiveButtonGroup.args = { + expand: false, +}; + +InteractiveButtonGroup.argTypes = { + expand: { + description: 'When true, buttons expand to fill available width.', + control: 'boolean', + }, + className: { + description: 'CSS class name for custom styling.', + control: 'text', + }, + children: { + description: 'Button components to render inside the group.', + control: false, + }, +}; + +InteractiveButtonGroup.parameters = { + actions: { + disable: true, + }, + docs: { + staticProps: { + expand: false, + }, + sampleChildren: [ + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 1' }, + }, + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 2' }, + }, + { + component: 'Button', + props: { buttonStyle: 'tertiary', children: 'Button 3' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + + + ); +}`, + }, +}; + +// Gallery showing different button styles in groups +export const ButtonGroupGallery = (args: ButtonProps) => ( <> @@ -42,25 +113,34 @@ export const InteractiveButtonGroup = (args: ButtonProps) => ( ); -InteractiveButtonGroup.args = { + +ButtonGroupGallery.args = { buttonStyle: 'tertiary', buttonSize: 'default', }; -InteractiveButtonGroup.argTypes = { +ButtonGroupGallery.argTypes = { buttonStyle: { - name: STYLES.label, + description: 'Style variant for the buttons.', control: { type: 'select' }, - options: STYLES.options, + options: [ + 'primary', + 'secondary', + 'tertiary', + 'dashed', + 'link', + 'warning', + 'danger', + ], }, buttonSize: { - name: SIZES.label, + description: 'Size of the buttons.', control: { type: 'select' }, - options: SIZES.options, + options: ['default', 'small', 'xsmall'], }, }; -InteractiveButtonGroup.parameters = { +ButtonGroupGallery.parameters = { actions: { disable: true, }, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx index 3009da49b92b..34775c6c8a2e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Card/Card.stories.tsx @@ -22,29 +22,94 @@ import type { CardProps } from './types'; export default { title: 'Components/Card', component: Card, + parameters: { + docs: { + description: { + component: + 'A container component for grouping related content. ' + + 'Supports titles, borders, loading states, and hover effects.', + }, + }, + }, }; export const InteractiveCard = (args: CardProps) => ; InteractiveCard.args = { padded: true, - title: 'Components/Card', - children: 'Card content', + title: 'Dashboard Overview', + children: + 'This card displays a summary of your dashboard metrics and recent activity.', bordered: true, loading: false, hoverable: false, }; InteractiveCard.argTypes = { + padded: { + control: { type: 'boolean' }, + description: 'Whether the card content has padding.', + }, + title: { + control: { type: 'text' }, + description: 'Title text displayed at the top of the card.', + }, + children: { + control: { type: 'text' }, + description: 'The content inside the card.', + }, + bordered: { + control: { type: 'boolean' }, + description: 'Whether to show a border around the card.', + }, + loading: { + control: { type: 'boolean' }, + description: 'Whether to show a loading skeleton.', + }, + hoverable: { + control: { type: 'boolean' }, + description: 'Whether the card lifts on hover.', + }, onClick: { - table: { - disable: true, - }, + table: { disable: true }, action: 'onClick', }, theme: { - table: { - disable: true, - }, + table: { disable: true }, + }, +}; + +InteractiveCard.parameters = { + docs: { + liveExample: `function Demo() { + return ( + + This card displays a summary of your dashboard metrics and recent activity. + + ); +}`, + examples: [ + { + title: 'Card States', + code: `function CardStates() { + return ( +
+ + Default card content. + + + Hover over this card. + + + This content is hidden while loading. + + + Borderless card. + +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx index afa3c211555e..cae77987c5f3 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Checkbox/Checkbox.stories.tsx @@ -67,6 +67,75 @@ InteractiveCheckbox.args = { indeterminate: false, }; +InteractiveCheckbox.argTypes = { + checked: { + control: { type: 'boolean' }, + description: 'Whether the checkbox is checked.', + }, + indeterminate: { + control: { type: 'boolean' }, + description: + 'Whether the checkbox is in indeterminate state (partially selected).', + }, +}; + +InteractiveCheckbox.parameters = { + docs: { + examples: [ + { + title: 'All Checkbox States', + code: `function AllStates() { + return ( +
+ Unchecked + Checked + Indeterminate + Disabled unchecked + Disabled checked +
+ ); +}`, + }, + { + title: 'Select All Pattern', + code: `function SelectAllDemo() { + const [selected, setSelected] = React.useState([]); + const options = ['Option A', 'Option B', 'Option C']; + + const allSelected = selected.length === options.length; + const indeterminate = selected.length > 0 && !allSelected; + + return ( +
+ setSelected(e.target.checked ? [...options] : [])} + > + Select All + +
+ {options.map(opt => ( +
+ setSelected(prev => + prev.includes(opt) ? prev.filter(x => x !== opt) : [...prev, opt] + )} + > + {opt} + +
+ ))} +
+
+ ); +}`, + }, + ], + }, +}; + // All checkbox states including indeterminate const STATES = [ { diff --git a/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx index 6043effffb5e..1168ae69a9ad 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/DatePicker/DatePicker.stories.tsx @@ -80,11 +80,51 @@ export const InteractiveDatePicker: any = (args: DatePickerProps) => ( InteractiveDatePicker.args = { ...commonArgs, placeholder: 'Placeholder', - showToday: true, + showNow: true, showTime: { format: 'hh:mm a', needConfirm: false }, }; -InteractiveDatePicker.argTypes = interactiveTypes; +InteractiveDatePicker.argTypes = { + ...interactiveTypes, + showNow: { + description: 'Show "Now" button to select current date and time.', + control: 'boolean', + }, +}; + +InteractiveDatePicker.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: 'A date picker component with time selection support.', + }, + staticProps: { + allowClear: false, + autoFocus: true, + disabled: false, + format: 'YYYY-MM-DD hh:mm a', + inputReadOnly: false, + picker: 'date', + placement: 'bottomLeft', + size: 'middle', + showNow: true, + placeholder: 'Select date', + showTime: { format: 'hh:mm a', needConfirm: false }, + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, +}; export const InteractiveRangePicker = ( args: Omit & { diff --git a/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx index 903fa3b238b2..e48ab78a6cd3 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Divider/Divider.stories.tsx @@ -30,6 +30,7 @@ InteractiveDivider.args = { dashed: false, variant: 'solid', orientation: 'center', + orientationMargin: '', plain: true, type: 'horizontal', }; @@ -38,16 +39,56 @@ InteractiveDivider.argTypes = { variant: { control: { type: 'select' }, options: ['dashed', 'dotted', 'solid'], + description: 'Line style of the divider.', }, orientation: { control: { type: 'select' }, options: ['left', 'right', 'center'], + description: 'Position of title inside divider.', }, orientationMargin: { control: { type: 'text' }, + description: 'Margin from divider edge to title.', }, type: { control: { type: 'select' }, options: ['horizontal', 'vertical'], + description: 'Direction of the divider.', + }, + dashed: { + description: 'Whether line is dashed (deprecated, use variant).', + }, + plain: { + description: 'Use plain style without bold title.', + }, +}; + +InteractiveDivider.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: + 'A divider line to separate content. Use horizontal for sections, vertical for inline elements.', + }, + liveExample: `function Demo() { + return ( + <> +

Horizontal divider with title (orientationMargin applies here):

+ Left Title + Right Title + Center Title +

Vertical divider (use container gap for spacing):

+
+ Link + + Link + + Link +
+ + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx index 7a92df392df8..663436ba8fe2 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.stories.tsx @@ -27,6 +27,14 @@ import { DropdownContainer } from '.'; export default { title: 'Design System/Components/DropdownContainer', component: DropdownContainer, + parameters: { + docs: { + description: { + component: + 'DropdownContainer arranges items horizontally and moves overflowing items into a dropdown popover. Resize the container to see the overflow behavior.', + }, + }, + }, }; const ITEMS_COUNT = 6; @@ -108,3 +116,134 @@ export const Component = (props: DropdownContainerProps) => {
); }; + +// Interactive story for docs generation +export const InteractiveDropdownContainer = (args: DropdownContainerProps) => { + const simpleItems = Array.from({ length: 6 }, (_, i) => ({ + id: `item-${i}`, + element: ( +
+ Filter {i + 1} +
+ ), + })); + return ( +
+ +
+ ); +}; + +InteractiveDropdownContainer.args = {}; + +InteractiveDropdownContainer.argTypes = {}; + +InteractiveDropdownContainer.parameters = { + docs: { + staticProps: { + style: { maxWidth: 360 }, + items: [ + { + id: 'item-0', + element: { + component: 'Tag', + props: { children: 'Region', color: 'blue' }, + }, + }, + { + id: 'item-1', + element: { + component: 'Tag', + props: { children: 'Category', color: 'blue' }, + }, + }, + { + id: 'item-2', + element: { + component: 'Tag', + props: { children: 'Date Range', color: 'blue' }, + }, + }, + { + id: 'item-3', + element: { + component: 'Tag', + props: { children: 'Status', color: 'blue' }, + }, + }, + { + id: 'item-4', + element: { + component: 'Tag', + props: { children: 'Owner', color: 'blue' }, + }, + }, + { + id: 'item-5', + element: { + component: 'Tag', + props: { children: 'Priority', color: 'blue' }, + }, + }, + ], + }, + liveExample: `function Demo() { + const items = Array.from({ length: 6 }, (_, i) => ({ + id: 'item-' + i, + element: React.createElement('div', { + style: { + minWidth: 120, + padding: '4px 12px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: 4, + }, + }, 'Filter ' + (i + 1)), + })); + return ( +
+ +
+ Drag the right edge to resize and see items overflow into a dropdown +
+
+ ); +}`, + examples: [ + { + title: 'With Select Filters', + code: `function SelectFilters() { + const items = ['Region', 'Category', 'Date Range', 'Status', 'Owner'].map( + (label, i) => ({ + id: 'filter-' + i, + element: React.createElement('div', { + style: { minWidth: 150, padding: '4px 12px', background: '#f5f5f5', border: '1px solid #d9d9d9', borderRadius: 4 }, + }, label + ': All'), + }) + ); + return ( +
+ +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx index e778e71ed4e1..bcadf53679dc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/EditableTitle/EditableTitle.stories.tsx @@ -37,10 +37,71 @@ InteractiveEditableTitle.args = { title: 'Title', defaultTitle: 'Default title', placeholder: 'Placeholder', + certifiedBy: '', + certificationDetails: '', maxWidth: 100, autoSize: true, }; InteractiveEditableTitle.argTypes = { + canEdit: { + description: 'Whether the title can be edited.', + }, + editing: { + description: 'Whether the title is currently in edit mode.', + }, + emptyText: { + description: 'Text to display when title is empty.', + }, + noPermitTooltip: { + description: 'Tooltip shown when user lacks edit permission.', + }, + showTooltip: { + description: 'Whether to show tooltip on hover.', + }, + title: { + description: 'The title text to display.', + }, + defaultTitle: { + description: 'Default title when none is provided.', + }, + placeholder: { + description: 'Placeholder text when editing.', + }, + certifiedBy: { + description: 'Name of person/team who certified this item.', + }, + certificationDetails: { + description: 'Additional certification details or description.', + }, + maxWidth: { + description: 'Maximum width of the title in pixels.', + }, + autoSize: { + description: 'Whether to auto-size based on content.', + }, onSaveTitle: { action: 'onSaveTitle' }, }; + +InteractiveEditableTitle.parameters = { + actions: { + disable: true, + }, + docs: { + description: { + story: 'An editable title component with optional certification badge.', + }, + liveExample: `function Demo() { + return ( + console.log('Saved:', newTitle)} + /> + ); +}`, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx index 2c58d7ca4597..d2c11bef7698 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/EmptyState/EmptyState.stories.tsx @@ -33,14 +33,6 @@ const emptyStates = [ export default { title: 'Components/EmptyState', component: EmptyState, - argTypes: { - size: { - control: { type: 'select' }, - options: ['small', 'medium', 'large'], - defaultValue: 'medium', - description: 'Size of the Empty State components', - }, - }, } as Meta; export const Gallery: StoryFn<{ size: 'small' | 'medium' | 'large' }> = ({ @@ -65,3 +57,117 @@ export const Gallery: StoryFn<{ size: 'small' | 'medium' | 'large' }> = ({ Gallery.args = { size: 'medium', }; + +Gallery.argTypes = { + size: { + control: { type: 'select' }, + options: ['small', 'medium', 'large'], + description: 'Size of the Empty State components', + }, +}; + +// Interactive story for docs +export const InteractiveEmptyState: StoryFn<{ + size: 'small' | 'medium' | 'large'; + title: string; + description: string; + image: string; + buttonText: string; +}> = args => ; + +InteractiveEmptyState.args = { + size: 'medium', + title: 'No Data Available', + description: 'There is no data to display at this time.', + image: 'empty.svg', + buttonText: '', +}; + +InteractiveEmptyState.argTypes = { + size: { + control: { type: 'select' }, + options: ['small', 'medium', 'large'], + description: 'Size of the empty state component.', + }, + title: { + control: { type: 'text' }, + description: 'Main title text.', + }, + description: { + control: { type: 'text' }, + description: 'Description text below the title.', + }, + image: { + control: { type: 'select' }, + options: [ + 'chart.svg', + 'document.svg', + 'empty-charts.svg', + 'empty-dashboard.svg', + 'empty-dataset.svg', + 'empty-query.svg', + 'empty-table.svg', + 'empty.svg', + 'empty_sql_chart.svg', + 'filter-results.svg', + 'filter.svg', + 'star-circle.svg', + 'union.svg', + 'vector.svg', + ], + description: 'Predefined image to display.', + }, + buttonText: { + control: { type: 'text' }, + description: 'Text for optional action button.', + }, +}; + +// All available image keys for gallery +const imageKeys = [ + 'chart.svg', + 'document.svg', + 'empty-charts.svg', + 'empty-dashboard.svg', + 'empty-dataset.svg', + 'empty-query.svg', + 'empty-table.svg', + 'empty.svg', + 'empty_sql_chart.svg', + 'filter-results.svg', + 'filter.svg', + 'star-circle.svg', + 'union.svg', + 'vector.svg', +]; + +// Single size for gallery display +const gallerySizes = ['medium']; + +InteractiveEmptyState.parameters = { + docs: { + description: { + story: + 'A component for displaying empty states with optional images and actions.', + }, + gallery: { + component: 'EmptyState', + sizes: gallerySizes, + styles: imageKeys, + sizeProp: 'size', + styleProp: 'image', + }, + liveExample: `function Demo() { + return ( + alert('Filters cleared!')} + /> + ); +}`, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx index 6834c4495c2b..07581cc03cf8 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/FaveStar/FaveStar.stories.tsx @@ -71,3 +71,46 @@ export const Default: Story = {
), }; + +export const InteractiveFaveStar: Story = { + args: { + itemId: 1, + isStarred: false, + showTooltip: true, + saveFaveStar: () => {}, + }, + argTypes: { + isStarred: { + control: 'boolean', + description: 'Whether the item is currently starred.', + }, + showTooltip: { + control: 'boolean', + description: 'Show tooltip on hover.', + }, + }, + render: args => ( + + + + ), + parameters: { + docs: { + description: { + story: 'A star icon for marking items as favorites.', + }, + liveExample: `function Demo() { + const [starred, setStarred] = React.useState(false); + const toggle = React.useCallback(() => setStarred(prev => !prev), []); + return ( + + ); +}`, + }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx index 553a6603b7de..5ba2311a73a9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Flex/Flex.stories.tsx @@ -26,6 +26,17 @@ export default { component: Flex, }; +// Sample children used in both Storybook and auto-generated docs +const SAMPLE_ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + +// Shared styling for sample items - matches docs site rendering +const sampleItemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', +}; + export const InteractiveFlex = (args: FlexProps) => ( ( height: 90vh; `} > - {new Array(20).fill(null).map((_, i) => ( -

Item

+ {SAMPLE_ITEMS.map((item, i) => ( +
+ {item} +
))}
); @@ -85,3 +98,80 @@ InteractiveFlex.argTypes = { type: { name: 'string', required: false }, }, }; + +InteractiveFlex.parameters = { + docs: { + // Inline for the static parser (can't resolve variable references) + sampleChildren: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + sampleChildrenStyle: { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', + }, + liveExample: `function Demo() { + return ( + + {['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => ( +
+ {item} +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Vertical Layout', + code: `function VerticalFlex() { + return ( + + + + + + ); +}`, + }, + { + title: 'Justify and Align', + code: `function JustifyAlign() { + const boxStyle = { + width: '100%', + height: 120, + borderRadius: 6, + border: '1px solid #40a9ff', + }; + const itemStyle = { + width: 60, + height: 40, + backgroundColor: '#1677ff', + borderRadius: 4, + }; + return ( +
+ {['flex-start', 'center', 'flex-end', 'space-between', 'space-around'].map(justify => ( +
+ {justify} + +
+
+
+ +
+ ))} +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx index 0a09f703ddba..e08ff73f6b03 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Grid/Grid.stories.tsx @@ -26,18 +26,30 @@ export default { title: 'Design System/Components/Grid', component: Row, subcomponents: { Col }, + parameters: { + docs: { + description: { + component: + 'The Grid system of Ant Design is based on a 24-grid layout. The `Row` and `Col` components are used to create flexible and responsive grid layouts.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveGrid: Story = { + args: { + align: 'top', + justify: 'start', + wrap: true, + gutter: 16, + }, argTypes: { - // Row properties align: { control: 'select', options: ['top', 'middle', 'bottom', 'stretch'], - description: 'Vertical alignment of flex items.', - defaultValue: 'top', - table: { - category: 'Row', - type: { summary: 'string' }, - defaultValue: { summary: 'top' }, - }, + description: 'Vertical alignment of columns within the row.', }, justify: { control: 'select', @@ -49,161 +61,207 @@ export default { 'space-between', 'space-evenly', ], - description: 'Horizontal arrangement of flex items.', - defaultValue: undefined, - table: { - category: 'Row', - type: { summary: 'string' }, - defaultValue: { summary: 'start' }, - }, - }, - gutter: { - control: false, - description: 'Spacing between grids (horizontal and vertical).', - defaultValue: 0, - table: { - category: 'Row', - type: { summary: 'number | object | array' }, - defaultValue: { summary: '0' }, - }, + description: 'Horizontal distribution of columns within the row.', }, wrap: { control: 'boolean', - description: 'Whether the flex container is allowed to wrap its items.', - defaultValue: true, - table: { - category: 'Row', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - // Col properties - span: { - control: 'number', - description: 'Number of grid columns to span.', - defaultValue: 24, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 24 }, - }, - }, - offset: { - control: 'number', - description: 'Number of grid columns to offset from the left.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - order: { - control: 'number', - description: 'Flex order style of the grid column.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - pull: { - control: 'number', - description: 'Number of grid columns to pull to the left.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - push: { - control: 'number', - description: 'Number of grid columns to push to the right.', - defaultValue: 0, - table: { - category: 'Col', - type: { summary: 'number' }, - defaultValue: { summary: 0 }, - }, - }, - flex: { - control: 'text', - description: 'Flex layout style for the column.', - table: { - category: 'Col', - type: { summary: 'string | number' }, - }, + description: 'Whether columns are allowed to wrap to the next line.', }, - // Responsive properties (xs, sm, md, etc.) - xs: { - control: 'number', - description: - 'Settings for extra small screens (< 576px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, - }, - }, - sm: { + gutter: { control: 'number', - description: - 'Settings for small screens (≥ 576px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, - }, + description: 'Spacing between columns in pixels.', }, - md: { - control: 'number', - description: - 'Settings for medium screens (≥ 768px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + }, + render: ({ align, justify, wrap, ...rest }: RowProps & ColProps) => { + const [gutter, setGutter] = useState(24); + const [vgutter, setVgutter] = useState(24); + const [colCount, setColCount] = useState(4); + const rowProps = { align, justify, wrap }; + const colProps = rest; + + const cols = Array.from({ length: colCount }, (_, i) => ( +
+ Column {i + 1} + + )); + + return ( +
+
+ Horizontal Gutter: + +
+
+ Vertical Gutter: + +
+
+ Column Count: + +
+ + {cols} + +
+ ); + }, +}; + +InteractiveGrid.parameters = { + docs: { + renderComponent: 'Row', + sampleChildren: [ + { + component: 'Col', + props: { + span: 4, + children: 'col-4', + style: { + background: '#e6f4ff', + padding: '8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, - }, - lg: { - control: 'number', - description: - 'Settings for large screens (≥ 992px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + { + component: 'Col', + props: { + span: 4, + children: 'col-4 (tall)', + style: { + background: '#e6f4ff', + padding: '24px 8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, - }, - xl: { - control: 'number', - description: - 'Settings for extra-large screens (≥ 1200px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + { + component: 'Col', + props: { + span: 4, + children: 'col-4', + style: { + background: '#e6f4ff', + padding: '8px', + border: '1px solid #91caff', + textAlign: 'center', + }, + }, }, + ], + description: { + story: + 'Grid layout system based on 24 columns with configurable gutters.', }, - xxl: { - control: 'number', - description: - 'Settings for extra-extra-large screens (≥ 1600px). Can be a number (span) or object.', - table: { - category: 'Col', - type: { summary: 'number | object' }, + liveExample: `function Demo() { + return ( + +
+
col-12
+ +
+
col-12
+ +
+
col-8
+ +
+
col-8
+ +
+
col-8
+ + + ); +}`, + examples: [ + { + title: 'Responsive Grid', + code: `function ResponsiveGrid() { + return ( + +
+
+ Responsive +
+ +
+
+ Responsive +
+ +
+
+ Responsive +
+ +
+
+ Responsive +
+ + + ); +}`, }, - }, - }, - parameters: { - docs: { - description: { - component: - 'The Grid system of Ant Design is based on a 24-grid layout. The `Row` and `Col` components are used to create flexible and responsive grid layouts.', + { + title: 'Alignment', + code: `function AlignmentDemo() { + const boxStyle = { background: '#e6f4ff', padding: '16px 0', border: '1px solid #91caff', textAlign: 'center' }; + return ( +
+ +
+ + + + + + + + + + + + + + + + ); +}`, }, - }, + ], }, -} as Meta; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const GridStory: Story = { render: ({ align, justify, wrap, ...rest }: RowProps & ColProps) => { const [gutter, setGutter] = useState(24); diff --git a/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx index acf1085b3e3d..d667a28c9291 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/IconButton/IconButton.stories.tsx @@ -22,6 +22,28 @@ import { IconButton } from '.'; export default { title: 'Components/IconButton', component: IconButton, + parameters: { + docs: { + description: { + component: + 'The IconButton component is a versatile button that allows you to combine an icon with a text label. It is designed for use in situations where you want to display an icon along with some text in a single clickable element.', + }, + a11y: { + enabled: true, + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveIconButton: Story = { + args: { + buttonText: 'IconButton', + altText: 'Icon button alt text', + padded: true, + icon: 'https://superset.apache.org/img/superset-logo-horiz.svg', + }, argTypes: { altText: { control: 'text', @@ -33,22 +55,21 @@ export default { }, buttonText: { control: 'text', - description: 'The text inside the button', + description: 'The text inside the button.', table: { type: { summary: 'string' }, }, }, icon: { - control: false, - description: 'Icon inside the button', + control: 'text', + description: 'Icon inside the button (URL or path).', table: { type: { summary: 'string' }, - defaultValue: { summary: 'string' }, }, }, padded: { control: 'boolean', - description: 'add padding between icon and button text', + description: 'Add padding between icon and button text.', table: { type: { summary: 'boolean' }, }, @@ -57,17 +78,21 @@ export default { parameters: { docs: { description: { - component: - 'The IconButton component is a versatile button that allows you to combine an icon with a text label. It is designed for use in situations where you want to display an icon along with some text in a single clickable element.', - }, - a11y: { - enabled: true, + story: 'A button with an icon and text label.', }, + liveExample: `function Demo() { + return ( + alert('Clicked!')} + /> + ); +}`, }, }, -} as Meta; - -type Story = StoryObj; +}; export const Default: Story = { args: { @@ -78,6 +103,6 @@ export const Default: Story = { export const CustomIcon: Story = { args: { buttonText: 'Custom icon IconButton', - icon: '/images/sqlite.png', + icon: 'https://superset.apache.org/img/superset-logo-horiz.svg', }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx index d254c8c4cc20..f6bf8361b0fe 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.stories.tsx @@ -64,5 +64,29 @@ InteractiveIconTooltip.argTypes = { defaultValue: 'top', control: { type: 'select' }, options: PLACEMENTS, + description: 'Position of the tooltip relative to the icon.', + }, + tooltip: { + control: { type: 'text' }, + description: 'Text content to display in the tooltip.', + }, +}; + +InteractiveIconTooltip.parameters = { + docs: { + description: { + story: + 'A tooltip wrapper for icons. Pass an icon component as children and specify tooltip text.', + }, + sampleChildren: [ + { component: 'Icons.InfoCircleOutlined', props: { iconSize: 'l' } }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx index 061c2609c633..8aeea5fd2b82 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Icons/Icons.stories.tsx @@ -26,6 +26,14 @@ import { BaseIconComponent } from './BaseIcon'; export default { title: 'Components/Icons', component: BaseIconComponent, + parameters: { + docs: { + description: { + component: + 'Icon library for Apache Superset. Contains over 200 icons based on Ant Design icons with consistent sizing and theming support.', + }, + }, + }, }; const palette: Record = { @@ -102,6 +110,10 @@ export const InteractiveIcons = ({ ); }; +InteractiveIcons.args = { + iconSize: 'xl', +}; + InteractiveIcons.argTypes = { showNames: { name: 'Show names', @@ -112,6 +124,8 @@ InteractiveIcons.argTypes = { defaultValue: 'xl', control: { type: 'inline-radio' }, options: ['s', 'm', 'l', 'xl', 'xxl'], + description: + 'Size of the icons: s (12px), m (16px), l (20px), xl (24px), xxl (32px).', }, iconColor: { defaultValue: null, @@ -124,3 +138,168 @@ InteractiveIcons.argTypes = { }, }, }; + +InteractiveIcons.parameters = { + docs: { + // Use a specific icon for the live example since Icons is a namespace, not a component + renderComponent: 'Icons.InfoCircleOutlined', + liveExample: `function Demo() { + return ( +
+ + + + +
+ ); +}`, + examples: [ + { + title: 'Icon Sizes', + code: `function IconSizes() { + const sizes = ['s', 'm', 'l', 'xl', 'xxl']; + return ( +
+ {sizes.map(size => ( +
+ +
{size}
+
+ ))} +
+ ); +}`, + }, + { + title: 'Icon Gallery', + code: `function IconGallery() { + const Section = ({ title, children }) => ( +
+
{title}
+
{children}
+
+ ); + return ( +
+
+ + + + + + +
+
+ + + + + + + + +
+
+ + + + + + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + +
+
+ + + + + + + + +
+
+ ); +}`, + }, + { + title: 'Icon with Text', + code: `function IconWithText() { + return ( +
+
+ + Success message +
+
+ + Information message +
+
+ + Warning message +
+
+ + Error message +
+
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx index 0d0ce36eb92d..e452dc90e9d4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Input/Input.stories.tsx @@ -16,17 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; import { Input, InputNumber } from '.'; import type { InputProps, InputNumberProps, TextAreaProps } from './types'; -const meta: Meta = { +export default { title: 'Components/Input', component: Input, }; -export default meta; - type Story = StoryObj; type InputNumberStory = StoryObj; type TextAreaStory = StoryObj; @@ -41,6 +39,16 @@ export const InteractiveInput: Story = { variant: 'outlined', }, argTypes: { + type: { + control: { type: 'select' }, + options: ['text', 'password', 'email', 'number', 'tel', 'url', 'search'], + description: 'HTML input type', + table: { + category: 'Input', + type: { summary: 'string' }, + defaultValue: { summary: 'text' }, + }, + }, defaultValue: { control: { type: 'text' }, description: 'Default input value', diff --git a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx index a1012c76e77e..b14e8833180d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.stories.tsx @@ -95,3 +95,36 @@ LabelGallery.argTypes = { control: { type: 'boolean' }, }, }; + +// Interactive single Label story +interface InteractiveLabelProps { + type: LabelType; + children: string; + monospace?: boolean; +} + +export const InteractiveLabel: StoryFn = args => ( + +); + +InteractiveLabel.args = { + type: 'default', + children: 'Label text', + monospace: false, +}; + +InteractiveLabel.argTypes = { + type: { + description: 'The visual style of the label.', + options, + control: { type: 'select' }, + }, + children: { + description: 'The label text content.', + control: { type: 'text' }, + }, + monospace: { + description: 'Use monospace font.', + control: { type: 'boolean' }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx index 9040d7b43fdd..16f766b23489 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Layout/Layout.stories.tsx @@ -28,100 +28,190 @@ export default { title: 'Design System/Components/Layout', component: Layout, subcomponents: { Header, Footer, Sider, Content }, - argTypes: { - // Layout properties - className: { - control: false, - table: { - category: 'Layout', - type: { summary: 'string' }, - defaultValue: { summary: 'undefined' }, + parameters: { + docs: { + description: { + component: + 'Ant Design Layout component with configurable Sider, Header, Footer, and Content.', }, }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveLayout: Story = { + args: { + hasSider: false, + }, + argTypes: { hasSider: { control: 'boolean', - description: 'Include a sider', - table: { - category: 'Layout', - type: { summary: 'boolean' }, - }, - }, - // Layout.Sider properties - breakpoint: { - control: 'select', - options: ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'], - description: 'Responsive breakpoint for the Sider', - table: { - category: 'Sider', - type: { summary: 'text' }, - }, + description: 'Whether the layout contains a Sider sub-component.', }, - collapsible: { - control: 'boolean', - description: 'Whether the Sider can be collapsed', - table: { - category: 'Sider', - type: { summary: 'boolean' }, - }, - }, - collapsed: { - control: 'boolean', - description: 'To set the current status of the Sider', - table: { - category: 'Sider', - type: { summary: 'boolean' }, - }, + }, + render: ({ + className, + hasSider, + ...siderProps + }: LayoutProps & SiderProps) => ( + + {hasSider && ( + +
+ + }> + Option 1 + + }> + Option 2 + + + + )} + +
+ Header +
+ + Content Area + +
+ Ant Design Layout Footer +
+
+ + ), +}; + +InteractiveLayout.parameters = { + docs: { + staticProps: { + style: { minHeight: 200 }, }, - collapsedWith: { - control: false, - description: - 'Width of the collapsed sidebar, by setting to 0 a special trigger will appear', - table: { - category: 'Sider', - type: { summary: 'number' }, - defaultValue: 80, + sampleChildren: [ + { + component: 'Layout.Header', + props: { + children: 'Header', + style: { + background: '#001529', + color: '#fff', + padding: '0 24px', + lineHeight: '64px', + }, + }, }, - }, - reverseArrow: { - control: 'boolean', - description: 'Whether the arrow icon is reversed', - table: { - category: 'Sider', - type: { summary: 'boolean' }, + { + component: 'Layout.Content', + props: { + children: 'Content Area', + style: { padding: '24px', background: '#fff', flex: 1 }, + }, }, - }, - theme: { - control: 'select', - options: ['light', 'dark'], - description: 'Theme for the Sider', - table: { - category: 'Sider', - type: { summary: 'string' }, - defaultValue: { summary: 'dark' }, + { + component: 'Layout.Footer', + props: { + children: 'Footer', + style: { + textAlign: 'center', + background: '#f5f5f5', + padding: '12px', + }, + }, }, + ], + description: { + story: 'Layout component with Header, Footer, Sider, and Content areas.', }, - width: { - control: 'number', - description: 'Width of the Sider', - table: { - category: 'Sider', - type: { summary: 'number' }, - defaultValue: { summary: '200' }, + liveExample: `function Demo() { + return ( + + +
Sidebar
+
+ + + Header + + + Content + + + Footer + + +
+ ); +}`, + examples: [ + { + title: 'Content Only', + code: `function ContentOnly() { + return ( + + + Application Header + + + Main content area without a sidebar + + + Footer Content + + + ); +}`, }, - }, - }, - parameters: { - docs: { - description: { - component: - 'Ant Design Layout component with configurable Sider, Header, Footer, and Content.', + { + title: 'Right Sidebar', + code: `function RightSidebar() { + return ( + + + + Header + + + Content with right sidebar + + + +
Right Sidebar
+
+
+ ); +}`, }, - }, + ], }, -} as Meta; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const LayoutStory: Story = { render: ({ className, diff --git a/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx index efb5bc093723..3a38db424fb5 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/List/List.stories.tsx @@ -38,7 +38,6 @@ export const InteractiveList = (args: ListProps) => ( InteractiveList.args = { bordered: false, split: true, - itemLayout: 'horizontal', size: 'default', loading: false, }; @@ -46,20 +45,42 @@ InteractiveList.args = { InteractiveList.argTypes = { bordered: { control: { type: 'boolean' }, + description: 'Whether to show a border around the list.', }, split: { control: { type: 'boolean' }, + description: 'Whether to show a divider between items.', }, loading: { control: { type: 'boolean' }, - }, - itemLayout: { - control: { type: 'select' }, - options: ['horizontal', 'vertical'], + description: 'Whether to show a loading indicator.', }, size: { control: { type: 'select' }, options: ['default', 'small', 'large'], + description: 'Size of the list.', + }, +}; + +InteractiveList.parameters = { + docs: { + description: { + story: + 'A list component for displaying rows of data. Requires dataSource array and renderItem function.', + }, + staticProps: { + dataSource: ['Dashboard Analytics', 'User Management', 'Data Sources'], + }, + liveExample: `function Demo() { + const data = ['Dashboard Analytics', 'User Management', 'Data Sources']; + return ( + {item}} + /> + ); +}`, }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx index 70bf20c592a3..0b45f38bb358 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ListViewCard/ListViewCard.stories.tsx @@ -16,29 +16,97 @@ * specific language governing permissions and limitations * under the License. */ +import type { Meta, StoryObj } from '@storybook/react'; import { ListViewCard } from '.'; export default { title: 'Components/ListViewCard', component: ListViewCard, + parameters: { + docs: { + description: { + component: + 'ListViewCard is a card component used to display items in list views with an image, title, description, and optional cover sections.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveListViewCard: Story = { + args: { + title: 'Superset Card Title', + loading: false, + url: '/superset/dashboard/births/', + imgURL: 'https://picsum.photos/seed/superset/300/200', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...', + coverLeft: 'Left Section', + coverRight: 'Right Section', + }, argTypes: { - loading: { control: 'boolean' }, + title: { + control: { type: 'text' }, + description: 'Title displayed on the card.', + }, + loading: { + control: { type: 'boolean' }, + description: 'Whether the card is in loading state.', + }, + url: { + name: 'url', + control: { type: 'text' }, + description: 'URL the card links to.', + }, + imgURL: { + name: 'imgURL', + control: { type: 'text' }, + description: 'Primary image URL for the card.', + }, + description: { + control: { type: 'text' }, + description: 'Description text displayed on the card.', + }, + coverLeft: { + control: { type: 'text' }, + description: 'Content for the left section of the cover.', + }, + coverRight: { + control: { type: 'text' }, + description: 'Content for the right section of the cover.', + }, + }, + parameters: { + docs: { + description: { + story: + 'A card component for displaying items in list views with images and descriptions.', + }, + liveExample: `function Demo() { + return ( + + ); +}`, + }, }, }; -export const SupersetListViewCard = ({ - loading = false, -}: { - loading?: boolean; -}) => ( - -); +// Keep original for backwards compatibility +export const SupersetListViewCard: Story = { + args: { + title: 'Superset Card Title', + loading: false, + url: '/superset/dashboard/births/', + imgURL: 'https://picsum.photos/seed/superset2/300/200', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit...', + coverLeft: 'Left Section', + coverRight: 'Right Section', + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx index 92300215bb39..5bcc96558d22 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Loading/Loading.stories.tsx @@ -223,25 +223,120 @@ ContextualExamples.parameters = { export const InteractiveLoading = (args: LoadingProps) => ; InteractiveLoading.args = { - image: '', - className: '', size: 'm', + position: 'normal', muted: false, }; InteractiveLoading.argTypes = { position: { - name: 'position', control: { type: 'select' }, options: POSITIONS, + description: + 'Position style: normal (inline flow), floating (overlay), or inline.', }, size: { - name: 'size', control: { type: 'select' }, options: SIZES, + description: 'Size of the spinner: s (40px), m (70px), or l (100px).', }, muted: { - name: 'muted', control: { type: 'boolean' }, + description: 'Whether to show a muted/subtle version of the spinner.', + }, +}; + +InteractiveLoading.parameters = { + docs: { + description: { + story: 'A loading spinner component with configurable size and position.', + }, + liveExample: `function Demo() { + return ( +
+ {['normal', 'floating', 'inline'].map(position => ( +
+

{position}

+ +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Size and Opacity Showcase', + code: `function SizeShowcase() { + const sizes = ['s', 'm', 'l']; + return ( +
+
+
Size
+
Normal
+
Muted
+
Usage
+ {sizes.map(size => ( + +
+ {size.toUpperCase()} ({size === 's' ? '40px' : size === 'm' ? '70px' : '100px'}) +
+
+ +
+
+ +
+
+ {size === 's' && 'Filter bars, inline'} + {size === 'm' && 'Explore pages'} + {size === 'l' && 'Full page loading'} +
+
+ ))} +
+
+ ); +}`, + }, + { + title: 'Contextual Examples', + code: `function ContextualDemo() { + return ( +
+

Filter Bar (size="s", muted)

+
+ Filter 1: + + Filter 2: + +
+ +

Dashboard Grid (size="s", muted)

+
+ {[1, 2, 3].map(i => ( +
+ +
+ ))} +
+ +

Main Loading (size="l")

+
+ +
+
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx index b7bf895a2f4a..2789edf4dd7e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx @@ -21,6 +21,14 @@ import { Menu, MainNav } from '.'; export default { title: 'Components/Menu', component: Menu as React.FC, + parameters: { + docs: { + description: { + component: + 'Navigation menu component supporting horizontal, vertical, and inline modes. Based on Ant Design Menu with Superset styling.', + }, + }, + }, }; export const MainNavigation = (args: any) => ( @@ -47,18 +55,97 @@ export const InteractiveMenu = (args: any) => ( ); InteractiveMenu.args = { - defaultSelectedKeys: ['1'], - inlineCollapsed: false, mode: 'horizontal', - multiple: false, selectable: true, }; InteractiveMenu.argTypes = { mode: { - control: { - type: 'select', - }, + control: 'select', options: ['horizontal', 'vertical', 'inline'], + description: + 'Menu display mode: horizontal navbar, vertical sidebar, or inline collapsible.', + }, + selectable: { + control: 'boolean', + description: 'Whether menu items can be selected.', + }, + multiple: { + control: 'boolean', + description: 'Allow multiple items to be selected.', + }, + inlineCollapsed: { + control: 'boolean', + description: + 'Whether the inline menu is collapsed (only applies to inline mode).', + }, +}; + +InteractiveMenu.parameters = { + docs: { + staticProps: { + items: [ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { label: 'SQL Lab', key: 'sqllab' }, + ], + }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Vertical Menu', + code: `function VerticalMenu() { + return ( + + ); +}`, + }, + { + title: 'Menu with Icons', + code: `function MenuWithIcons() { + return ( + Dashboards, key: 'dashboards' }, + { label: <> Charts, key: 'charts' }, + { label: <> Datasets, key: 'datasets' }, + { label: <> SQL Lab, key: 'sqllab' }, + ]} + /> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx index 8f7d0d700dcc..9228769255e4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/MetadataBar/MetadataBar.stories.tsx @@ -21,8 +21,16 @@ import { useResizeDetector } from 'react-resize-detector'; import MetadataBar, { MetadataBarProps, MetadataType } from '.'; export default { - title: 'Design System/Components/MetadataBar/Examples', + title: 'Design System/Components/MetadataBar', component: MetadataBar, + parameters: { + docs: { + description: { + component: + 'MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.', + }, + }, + }, }; const A_WEEK_AGO = 'a week ago'; @@ -98,3 +106,112 @@ Basic.argTypes = { }, }, }; + +// Interactive story for docs generation +export const InteractiveMetadataBar = (args: MetadataBarProps) => ( + +); + +InteractiveMetadataBar.args = { + items: [ + { + type: MetadataType.Sql, + title: 'Click to view query', + }, + { + type: MetadataType.Owner, + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: A_WEEK_AGO, + }, + { + type: MetadataType.LastModified, + value: A_WEEK_AGO, + modifiedBy: 'Jane Smith', + }, + { + type: MetadataType.Tags, + values: ['management', 'research', 'poc'], + }, + { + type: MetadataType.Dashboards, + title: 'Added to 3 dashboards', + description: 'To preview the list of dashboards go to More settings.', + }, + ], +}; + +InteractiveMetadataBar.argTypes = {}; + +InteractiveMetadataBar.parameters = { + docs: { + staticProps: { + items: [ + { type: 'sql', title: 'Click to view query' }, + { + type: 'owner', + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: 'a week ago', + }, + { + type: 'lastModified', + value: 'a week ago', + modifiedBy: 'Jane Smith', + }, + { type: 'tags', values: ['management', 'research', 'poc'] }, + { + type: 'dashboards', + title: 'Added to 3 dashboards', + description: 'To preview the list of dashboards go to More settings.', + }, + ], + }, + liveExample: `function Demo() { + const items = [ + { type: 'sql', title: 'Click to view query' }, + { + type: 'owner', + createdBy: 'Jane Smith', + owners: ['John Doe', 'Mary Wilson'], + createdOn: 'a week ago', + }, + { + type: 'lastModified', + value: 'a week ago', + modifiedBy: 'Jane Smith', + }, + { type: 'tags', values: ['management', 'research', 'poc'] }, + ]; + return ; +}`, + examples: [ + { + title: 'Minimal Metadata', + code: `function MinimalMetadata() { + const items = [ + { type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' }, + { type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' }, + ]; + return ; +}`, + }, + { + title: 'Full Metadata', + code: `function FullMetadata() { + const items = [ + { type: 'sql', title: 'SELECT * FROM ...' }, + { type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' }, + { type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' }, + { type: 'tags', values: ['production', 'finance', 'quarterly'] }, + { type: 'dashboards', title: 'Used in 12 dashboards' }, + { type: 'description', value: 'This chart shows quarterly revenue breakdown by region and product line.' }, + { type: 'rows', title: '1.2M rows' }, + { type: 'table', title: 'public.revenue_data' }, + ]; + return ; +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx index 3b32618b495c..66547cbe3f70 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx @@ -24,6 +24,14 @@ import type { ModalProps, ModalFuncProps } from './types'; export default { title: 'Components/Modal', component: Modal, + parameters: { + docs: { + description: { + component: + 'Modal dialog component for displaying content that requires user attention or interaction. Supports customizable buttons, drag/resize, and confirmation dialogs.', + }, + }, + }, }; export const InteractiveModal = (props: ModalProps) => ( @@ -32,9 +40,9 @@ export const InteractiveModal = (props: ModalProps) => ( InteractiveModal.args = { disablePrimaryButton: false, - primaryButtonName: 'Danger', - primaryButtonStyle: 'danger', - show: true, + primaryButtonName: 'Submit', + primaryButtonStyle: 'primary', + show: false, title: "I'm a modal!", resizable: false, draggable: false, @@ -42,10 +50,119 @@ InteractiveModal.args = { }; InteractiveModal.argTypes = { + show: { + control: 'boolean', + description: + 'Whether the modal is visible. Use the "Try It" example below for a working demo.', + }, + title: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + primaryButtonName: { + control: 'text', + description: 'Text for the primary action button.', + }, + primaryButtonStyle: { + control: 'select', + options: ['primary', 'secondary', 'dashed', 'danger', 'link'], + description: 'The style of the primary action button.', + }, + width: { + control: 'number', + description: 'Width of the modal in pixels.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + disablePrimaryButton: { + control: 'boolean', + description: 'Whether the primary button is disabled.', + }, onHandledPrimaryAction: { action: 'onHandledPrimaryAction' }, onHide: { action: 'onHide' }, }; +InteractiveModal.parameters = { + docs: { + triggerProp: 'show', + onHideProp: 'onHide', + liveExample: `function ModalDemo() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + + setIsOpen(false)} + title="Example Modal" + primaryButtonName="Submit" + onHandledPrimaryAction={() => { + alert('Submitted!'); + setIsOpen(false); + }} + > +

This is the modal content. Click Submit or close the modal.

+
+ + ); +}`, + examples: [ + { + title: 'Danger Modal', + code: `function DangerModal() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + + setIsOpen(false)} + title="Confirm Delete" + primaryButtonName="Delete" + primaryButtonStyle="danger" + onHandledPrimaryAction={() => { + alert('Deleted!'); + setIsOpen(false); + }} + > +

Are you sure you want to delete this item? This action cannot be undone.

+
+ + ); +}`, + }, + { + title: 'Confirmation Dialogs', + code: `function ConfirmationDialogs() { + return ( +
+ + + +
+ ); +}`, + }, + ], + }, +}; + export const ModalFunctions = (props: ModalFuncProps) => (
diff --git a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx index 739cfecc87a2..3fd8bad4bfd4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx @@ -39,6 +39,14 @@ interface IModalTriggerProps { export default { title: 'Components/ModalTrigger', component: ModalTrigger, + parameters: { + docs: { + description: { + component: + 'A component that renders a trigger element which opens a modal when clicked. Useful for actions that need confirmation or additional input.', + }, + }, + }, }; export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( @@ -47,13 +55,116 @@ export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( InteractiveModalTrigger.args = { isButton: true, - modalTitle: 'I am a modal title', - modalBody: 'I am a modal body', - modalFooter: 'I am a modal footer', - tooltip: 'I am a tooltip', + modalTitle: 'Modal Title', + modalBody: 'This is the modal body content.', + tooltip: 'Click to open modal', width: '600px', maxWidth: '1000px', responsive: true, draggable: false, resizable: false, }; + +InteractiveModalTrigger.argTypes = { + triggerNode: { + control: false, + description: 'The clickable element that opens the modal when clicked.', + }, + isButton: { + control: 'boolean', + description: 'Whether to wrap the trigger in a button element.', + }, + modalTitle: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + modalBody: { + control: 'text', + description: 'Content displayed in the modal body.', + }, + tooltip: { + control: 'text', + description: 'Tooltip text shown on hover over the trigger.', + }, + width: { + control: 'text', + description: 'Width of the modal (e.g., "600px", "80%").', + }, + maxWidth: { + control: 'text', + description: 'Maximum width of the modal.', + }, + responsive: { + control: 'boolean', + description: 'Whether the modal should be responsive.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, +}; + +InteractiveModalTrigger.parameters = { + docs: { + // Use a simple span for triggerNode since isButton: true wraps it in a button + staticProps: { + triggerNode: 'Click to Open Modal', + }, + liveExample: `function Demo() { + return ( + Click to Open} + modalTitle="Example Modal" + modalBody={

This is the modal content. You can put any React elements here.

} + width="500px" + responsive + /> + ); +}`, + examples: [ + { + title: 'With Custom Trigger', + code: `function CustomTrigger() { + return ( + + Add New Item + + } + modalTitle="Add New Item" + modalBody={ +
+

Fill out the form to add a new item.

+ +
+ } + width="400px" + /> + ); +}`, + }, + { + title: 'Draggable & Resizable', + code: `function DraggableModal() { + return ( + Open Draggable Modal} + modalTitle="Draggable & Resizable" + modalBody={

Try dragging the header or resizing from the corners!

} + draggable + resizable + width="500px" + /> + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx index c7d58485ece3..f1c3c9b8faad 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx @@ -22,6 +22,14 @@ import { Button } from '../Button'; export default { title: 'Components/Popover', component: Popover, + parameters: { + docs: { + description: { + component: + 'A floating card that appears when hovering or clicking a trigger element. Supports configurable placement, trigger behavior, and custom content.', + }, + }, + }, }; export const InteractivePopover = (args: PopoverProps) => ( @@ -37,31 +45,6 @@ export const InteractivePopover = (args: PopoverProps) => ( ); -const PLACEMENTS = { - label: 'placement', - options: [ - 'topLeft', - 'top', - 'topRight', - 'leftTop', - 'left', - 'leftBottom', - 'rightTop', - 'right', - 'rightBottom', - 'bottomLeft', - 'bottom', - 'bottomRight', - ], - defaultValue: null, -}; - -const TRIGGERS = { - label: 'trigger', - options: ['hover', 'click', 'focus'], - defaultValue: null, -}; - InteractivePopover.args = { content: 'Popover sample content', title: 'Popover title', @@ -70,24 +53,116 @@ InteractivePopover.args = { }; InteractivePopover.argTypes = { + content: { + control: 'text', + description: 'Content displayed inside the popover body.', + }, + title: { + control: 'text', + description: 'Title displayed in the popover header.', + }, placement: { - name: PLACEMENTS.label, control: { type: 'select' }, - options: PLACEMENTS.options, + options: [ + 'topLeft', + 'top', + 'topRight', + 'leftTop', + 'left', + 'leftBottom', + 'rightTop', + 'right', + 'rightBottom', + 'bottomLeft', + 'bottom', + 'bottomRight', + ], + description: 'Position of the popover relative to the trigger element.', }, trigger: { - name: TRIGGERS.label, control: { type: 'select' }, - options: TRIGGERS.options, + options: ['hover', 'click', 'focus'], + description: 'Event that triggers the popover to appear.', }, arrow: { - name: 'arrow', control: { type: 'boolean' }, - description: "Change arrow's visible state", + description: "Whether to show the popover's arrow pointing to the trigger.", }, color: { - name: 'color', control: { type: 'color' }, description: 'The background color of the popover.', }, }; + +InteractivePopover.parameters = { + docs: { + sampleChildren: [{ component: 'Button', props: { children: 'Hover me' } }], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Click Trigger', + code: `function ClickPopover() { + return ( + + + + ); +}`, + }, + { + title: 'Placements', + code: `function PlacementsDemo() { + return ( +
+ {['top', 'right', 'bottom', 'left'].map(placement => ( + + + + ))} +
+ ); +}`, + }, + { + title: 'Rich Content', + code: `function RichPopover() { + return ( + +

Created by: Admin

+

Last modified: Jan 2025

+

Charts: 12

+
+ } + > + + + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx index 46182af6e17c..d056a992ad4d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx @@ -21,10 +21,18 @@ import ProgressBar, { ProgressBarProps } from '.'; export default { title: 'Components/ProgressBar', component: ProgressBar, + parameters: { + docs: { + description: { + component: + 'Progress bar component for displaying completion status. Supports line, circle, and dashboard display types.', + }, + }, + }, }; export const InteractiveProgressBar = (args: ProgressBarProps) => ( - + ); export const InteractiveProgressCircle = (args: ProgressBarProps) => ( @@ -35,6 +43,120 @@ export const InteractiveProgressDashboard = (args: ProgressBarProps) => ( ); +InteractiveProgressBar.args = { + percent: 75, + status: 'normal', + type: 'line', + striped: false, + showInfo: true, + strokeLinecap: 'round', +}; + +InteractiveProgressBar.argTypes = { + percent: { + control: { type: 'number', min: 0, max: 100 }, + description: 'Completion percentage (0-100).', + }, + status: { + control: 'select', + options: ['normal', 'success', 'exception', 'active'], + description: 'Current status of the progress bar.', + }, + type: { + control: 'select', + options: ['line', 'circle', 'dashboard'], + description: 'Display type: line, circle, or dashboard gauge.', + }, + striped: { + control: 'boolean', + description: 'Whether to show striped animation on the bar.', + }, + showInfo: { + control: 'boolean', + description: 'Whether to show the percentage text.', + }, + strokeColor: { + control: 'color', + description: 'Color of the progress bar fill.', + }, + trailColor: { + control: 'color', + description: 'Color of the unfilled portion.', + }, + strokeLinecap: { + control: 'select', + options: ['round', 'butt', 'square'], + description: 'Shape of the progress bar endpoints.', + }, +}; + +InteractiveProgressBar.parameters = { + docs: { + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'All Progress Types', + code: `function AllTypesDemo() { + return ( +
+
+

Line

+ +
+
+

Circle

+ +
+
+

Dashboard

+ +
+
+ ); +}`, + }, + { + title: 'Status Variants', + code: `function StatusDemo() { + const statuses = ['normal', 'success', 'exception', 'active']; + return ( +
+ {statuses.map(status => ( +
+ {status} + +
+ ))} +
+ ); +}`, + }, + { + title: 'Custom Colors', + code: `function CustomColors() { + return ( +
+ + + + +
+ ); +}`, + }, + ], + }, +}; + const commonArgs = { striped: true, percent: 90, @@ -46,39 +168,40 @@ const commonArgs = { }; const commonArgTypes = { + percent: { + control: { type: 'number', min: 0, max: 100 }, + description: 'Completion percentage (0-100).', + }, + striped: { + control: 'boolean', + description: 'Whether to show striped animation on the bar.', + }, + showInfo: { + control: 'boolean', + description: 'Whether to show the percentage text.', + }, + strokeColor: { + control: 'color', + description: 'Color of the progress bar.', + }, + trailColor: { + control: 'color', + description: 'Color of the unfilled portion.', + }, strokeLinecap: { - control: { - type: 'select', - }, + control: 'select', options: ['round', 'butt', 'square'], + description: 'Shape of the progress bar endpoints.', }, type: { - control: { - type: 'select', - }, + control: 'select', options: ['line', 'circle', 'dashboard'], - }, -}; - -InteractiveProgressBar.args = { - ...commonArgs, - status: 'normal', -}; - -InteractiveProgressBar.argTypes = { - ...commonArgTypes, - status: { - control: { - type: 'select', - }, - options: ['normal', 'success', 'exception', 'active'], + description: 'Display type: line, circle, or dashboard gauge.', }, }; InteractiveProgressCircle.args = commonArgs; - InteractiveProgressCircle.argTypes = commonArgTypes; InteractiveProgressDashboard.args = commonArgs; - InteractiveProgressDashboard.argTypes = commonArgTypes; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx index c0453ed038fa..403fd0eed731 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Radio/Radio.stories.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import type { StoryObj } from '@storybook/react'; import { css } from '@apache-superset/core/ui'; import { Icons } from '@superset-ui/core/components/Icons'; import { Space } from '../Space'; @@ -25,20 +26,32 @@ export default { title: 'Components/Radio', component: Radio, tags: ['autodocs'], + parameters: { + docs: { + description: { + component: + 'Radio button component for selecting one option from a set. Supports standalone radio buttons, radio buttons styled as buttons, and grouped radio buttons with layout configuration.', + }, + }, + }, }; const RadioArgsType = { value: { control: 'text', - description: 'The value of the radio button.', + description: 'The value associated with this radio button.', }, disabled: { control: 'boolean', - description: 'Whether the radio button is disabled or not.', + description: 'Whether the radio button is disabled.', }, checked: { control: 'boolean', - description: 'The checked state of the radio button.', + description: 'Whether the radio button is checked (controlled mode).', + }, + children: { + control: 'text', + description: 'Label text displayed next to the radio button.', }, }; @@ -76,14 +89,66 @@ const radioGroupWrapperArgsType = { }, }; -export const RadioStory = { +export const RadioStory: StoryObj = { args: { value: 'radio1', disabled: false, checked: false, children: 'Radio', }, - argTypes: RadioArgsType, + argTypes: { + value: { + control: 'text', + description: 'The value associated with this radio button.', + }, + disabled: { + control: 'boolean', + description: 'Whether the radio button is disabled.', + }, + checked: { + control: 'boolean', + description: 'Whether the radio button is checked (controlled mode).', + }, + children: { + control: 'text', + description: 'Label text displayed next to the radio button.', + }, + }, +}; + +RadioStory.parameters = { + docs: { + examples: [ + { + title: 'Radio Button Variants', + code: `function RadioButtonDemo() { + const [value, setValue] = React.useState('line'); + return ( + setValue(e.target.value)}> + Line Chart + Bar Chart + Pie Chart + + ); +}`, + }, + { + title: 'Vertical Radio Group', + code: `function VerticalDemo() { + const [value, setValue] = React.useState('option1'); + return ( + setValue(e.target.value)}> +
+ First option + Second option + Third option +
+
+ ); +}`, + }, + ], + }, }; export const RadioButtonStory = (args: RadioProps) => ( diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx index 3323a487281e..97e7f840df4b 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx @@ -16,14 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import { StoryObj } from '@storybook/react'; -import { noop } from 'lodash'; import { SelectOptionsType, SelectProps } from './types'; import { Select } from '.'; export default { title: 'Components/Select', component: Select, + parameters: { + docs: { + description: { + component: + 'A versatile select component supporting single and multi-select modes, search filtering, option creation, and both synchronous and asynchronous data sources.', + }, + }, + }, }; const DEFAULT_WIDTH = 200; @@ -88,145 +94,210 @@ const generateOptions = (opts: SelectOptionsType, count: number) => { return generated.slice(0, count); }; -export const InteractiveSelect: StoryObj = { - render: ({ - header, - options, - optionsCount, - ...args - }: SelectProps & { header: string; optionsCount: number }) => { - noop(header); - return ( -
- +
+); + +InteractiveSelect.args = { + mode: 'single', + placeholder: 'Select ...', + showSearch: true, + allowNewOptions: false, + allowClear: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + oneLine: false, + maxTagCount: 4, +}; + +InteractiveSelect.argTypes = { + mode: { + control: 'inline-radio', + options: ['single', 'multiple'], + description: 'Whether to allow selection of a single option or multiple.', }, - args: { - autoFocus: true, - allowNewOptions: false, - allowClear: false, - autoClearSearchValue: false, - allowSelectAll: true, - disabled: false, - header: 'none', - invertSelection: false, - labelInValue: true, - maxTagCount: 4, - mode: 'single', - oneLine: false, - options, - optionsCount: options.length, - optionFilterProps: ['value', 'label', 'custom'], - placeholder: 'Select ...', - showSearch: true, + placeholder: { + control: 'text', + description: 'Placeholder text when no option is selected.', }, - argTypes: { - options: { - description: `It defines the options of the Select. - The options can be static, an array of options. - The options can also be async, a promise that returns an array of options. - `, - }, - ariaLabel: { - description: `It adds the aria-label tag for accessibility standards. - Must be plain English and localized. - `, - }, - labelInValue: { - table: { - disable: true, - }, - }, - name: { - table: { - disable: true, - }, + showSearch: { + control: 'boolean', + description: 'Whether to show a search input for filtering.', + }, + allowNewOptions: { + control: 'boolean', + description: + 'Whether users can create new options by typing a value not in the list.', + }, + allowClear: { + control: 'boolean', + description: 'Whether to show a clear button to reset the selection.', + }, + allowSelectAll: { + control: 'boolean', + description: 'Whether to show a "Select All" option in multiple mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the select is disabled.', + }, + invertSelection: { + control: 'boolean', + description: + 'Shows a stop icon instead of a checkmark on selected options, indicating deselection on click.', + }, + oneLine: { + control: 'boolean', + description: + 'Forces tags onto one line with overflow count. Requires multiple mode.', + }, + maxTagCount: { + control: { type: 'number' }, + description: + 'Maximum number of tags to display in multiple mode before showing an overflow count.', + }, +}; + +InteractiveSelect.parameters = { + docs: { + staticProps: { + options: [ + { + label: 'Such an incredibly awesome long long label', + value: 'long-label-1', + }, + { + label: 'Another incredibly awesome long long label', + value: 'long-label-2', + }, + { label: 'Option A', value: 'A' }, + { label: 'Option B', value: 'B' }, + { label: 'Option C', value: 'C' }, + { label: 'Option D', value: 'D' }, + { label: 'Option E', value: 'E' }, + { label: 'Option F', value: 'F' }, + { label: 'Option G', value: 'G' }, + { label: 'Option H', value: 'H' }, + { label: 'Option I', value: 'I' }, + ], }, - notFoundContent: { - table: { - disable: true, + liveExample: `function Demo() { + return ( +
+ +
+ ); +}`, }, - }, - mappedMode: { - table: { - disable: true, + { + title: 'Allow New Options', + code: `function AllowNewDemo() { + return ( +
+ +
+ ); +}`, }, - }, - allowNewOptions: { - description: `It enables the user to create new options. - Can be used with standard or async select types. - Can be used with any mode, single or multiple. False by default. - `, - }, - invertSelection: { - description: `It shows a stop-outlined icon at the far right of a selected - option instead of the default checkmark. - Useful to better indicate to the user that by clicking on a selected - option it will be de-selected. False by default. - `, - }, - optionFilterProps: { - description: `It allows to define which properties of the option object - should be looked for when searching. - By default label and value. - `, - }, - oneLine: { - description: `Sets maxTagCount to 1. The overflow tag is always displayed in - the same line, line wrapping is disabled. - When the dropdown is open, sets maxTagCount to 0, - displays only the overflow tag. - Requires '"mode=multiple"'. - `, - }, - maxTagCount: { - description: `Sets maxTagCount attribute. The overflow tag is displayed in - place of the remaining items. - Requires '"mode=multiple"'. - `, - }, - optionsCount: { - control: { - type: 'number', + { + title: 'One Line Mode', + code: `function OneLineDemo() { + return ( +
+ +
+ ); +}; + +AdvancedPlayground.args = { + autoFocus: true, + allowNewOptions: false, + allowClear: false, + autoClearSearchValue: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + labelInValue: true, + maxTagCount: 4, + mode: 'multiple', + oneLine: false, + optionsCount: options.length, + optionFilterProps: ['value', 'label', 'custom'], + placeholder: 'Select ...', + showSearch: true, +}; + +AdvancedPlayground.argTypes = { + mode: { + control: { type: 'inline-radio' }, + options: ['single', 'multiple'], + }, + optionsCount: { + control: { type: 'number' }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx index cafbfdb1de7c..e873c30bbe8c 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Skeleton/Skeleton.stories.tsx @@ -17,131 +17,114 @@ * under the License. */ import type { Meta, StoryObj } from '@storybook/react'; -import { type SkeletonButtonProps } from 'antd/es/skeleton/Button'; import { Space } from '../Space'; -import { AvatarProps } from '../Avatar/types'; import { Skeleton, type SkeletonProps } from '.'; const { Avatar, Button, Input, Image } = Skeleton; +type SkeletonStoryArgs = SkeletonProps & { + shape?: 'circle' | 'square'; + size?: 'large' | 'small' | 'default'; + block?: boolean; +}; + export default { title: 'Components/Skeleton', component: Skeleton, subcomponents: { Avatar, Button, Input, Image }, + parameters: { + docs: { + description: { + component: + 'Skeleton loading component with support for avatar, title, paragraph, button, and input placeholders.', + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const InteractiveSkeleton: Story = { + args: { + active: true, + avatar: false, + loading: true, + title: true, + shape: 'circle', + size: 'default', + block: false, + }, argTypes: { - // Skeleton props active: { control: 'boolean', - description: 'Show animation effect', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Show animation effect.', }, avatar: { control: 'boolean', - description: 'Show avatar placeholder', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: false }, - }, + description: 'Show avatar placeholder.', }, loading: { control: 'boolean', - description: 'Display the skeleton when true', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - }, - }, - paragraph: { - control: 'false', - description: 'Paragraph skeleton', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: 'true' }, - }, - }, - round: { - control: false, - description: 'Show paragraph and title radius when true ', - table: { - category: 'Skeleton', - type: { summary: 'boolean' }, - defaultValue: { summary: false }, - }, + description: 'Display the skeleton when true.', }, title: { control: 'boolean', - description: 'Show title placeholder', - table: { - category: 'Skeleton', - type: { summary: 'boolean | object' }, - defaultValue: { summary: true }, - }, + description: 'Show title placeholder.', }, - - // Skeleton.Avatar props shape: { control: 'select', - description: 'Shape of the avatar', + description: 'Shape of the avatar/button skeleton.', options: ['circle', 'square'], - table: { - name: 'shape', - category: 'Avatar | Button', - type: { summary: 'string' }, - }, }, size: { control: 'select', options: ['large', 'small', 'default'], - description: 'Set the size of avatar in the skeleton', - table: { - category: 'Avatar | Button', - type: { summary: 'number | string' }, - }, - }, - - // Skeleton.Title props - width: { - control: false, - description: 'Set the width of title in the skeleton', - table: { - category: 'Title', - type: { summary: 'number | string' }, - }, + description: 'Size of the skeleton elements.', }, - - // Skeleton.Button props block: { control: 'boolean', - description: 'Option to fit button width to its parent width', - table: { - category: 'Button', - type: { summary: 'boolean' }, - defaultValue: { summary: false }, - }, + description: 'Option to fit button width to its parent width.', }, }, + render: args => { + const avatar = { + shape: args.shape, + size: args.size, + }; + const button = { + block: args.block, + shape: args.shape, + size: args.size, + }; + + return ( + + Skeleton + + Avatar + + Button + + + ); + }, parameters: { docs: { description: { - component: - 'Skeleton loading component with support for avatar, title, paragraph, button, and input placeholders.', + story: 'A loading placeholder for content that is not yet loaded.', }, + liveExample: `function Demo() { + return ( + + ); +}`, }, }, -} as Meta< - typeof Skeleton & typeof Avatar & typeof Button & typeof Input & typeof Image ->; - -type Story = StoryObj; +}; +// Keep original for backwards compatibility export const SkeletonStory: Story = { - render: (args: SkeletonProps & AvatarProps & SkeletonButtonProps) => { + render: args => { const avatar = { shape: args.shape, size: args.size, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx index 121f1fa04845..3864bff4cd89 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx @@ -21,6 +21,14 @@ import Slider, { SliderSingleProps, SliderRangeProps } from '.'; export default { title: 'Components/Slider', component: Slider, + parameters: { + docs: { + description: { + component: + 'A slider input for selecting a value or range from a continuous or stepped interval. Supports single value, range, vertical orientation, marks, and tooltip display.', + }, + }, + }, }; const tooltipPlacement = [ @@ -75,33 +83,182 @@ InteractiveSlider.args = { max: 100, defaultValue: 70, step: 1, - marks: {}, disabled: false, reverse: false, vertical: false, - autoFocus: false, keyboard: true, dots: false, included: true, - tooltipPosition: 'bottom', }; InteractiveSlider.argTypes = { - onChange: { action: 'onChange' }, - onChangeComplete: { action: 'onChangeComplete' }, + min: { + control: { type: 'number' }, + description: 'Minimum value of the slider.', + }, + max: { + control: { type: 'number' }, + description: 'Maximum value of the slider.', + }, + defaultValue: { + control: { type: 'number' }, + description: 'Initial value of the slider.', + }, + step: { + control: { type: 'number' }, + description: 'Step increment between values. Use null for marks-only mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the slider is disabled.', + }, + reverse: { + control: 'boolean', + description: 'Whether to reverse the slider direction.', + }, + vertical: { + control: 'boolean', + description: 'Whether to display the slider vertically.', + }, + keyboard: { + control: 'boolean', + description: 'Whether keyboard arrow keys can control the slider.', + }, + dots: { + control: 'boolean', + description: 'Whether to show dots at each step mark.', + }, + included: { + control: 'boolean', + description: 'Whether to highlight the filled portion of the track.', + }, tooltipOpen: { - control: { type: 'boolean' }, + control: 'boolean', + description: 'Whether the value tooltip is always visible.', }, tooltipPosition: { - options: tooltipPlacement, control: { type: 'select' }, + options: [ + 'top', + 'left', + 'bottom', + 'right', + 'topLeft', + 'topRight', + 'bottomLeft', + 'bottomRight', + 'leftTop', + 'leftBottom', + 'rightTop', + 'rightBottom', + ], + description: 'Position of the value tooltip relative to the handle.', + }, + onChange: { action: 'onChange' }, + onChangeComplete: { action: 'onChangeComplete' }, +}; + +InteractiveSlider.parameters = { + docs: { + liveExample: `function Demo() { + return ( +
+ +
+ ); +}`, + examples: [ + { + title: 'Range Slider', + code: `function RangeSliderDemo() { + return ( +
+

Basic Range

+ +
+

Draggable Track

+ +
+ ); +}`, + }, + { + title: 'With Marks', + code: `function MarksDemo() { + return ( +
+ +
+ ); +}`, + }, + { + title: 'Stepped and Dots', + code: `function SteppedDemo() { + return ( +
+

Step = 10 with Dots

+ +
+

Step = 25

+ +
+ ); +}`, + }, + { + title: 'Vertical Slider', + code: `function VerticalDemo() { + return ( +
+ + + +
+ ); +}`, + }, + ], }, }; InteractiveRangeSlider.args = { - ...InteractiveSlider.args, + min: 0, + max: 100, defaultValue: [50, 70], + step: 1, + disabled: false, + reverse: false, + vertical: false, + keyboard: true, + dots: false, + included: true, draggableTrack: false, }; -InteractiveRangeSlider.argTypes = InteractiveSlider.argTypes; +InteractiveRangeSlider.argTypes = { + ...InteractiveSlider.argTypes, + draggableTrack: { + control: 'boolean', + description: + 'Whether the track between handles can be dragged to move both handles together.', + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx index c6e935521d77..39ca191f0167 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Space/Space.stories.tsx @@ -23,10 +23,23 @@ export default { component: Space, }; +// Sample children used in both Storybook and auto-generated docs +const SAMPLE_ITEMS = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + +// Shared styling for sample items - matches docs site rendering +const sampleItemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', +}; + export const InteractiveSpace = (args: SpaceProps) => ( - {new Array(20).fill(null).map((_, i) => ( -

Item

+ {SAMPLE_ITEMS.map((item, i) => ( +
+ {item} +
))}
); @@ -51,3 +64,75 @@ InteractiveSpace.argTypes = { options: ['small', 'middle', 'large'], }, }; + +InteractiveSpace.parameters = { + docs: { + // Inline for the static parser (can't resolve variable references) + sampleChildren: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'], + sampleChildrenStyle: { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: '4px', + }, + liveExample: `function Demo() { + return ( + + {['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map(item => ( +
+ {item} +
+ ))} +
+ ); +}`, + examples: [ + { + title: 'Vertical Space', + code: `function VerticalSpace() { + return ( + + + + + + ); +}`, + }, + { + title: 'Space Sizes', + code: `function SpaceSizes() { + const items = ['Item 1', 'Item 2', 'Item 3']; + const itemStyle = { + padding: '8px 16px', + background: '#e6f4ff', + border: '1px solid #91caff', + borderRadius: 4, + }; + return ( +
+ {['small', 'middle', 'large'].map(size => ( +
+

{size}

+ + {items.map(item => ( +
{item}
+ ))} +
+
+ ))} +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx index 336c0ef0dd05..b61846b66e8b 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx @@ -22,12 +22,21 @@ import { Steps, type StepsProps } from '.'; export default { title: 'Components/Steps', component: Steps as typeof AntdSteps, + parameters: { + docs: { + description: { + component: + 'A navigation component for guiding users through multi-step workflows. Supports horizontal, vertical, and inline layouts with progress tracking.', + }, + }, + }, }; export const InteractiveSteps = (args: StepsProps) => ; + InteractiveSteps.args = { direction: 'horizontal', - initial: 0, + current: 1, labelPlacement: 'horizontal', progressDot: false, size: 'default', @@ -51,23 +60,145 @@ InteractiveSteps.args = { InteractiveSteps.argTypes = { direction: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Layout direction of the steps.', + }, + current: { + control: { type: 'number' }, + description: 'Index of the current step (zero-based).', }, labelPlacement: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Position of step labels relative to the step icon.', + }, + progressDot: { + control: 'boolean', + description: 'Whether to use a dot style instead of numbered icons.', }, size: { - options: ['default', 'small'], control: { type: 'select' }, + options: ['default', 'small'], + description: 'Size of the step icons and text.', }, status: { - options: ['wait', 'process', 'finish', 'error'], control: { type: 'select' }, + options: ['wait', 'process', 'finish', 'error'], + description: 'Status of the current step.', }, type: { - options: ['default', 'navigation', 'inline'], control: { type: 'select' }, + options: ['default', 'navigation', 'inline'], + description: + 'Visual style: default numbered, navigation breadcrumb, or inline compact.', + }, +}; + +InteractiveSteps.parameters = { + docs: { + staticProps: { + items: [ + { title: 'Connect Database', description: 'Configure the connection' }, + { title: 'Create Dataset', description: 'Select tables and columns' }, + { title: 'Build Chart', description: 'Choose visualization type' }, + ], + }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Vertical Steps', + code: `function VerticalSteps() { + return ( + + ); +}`, + }, + { + title: 'Status Indicators', + code: `function StatusSteps() { + return ( +
+
+

Error on Step 2

+ +
+
+

All Complete

+ +
+
+ ); +}`, + }, + { + title: 'Dot Style and Small Size', + code: `function DotAndSmall() { + return ( +
+
+

Progress Dots

+ +
+
+

Small Size

+ +
+
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx index 508549ac0e3f..19f3626a9a09 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx @@ -21,6 +21,14 @@ import { Switch, type SwitchProps } from '.'; export default { title: 'Components/Switch', + parameters: { + docs: { + description: { + component: + 'A toggle switch for boolean on/off states. Supports loading indicators, sizing, and an HTML title attribute for accessibility tooltips.', + }, + }, + }, }; export const InteractiveSwitch = ({ checked, ...rest }: SwitchProps) => { @@ -39,15 +47,121 @@ InteractiveSwitch.args = { checked: defaultCheckedValue, disabled: false, loading: false, - title: 'Switch', + title: 'Toggle feature', defaultChecked: defaultCheckedValue, - autoFocus: true, }; InteractiveSwitch.argTypes = { + checked: { + control: 'boolean', + description: 'Whether the switch is on.', + }, + disabled: { + control: 'boolean', + description: 'Whether the switch is disabled.', + }, + loading: { + control: 'boolean', + description: 'Whether to show a loading spinner inside the switch.', + }, + title: { + control: 'text', + description: + 'HTML title attribute shown as a browser tooltip on hover. Useful for accessibility.', + }, size: { - defaultValue: 'default', control: { type: 'radio' }, options: ['small', 'default'], + description: 'Size of the switch.', + }, +}; + +InteractiveSwitch.parameters = { + docs: { + liveExample: `function Demo() { + const [checked, setChecked] = React.useState(true); + return ( +
+ + {checked ? 'On' : 'Off'} + (hover the switch to see the title tooltip) +
+ ); +}`, + examples: [ + { + title: 'Switch States', + code: `function SwitchStates() { + return ( +
+
+ + Checked +
+
+ + Unchecked +
+
+ + Disabled (on) +
+
+ + Disabled (off) +
+
+ + Loading +
+
+ ); +}`, + }, + { + title: 'Sizes', + code: `function SizesDemo() { + return ( +
+
+ + Small +
+
+ + Default +
+
+ ); +}`, + }, + { + title: 'Settings Panel', + code: `function SettingsPanel() { + const [notifications, setNotifications] = React.useState(true); + const [darkMode, setDarkMode] = React.useState(false); + const [autoRefresh, setAutoRefresh] = React.useState(true); + return ( +
+

Dashboard Settings

+ {[ + { label: 'Email notifications', checked: notifications, onChange: setNotifications, title: 'Toggle email notifications' }, + { label: 'Dark mode', checked: darkMode, onChange: setDarkMode, title: 'Toggle dark mode' }, + { label: 'Auto-refresh data', checked: autoRefresh, onChange: setAutoRefresh, title: 'Toggle auto-refresh' }, + ].map(({ label, checked, onChange, title }) => ( +
+ {label} + +
+ ))} +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx index 2a409e2a3c98..98ff8e0443bd 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/Table.stories.tsx @@ -42,9 +42,16 @@ import HeaderWithRadioGroup from './header-renderers/HeaderWithRadioGroup'; import TimeCell from './cell-renderers/TimeCell'; export default { - title: 'Design System/Components/Table/Examples', + title: 'Design System/Components/Table', component: Table, - argTypes: { onClick: { action: 'clicked' } }, + parameters: { + docs: { + description: { + component: + 'A data table component with sorting, pagination, row selection, resizable columns, reorderable columns, and virtualization for large datasets.', + }, + }, + }, } as Meta; interface BasicData { @@ -211,6 +218,187 @@ const basicColumns: ColumnsType = [ }, ]; +// Interactive story for docs generation +export const InteractiveTable = (args: TableProps) => ( +
+); + +InteractiveTable.args = { + size: 'small', + bordered: false, + loading: false, + sticky: true, + resizable: false, + reorderable: false, + usePagination: false, + data: [ + { + key: 1, + name: 'Floppy Disk 10 pack', + category: 'Disk Storage', + price: 9.99, + }, + { key: 2, name: 'DVD 100 pack', category: 'Optical Storage', price: 27.99 }, + { key: 3, name: '128 GB SSD', category: 'Harddrive', price: 49.99 }, + { key: 4, name: '4GB 144mhz', category: 'Memory', price: 19.99 }, + { + key: 5, + name: '1GB USB Flash Drive', + category: 'Portable Storage', + price: 9.99, + }, + ], + columns: [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 200 }, + { title: 'Category', dataIndex: 'category', key: 'category', width: 150 }, + { title: 'Price', dataIndex: 'price', key: 'price', width: 100 }, + ], +}; + +InteractiveTable.argTypes = { + size: { + control: 'select', + options: ['small', 'middle', 'large'], + description: 'Table size.', + }, + bordered: { + control: 'boolean', + description: 'Whether to show all table borders.', + }, + loading: { + control: 'boolean', + description: 'Whether the table is in a loading state.', + }, + sticky: { + control: 'boolean', + description: 'Whether the table header is sticky.', + }, + resizable: { + control: 'boolean', + description: 'Whether columns can be resized by dragging column edges.', + }, + reorderable: { + control: 'boolean', + description: + 'EXPERIMENTAL: Whether columns can be reordered by dragging. May not work in all contexts.', + }, + usePagination: { + control: 'boolean', + description: + 'Whether to enable pagination. When enabled, the table displays 5 rows per page.', + }, +}; + +InteractiveTable.parameters = { + docs: { + staticProps: { + height: 350, + defaultPageSize: 5, + pageSizeOptions: ['5', '10'], + data: [ + { + key: 1, + name: 'Floppy Disk 10 pack', + category: 'Disk Storage', + price: 9.99, + }, + { + key: 2, + name: 'DVD 100 pack', + category: 'Optical Storage', + price: 27.99, + }, + { key: 3, name: '128 GB SSD', category: 'Harddrive', price: 49.99 }, + { key: 4, name: '4GB 144mhz', category: 'Memory', price: 19.99 }, + { + key: 5, + name: '1GB USB Flash Drive', + category: 'Portable Storage', + price: 9.99, + }, + { key: 6, name: '256 GB SSD', category: 'Harddrive', price: 89.99 }, + { key: 7, name: '1 TB SSD', category: 'Harddrive', price: 349.99 }, + { key: 8, name: '16 GB DDR4', category: 'Memory', price: 59.99 }, + { key: 9, name: '32 GB DDR5', category: 'Memory', price: 129.99 }, + { + key: 10, + name: 'Blu-ray 50 pack', + category: 'Optical Storage', + price: 34.99, + }, + { + key: 11, + name: '64 GB USB Drive', + category: 'Portable Storage', + price: 14.99, + }, + { key: 12, name: '2 TB HDD', category: 'Harddrive', price: 59.99 }, + ], + columns: [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 200 }, + { + title: 'Category', + dataIndex: 'category', + key: 'category', + width: 150, + }, + { title: 'Price', dataIndex: 'price', key: 'price', width: 100 }, + ], + }, + liveExample: `function Demo() { + const data = [ + { key: 1, name: 'PostgreSQL', type: 'Database', status: 'Active' }, + { key: 2, name: 'MySQL', type: 'Database', status: 'Active' }, + { key: 3, name: 'SQLite', type: 'Database', status: 'Inactive' }, + { key: 4, name: 'Presto', type: 'Query Engine', status: 'Active' }, + ]; + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name', width: 150 }, + { title: 'Type', dataIndex: 'type', key: 'type' }, + { title: 'Status', dataIndex: 'status', key: 'status', width: 100 }, + ]; + return
; +}`, + examples: [ + { + title: 'With Pagination', + code: `function PaginatedTable() { + const data = Array.from({ length: 20 }, (_, i) => ({ + key: i, + name: 'Record ' + (i + 1), + value: Math.round(Math.random() * 1000), + category: ['A', 'B', 'C'][i % 3], + })); + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name' }, + { title: 'Value', dataIndex: 'value', key: 'value', width: 100 }, + { title: 'Category', dataIndex: 'category', key: 'category', width: 100 }, + ]; + return ( +
+ ); +}`, + }, + { + title: 'Loading State', + code: `function LoadingTable() { + const columns = [ + { title: 'Name', dataIndex: 'name', key: 'name' }, + { title: 'Status', dataIndex: 'status', key: 'status' }, + ]; + return
; +}`, + }, + ], + }, +}; + const bigColumns: ColumnsType = [ { title: 'Name', diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx index acbb24fffbf1..b081d23d06cc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/TableOverview.stories.tsx @@ -19,7 +19,7 @@ import Markdown from 'markdown-to-jsx'; export default { - title: 'Design System/Components/Table"', + title: 'Design System/Components/Table/TableOverview', }; export const Overview = () => ( diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx index 08c131060e1c..7f476eaa8759 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx @@ -21,6 +21,14 @@ import { TableView, TableViewProps, EmptyWrapperType } from '.'; export default { title: 'Components/TableView', component: TableView, + parameters: { + docs: { + description: { + component: + 'A data table component with sorting, pagination, text wrapping, and empty state support. Built on react-table.', + }, + }, + }, }; export const InteractiveTableView = (args: TableViewProps) => ( @@ -67,7 +75,7 @@ InteractiveTableView.args = { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet purus convallis placerat in at nunc. Nulla nec viverra augue.', }, { - id: 321, + id: 456, age: 10, name: 'John Smith', summary: @@ -76,7 +84,7 @@ InteractiveTableView.args = { ], initialSortBy: [{ id: 'name', desc: true }], noDataText: 'No data here', - pageSize: 1, + pageSize: 2, showRowCount: true, withPagination: true, columnsForWrapText: ['Summary'], @@ -84,22 +92,147 @@ InteractiveTableView.args = { }; InteractiveTableView.argTypes = { + pageSize: { + control: { type: 'number', min: 1 }, + description: 'Number of rows displayed per page.', + }, + withPagination: { + control: 'boolean', + description: 'Whether to show pagination controls below the table.', + }, + showRowCount: { + control: 'boolean', + description: 'Whether to display the total row count alongside pagination.', + }, + noDataText: { + control: 'text', + description: 'Text displayed when the table has no data.', + }, + scrollTopOnPagination: { + control: 'boolean', + description: + 'Whether to scroll to the top of the table when changing pages.', + }, emptyWrapperType: { - control: { - type: 'select', - }, + control: { type: 'select' }, options: [EmptyWrapperType.Default, EmptyWrapperType.Small], - }, - pageSize: { - control: { - type: 'number', - min: 1, - }, + description: 'Style of the empty state wrapper.', }, initialPageIndex: { - control: { - type: 'number', - min: 0, + control: { type: 'number', min: 0 }, + description: 'Initial page to display (zero-based).', + }, +}; + +InteractiveTableView.parameters = { + docs: { + staticProps: { + columns: [ + { accessor: 'id', Header: 'ID', sortable: true, id: 'id' }, + { accessor: 'age', Header: 'Age', id: 'age' }, + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'summary', Header: 'Summary', id: 'summary' }, + ], + data: [ + { + id: 123, + age: 27, + name: 'Emily', + summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + { + id: 321, + age: 10, + name: 'Kate', + summary: 'Nam id porta neque, a vehicula orci.', + }, + { + id: 456, + age: 10, + name: 'John Smith', + summary: 'Maecenas rhoncus elit sit amet purus convallis placerat.', + }, + ], }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Without Pagination', + code: `function NoPaginationDemo() { + return ( + + ); +}`, + }, + { + title: 'Empty State', + code: `function EmptyDemo() { + return ( + + ); +}`, + }, + { + title: 'With Sorting', + code: `function SortingDemo() { + return ( + + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx index 9cc87fcc6a62..dd35f72a1a68 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tabs/Tabs.stories.tsx @@ -21,61 +21,143 @@ import Tabs, { TabsProps } from '.'; export default { title: 'Components/Tabs', component: Tabs, + parameters: { + docs: { + description: { + component: + 'A tabs component for switching between different views or content sections. ' + + 'Supports multiple tab styles, positions, and sizes.', + }, + }, + }, }; -export const InteractiveTabs = (args: TabsProps) => ; +// Demo tab items (kept separate from args to avoid parser issues) +const demoItems = [ + { key: '1', label: 'Tab 1', children: 'Content of Tab Pane 1' }, + { key: '2', label: 'Tab 2', children: 'Content of Tab Pane 2' }, + { key: '3', label: 'Tab 3', children: 'Content of Tab Pane 3' }, +]; + +export const InteractiveTabs = (args: TabsProps) => ( + +); InteractiveTabs.args = { defaultActiveKey: '1', - animated: true, - centered: false, - allowOverflow: false, + type: 'line', tabPosition: 'top', size: 'middle', + animated: true, + centered: false, tabBarGutter: 8, - items: [ - { - key: '1', - label: 'Tab 1', - children: 'Content of Tab Pane 1', - }, - { - key: '2', - label: 'Tab 2', - children: 'Content of Tab Pane 2', - }, - { - key: '3', - label: 'Tab 3', - children: 'Content of Tab Pane 3', - }, - ], }; InteractiveTabs.argTypes = { onChange: { action: 'onChange' }, type: { - defaultValue: 'line', - control: { - type: 'inline-radio', - }, + description: 'The style of tabs. Options: line, card, editable-card.', + control: { type: 'inline-radio' }, options: ['line', 'card', 'editable-card'], }, tabPosition: { - control: { - type: 'inline-radio', - }, + description: 'Position of tabs. Options: top, bottom, left, right.', + control: { type: 'inline-radio' }, options: ['top', 'bottom', 'left', 'right'], }, size: { - control: { - type: 'inline-radio', - }, + description: 'Size of the tabs.', + control: { type: 'inline-radio' }, options: ['small', 'middle', 'large'], }, + animated: { + description: 'Whether to animate tab transitions.', + control: { type: 'boolean' }, + }, + centered: { + description: 'Whether to center the tabs.', + control: { type: 'boolean' }, + }, tabBarGutter: { - control: { - type: 'number', + description: 'The gap between tabs.', + control: { type: 'number' }, + }, +}; + +InteractiveTabs.parameters = { + docs: { + staticProps: { + items: [ + { key: '1', label: 'Tab 1', children: 'Content of Tab Pane 1' }, + { key: '2', label: 'Tab 2', children: 'Content of Tab Pane 2' }, + { key: '3', label: 'Tab 3', children: 'Content of Tab Pane 3' }, + ], }, + liveExample: `function Demo() { + return ( + + ); +}`, + examples: [ + { + title: 'Card Style', + code: `function CardTabs() { + return ( + + ); +}`, + }, + { + title: 'Tab Positions', + code: `function TabPositions() { + const items = [ + { key: '1', label: 'Tab 1', children: 'Content 1' }, + { key: '2', label: 'Tab 2', children: 'Content 2' }, + { key: '3', label: 'Tab 3', children: 'Content 3' }, + ]; + return ( +
+ {['top', 'bottom', 'left', 'right'].map(pos => ( +
+

{pos}

+ +
+ ))} +
+ ); +}`, + }, + { + title: 'With Icons', + code: `function IconTabs() { + return ( + Dashboards, children: 'Dashboard content here.' }, + { key: '2', label: <> Charts, children: 'Chart content here.' }, + { key: '3', label: <> Datasets, children: 'Dataset content here.' }, + { key: '4', label: <> SQL Lab, children: 'SQL Lab content here.' }, + ]} + /> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx index e64567a100b2..a42d0bc2eeb4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Timer/Timer.stories.tsx @@ -22,30 +22,31 @@ import { Timer, TimerProps } from '.'; export default { title: 'Components/Timer', component: Timer, + parameters: { + docs: { + description: { + component: + 'A live elapsed-time display that counts up from a given start time. Used to show query and dashboard load durations. Requires a startTime timestamp to function.', + }, + }, + }, }; export const InteractiveTimer = (args: TimerProps) => ; InteractiveTimer.args = { - isRunning: false, + isRunning: true, + status: 'success', }; InteractiveTimer.argTypes = { - startTime: { - defaultValue: extendedDayjs().utc().valueOf(), - table: { - disable: true, - }, - }, - endTime: { - table: { - disable: true, - }, + isRunning: { + control: 'boolean', + description: + 'Whether the timer is actively counting. Toggle to start/stop.', }, status: { - control: { - type: 'select', - }, + control: { type: 'select' }, options: [ 'success', 'warning', @@ -55,11 +56,99 @@ InteractiveTimer.argTypes = { 'primary', 'secondary', ], + description: 'Visual status of the timer badge.', + }, + startTime: { + defaultValue: extendedDayjs().utc().valueOf(), + table: { disable: true }, + }, + endTime: { + table: { disable: true }, }, }; InteractiveTimer.parameters = { - actions: { - disabled: true, + actions: { disabled: true }, + docs: { + staticProps: { + startTime: 1737936000000, + }, + liveExample: `function Demo() { + const [isRunning, setIsRunning] = React.useState(true); + const [startTime] = React.useState(Date.now()); + return ( +
+ + +
+ ); +}`, + examples: [ + { + title: 'Status Variants', + code: `function StatusVariants() { + const [startTime] = React.useState(Date.now()); + return ( +
+ {['success', 'warning', 'danger', 'info', 'default', 'primary', 'secondary'].map(status => ( +
+ {status} + +
+ ))} +
+ ); +}`, + }, + { + title: 'Completed Timer', + code: `function CompletedTimer() { + const start = Date.now() - 5230; + const end = Date.now(); + return ( +
+ + Query completed in ~5.2 seconds +
+ ); +}`, + }, + { + title: 'Start and Stop', + code: `function StartStop() { + const [isRunning, setIsRunning] = React.useState(false); + const [startTime, setStartTime] = React.useState(null); + const handleToggle = () => { + if (!isRunning && !startTime) { + setStartTime(Date.now()); + } + setIsRunning(r => !r); + }; + return ( +
+ + +
+ ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx index 0e0fa1ef1b68..bb04a26b31ed 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tooltip/Tooltip.stories.tsx @@ -18,30 +18,13 @@ */ import { Button } from '../Button'; import { Tooltip } from '.'; -import { TooltipPlacement, TooltipProps } from './types'; +import { TooltipProps } from './types'; export default { title: 'Components/Tooltip', component: Tooltip, }; -const PLACEMENTS: TooltipPlacement[] = [ - 'bottom', - 'bottomLeft', - 'bottomRight', - 'left', - 'leftBottom', - 'leftTop', - 'right', - 'rightBottom', - 'rightTop', - 'top', - 'topLeft', - 'topRight', -]; - -const TRIGGERS = ['hover', 'focus', 'click', 'contextMenu']; - export const InteractiveTooltip = (args: TooltipProps) => ( @@ -55,16 +38,98 @@ InteractiveTooltip.args = { }; InteractiveTooltip.argTypes = { + title: { + control: { type: 'text' }, + description: 'Text or content shown in the tooltip.', + }, placement: { - defaultValue: 'top', control: { type: 'select' }, - options: PLACEMENTS, + options: [ + 'bottom', + 'bottomLeft', + 'bottomRight', + 'left', + 'leftBottom', + 'leftTop', + 'right', + 'rightBottom', + 'rightTop', + 'top', + 'topLeft', + 'topRight', + ], + description: 'Position of the tooltip relative to the trigger element.', }, trigger: { - defaultValue: 'hover', control: { type: 'select' }, - options: TRIGGERS, + options: ['hover', 'focus', 'click', 'contextMenu'], + description: 'How the tooltip is triggered.', + }, + mouseEnterDelay: { + control: { type: 'number' }, + description: 'Delay in seconds before showing the tooltip on hover.', + }, + mouseLeaveDelay: { + control: { type: 'number' }, + description: + 'Delay in seconds before hiding the tooltip after mouse leave.', + }, + color: { + control: { type: 'color' }, + description: 'Custom background color for the tooltip.', }, - color: { control: { type: 'color' } }, onVisibleChange: { action: 'onVisibleChange' }, }; + +InteractiveTooltip.parameters = { + docs: { + sampleChildren: [ + { + component: 'Button', + props: { children: 'Hover me' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Placements', + code: `function Placements() { + const placements = ['top', 'bottom', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight']; + return ( +
+ {placements.map(placement => ( + + + + ))} +
+ ); +}`, + }, + { + title: 'Trigger Types', + code: `function Triggers() { + return ( +
+ + + + + + + + + +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx index 84aa0d614db1..334524fd7c9e 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Tree/Tree.stories.tsx @@ -16,162 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import { Meta, StoryObj } from '@storybook/react'; +import { StoryObj } from '@storybook/react'; import { Icons } from '@superset-ui/core/components/Icons'; import Tree, { TreeProps, type TreeDataNode } from './index'; -const meta = { +export default { title: 'Components/Tree', component: Tree, - argTypes: { - autoExpandParent: { - control: 'boolean', - description: 'Whether to automatically expand a parent treeNode ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - checkable: { - control: 'boolean', - description: 'Add a Checkbox before the treeNodes', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - checkStrictly: { - control: 'boolean', - description: - 'Check treeNode precisely; parent treeNode and children treeNodes are not associated', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - defaultExpandAll: { - control: 'boolean', - description: 'Whether to expand all treeNodes by default', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - defaultExpandParent: { - control: 'boolean', - description: 'If auto expand parent treeNodes when init ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - disabled: { - control: 'boolean', - description: 'Whether disabled the tree', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - draggable: { - control: 'boolean', - description: - 'Specifies whether this Tree or the node is draggable. Use icon: false to disable drag handler icon', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - height: { - control: 'number', - description: - 'Config virtual scroll height. Will not support horizontal scroll when enable this', - table: { - category: 'Tree', - type: { summary: 'number' }, - }, - }, - multiple: { - control: 'boolean', - description: 'Allows selecting multiple treeNodes ', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - selectable: { - control: 'boolean', - description: 'Whether can be selected', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - showIcon: { - control: 'boolean', - description: - 'Controls whether to display the icon node, no default style', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - showLine: { - control: 'boolean', - description: 'Shows a connecting line', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - virtual: { - control: 'boolean', - description: 'Disable virtual scroll when set to false', - table: { - category: 'Tree', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - // Exclude unwanted properties - defaultExpandedKeys: { - table: { - disable: true, - }, - }, - defaultSelectedKeys: { - table: { - disable: true, - }, - }, - treeData: { - table: { - disable: true, - }, - }, - }, parameters: { docs: { description: { component: - 'The Tree component is used to display hierarchical data in a tree structure. It allows for features such as selection, expansion, and drag-and-drop functionality.', + 'The Tree component is used to display hierarchical data in a tree structure. ' + + 'It allows for features such as selection, expansion, and drag-and-drop functionality.', }, }, }, -} as Meta; - -export default meta; +}; const treeData: TreeDataNode[] = [ { @@ -237,3 +98,174 @@ export const TreeStory: Story = { }, render: (args: TreeProps) => , }; + +// Interactive story with primitive args for documentation +export const InteractiveTree = (args: TreeProps) => ( + +); + +InteractiveTree.args = { + checkable: false, + defaultExpandAll: false, + disabled: false, + draggable: false, + multiple: false, + selectable: true, + showIcon: false, + showLine: false, +}; + +InteractiveTree.argTypes = { + checkable: { + description: 'Add a Checkbox before the treeNodes', + control: { type: 'boolean' }, + }, + defaultExpandAll: { + description: 'Whether to expand all treeNodes by default', + control: { type: 'boolean' }, + }, + disabled: { + description: 'Whether disabled the tree', + control: { type: 'boolean' }, + }, + draggable: { + description: 'Specifies whether this Tree or the node is draggable', + control: { type: 'boolean' }, + }, + multiple: { + description: 'Allows selecting multiple treeNodes', + control: { type: 'boolean' }, + }, + selectable: { + description: 'Whether can be selected', + control: { type: 'boolean' }, + }, + showIcon: { + description: 'Controls whether to display the icon node', + control: { type: 'boolean' }, + }, + showLine: { + description: 'Shows a connecting line', + control: { type: 'boolean' }, + }, +}; + +InteractiveTree.parameters = { + docs: { + staticProps: { + treeData: [ + { + title: 'parent 1', + key: '0-0', + children: [ + { + title: 'parent 1-0', + key: '0-0-0', + children: [ + { title: 'leaf', key: '0-0-0-0' }, + { title: 'leaf', key: '0-0-0-1' }, + { title: 'leaf', key: '0-0-0-2' }, + ], + }, + { + title: 'parent 1-1', + key: '0-0-1', + children: [{ title: 'leaf', key: '0-0-1-0' }], + }, + { + title: 'parent 1-2', + key: '0-0-2', + children: [ + { title: 'leaf', key: '0-0-2-0' }, + { title: 'leaf', key: '0-0-2-1' }, + ], + }, + ], + }, + ], + defaultExpandedKeys: ['0-0', '0-0-0'], + }, + liveExample: `function Demo() { + const treeData = [ + { + title: 'Databases', + key: 'databases', + children: [ + { title: 'PostgreSQL', key: 'postgres' }, + { title: 'MySQL', key: 'mysql' }, + { title: 'SQLite', key: 'sqlite' }, + ], + }, + { + title: 'Charts', + key: 'charts', + children: [ + { title: 'Bar Chart', key: 'bar' }, + { title: 'Line Chart', key: 'line' }, + { title: 'Pie Chart', key: 'pie' }, + ], + }, + ]; + return ; +}`, + examples: [ + { + title: 'Checkable Tree', + code: `function CheckableTree() { + const [checkedKeys, setCheckedKeys] = React.useState(['postgres']); + const treeData = [ + { + title: 'Databases', + key: 'databases', + children: [ + { title: 'PostgreSQL', key: 'postgres' }, + { title: 'MySQL', key: 'mysql' }, + ], + }, + { + title: 'Charts', + key: 'charts', + children: [ + { title: 'Bar Chart', key: 'bar' }, + { title: 'Line Chart', key: 'line' }, + ], + }, + ]; + return ( + + ); +}`, + }, + { + title: 'With Lines and Icons', + code: `function LinesAndIcons() { + const treeData = [ + { + title: 'Dashboards', + key: 'dashboards', + children: [ + { title: 'Sales Dashboard', key: 'sales' }, + { title: 'Marketing Dashboard', key: 'marketing' }, + ], + }, + { + title: 'Reports', + key: 'reports', + children: [ + { title: 'Weekly Report', key: 'weekly' }, + { title: 'Monthly Report', key: 'monthly' }, + ], + }, + ]; + return ; +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx index 7c0f6e33e0ac..da6a9cd778d4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TreeSelect/TreeSelect.stories.tsx @@ -22,199 +22,6 @@ import { TreeSelect, type TreeSelectProps } from '.'; export default { title: 'Components/TreeSelect', component: TreeSelect, - argTypes: { - allowClear: { - control: { type: 'boolean' }, - description: 'Whether to allow clearing the selected value.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - autoClearSearchValue: { - control: { type: 'boolean' }, - description: 'Whether to clear the search value automatically.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - disabled: { - control: { type: 'boolean' }, - description: 'Whether the component is disabled.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - labelInValue: { - control: { type: 'boolean' }, - description: 'Whether to use label in value.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - }, - }, - listHeight: { - control: { type: 'number' }, - description: 'Height of the dropdown list.', - defaultValue: 256, - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - defaultValue: { summary: '256' }, - }, - }, - maxTagCount: { - control: { type: 'number' }, - description: 'Maximum number of tags to display.', - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - }, - }, - maxTagTextLength: { - control: { type: 'number' }, - description: 'Maximum length of tag text.', - defaultValue: 20, - table: { - category: 'TreeSelect', - type: { summary: 'number' }, - }, - }, - multiple: { - control: { type: 'boolean' }, - description: 'Whether to allow multiple selections.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - placeholder: { - control: { type: 'text' }, - description: 'Placeholder text for the input field.', - defaultValue: 'Please select', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - placement: { - control: { type: 'select' }, - options: ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'], - description: 'Placement of the dropdown menu.', - defaultValue: 'bottomLeft', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - defaultValue: { summary: 'bottomLeft' }, - }, - }, - showSearch: { - control: { type: 'boolean' }, - description: 'Whether to show the search input.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - }, - }, - size: { - control: { type: 'select' }, - options: ['large', 'middle', 'small'], - description: 'Size of the component.', - defaultValue: 'middle', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - status: { - control: { type: 'select' }, - options: ['error', 'warning'], - description: 'Status of the component.', - defaultValue: 'error', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - }, - }, - treeCheckable: { - control: { type: 'boolean' }, - description: 'Whether to show checkable tree nodes.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeDefaultExpandAll: { - control: { type: 'boolean' }, - description: 'Whether to expand all tree nodes by default.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeIcon: { - control: { type: 'boolean' }, - description: 'Whether to show tree icons.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - treeLine: { - control: { type: 'boolean' }, - description: 'Whether to show tree lines.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - variant: { - control: { type: 'select' }, - options: ['outlined', 'borderless', 'filled', 'underlined'], - description: 'Variant of the component.', - defaultValue: 'outlined', - table: { - category: 'TreeSelect', - type: { summary: 'string' }, - defaultValue: { summary: 'outlined' }, - }, - }, - virtual: { - control: { type: 'boolean' }, - description: 'Whether to use virtual scrolling.', - defaultValue: false, - table: { - category: 'TreeSelect', - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - treeData: { - table: { - disable: true, - }, - }, - }, parameters: { docs: { description: { @@ -254,6 +61,199 @@ const treeData = [ }, ]; +export const InteractiveTreeSelect = (args: TreeSelectProps) => ( +
+ +
+); + +InteractiveTreeSelect.args = { + allowClear: true, + disabled: false, + multiple: false, + placeholder: 'Please select', + showSearch: true, + size: 'middle', + treeCheckable: false, + treeDefaultExpandAll: true, + treeLine: false, + variant: 'outlined', +}; + +InteractiveTreeSelect.argTypes = { + allowClear: { + control: { type: 'boolean' }, + description: 'Whether to allow clearing the selected value.', + }, + disabled: { + control: { type: 'boolean' }, + description: 'Whether the component is disabled.', + }, + multiple: { + control: { type: 'boolean' }, + description: 'Whether to allow multiple selections.', + }, + placeholder: { + control: { type: 'text' }, + description: 'Placeholder text for the input field.', + }, + showSearch: { + control: { type: 'boolean' }, + description: 'Whether to show the search input.', + }, + size: { + control: { type: 'select' }, + options: ['large', 'middle', 'small'], + description: 'Size of the component.', + }, + treeCheckable: { + control: { type: 'boolean' }, + description: 'Whether to show checkable tree nodes.', + }, + treeDefaultExpandAll: { + control: { type: 'boolean' }, + description: 'Whether to expand all tree nodes by default.', + }, + treeLine: { + control: { type: 'boolean' }, + description: 'Whether to show tree lines.', + }, + variant: { + control: { type: 'select' }, + options: ['outlined', 'borderless', 'filled'], + description: 'Variant of the component.', + }, + treeData: { + table: { disable: true }, + }, +}; + +InteractiveTreeSelect.parameters = { + docs: { + staticProps: { + treeData: [ + { + title: 'Node1', + value: '0-0', + children: [ + { title: 'Child Node1', value: '0-0-0' }, + { title: 'Child Node2', value: '0-0-1' }, + ], + }, + { + title: 'Node2', + value: '0-1', + children: [{ title: 'Child Node3', value: '0-1-0' }], + }, + ], + }, + liveExample: `function Demo() { + const [value, setValue] = React.useState(undefined); + const treeData = [ + { + title: 'Databases', + value: 'databases', + children: [ + { title: 'PostgreSQL', value: 'postgres' }, + { title: 'MySQL', value: 'mysql' }, + ], + }, + { + title: 'Charts', + value: 'charts', + children: [ + { title: 'Bar Chart', value: 'bar' }, + { title: 'Line Chart', value: 'line' }, + ], + }, + ]; + return ( + + ); +}`, + examples: [ + { + title: 'Multiple Selection with Checkboxes', + code: `function MultiSelectTree() { + const [value, setValue] = React.useState([]); + const treeData = [ + { + title: 'Databases', + value: 'databases', + children: [ + { title: 'PostgreSQL', value: 'postgres' }, + { title: 'MySQL', value: 'mysql' }, + { title: 'SQLite', value: 'sqlite' }, + ], + }, + { + title: 'File Formats', + value: 'formats', + children: [ + { title: 'CSV', value: 'csv' }, + { title: 'Excel', value: 'excel' }, + ], + }, + ]; + return ( + + ); +}`, + }, + { + title: 'With Tree Lines', + code: `function TreeLinesDemo() { + const treeData = [ + { + title: 'Dashboards', + value: 'dashboards', + children: [ + { title: 'Sales Dashboard', value: 'sales' }, + { title: 'Marketing Dashboard', value: 'marketing' }, + ], + }, + { + title: 'Charts', + value: 'charts', + children: [ + { title: 'Revenue Chart', value: 'revenue' }, + { title: 'User Growth', value: 'growth' }, + ], + }, + ]; + return ( + + ); +}`, + }, + ], + }, +}; + +// Keep original for backwards compatibility export const Default: Story = { args: { treeData, diff --git a/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx index 16700be76f09..902119fd2e4d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Typography/Typography.stories.tsx @@ -28,146 +28,171 @@ export default { Paragraph: Typography.Paragraph, Link: Typography.Link, }, + parameters: { + docs: { + description: { + component: + 'Typography is a component for displaying text with various styles and formats. It includes subcomponents like Title, Paragraph, and Link.', + }, + }, + }, +} as Meta< + typeof Typography.Text & + typeof Typography.Paragraph & + typeof Typography.Link & + typeof Typography.Title +>; + +type TextStory = StoryObj; + +export const InteractiveTypography: TextStory = { + args: { + children: 'Sample Text', + code: false, + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + keyboard: false, + mark: false, + italic: false, + underline: false, + strong: false, + type: undefined, + }, argTypes: { + children: { + control: 'text', + description: 'The text content.', + }, code: { control: 'boolean', - description: 'Code style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Code style.', }, copyable: { control: 'boolean', - description: 'Whether to be copyable, customize it via setting an object', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Whether the text is copyable.', }, delete: { control: 'boolean', - description: 'Deleted line style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Deleted line style.', }, disabled: { control: 'boolean', - description: 'Disabled content', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - editable: { - control: 'boolean', - description: 'Whether to be editable, customize it via setting an object', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Disabled content.', }, ellipsis: { control: 'boolean', - description: - 'Display ellipsis when text overflows, can not configure expandable、rows and onExpand by using object. Diff with Typography.Paragraph, Text do not have 100% width style which means it will fix width on the first ellipsis. If you want to have responsive ellipsis, please set width manually', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Display ellipsis when text overflows.', }, keyboard: { control: 'boolean', - description: 'Keyboard style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Keyboard style.', }, mark: { control: 'boolean', - description: 'Marked style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Marked/highlighted style.', }, italic: { control: 'boolean', - description: 'Italic style', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - type: { - control: 'select', - description: 'Text type', - options: ['secondary', 'success', 'warning', 'danger'], - table: { - category: 'Typography.Text', - type: { summary: 'string' }, - }, + description: 'Italic style.', }, underline: { control: 'boolean', - description: 'Underlined style ', - table: { - category: 'Typography.Text', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - level: { - control: { - type: 'number', - min: 1, - max: 5, - }, - description: 'Set content importance. Match with h1, h2, h3, h4, h5', - table: { - category: 'Typography.Title', - type: { summary: 'number' }, - }, + description: 'Underlined style.', }, strong: { control: 'boolean', - description: 'Bold style', - table: { - category: 'Typography.Paragraph', - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, + description: 'Bold style.', }, - }, - parameters: { - docs: { - description: { - component: - 'Typography is a component for displaying text with various styles and formats. It includes subcomponents like Title, Paragraph, and Link.', - }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Text type for semantic coloring.', }, }, -} as Meta< - typeof Typography.Text & - typeof Typography.Paragraph & - typeof Typography.Link & - typeof Typography.Title ->; + render: args => , +}; -type TextStory = StoryObj; +InteractiveTypography.parameters = { + docs: { + renderComponent: 'Typography.Text', + description: { + story: 'Text component with various styling options.', + }, + liveExample: `function Demo() { + return ( +
+ Default Text +
+ Secondary +
+ Success +
+ Warning +
+ Danger +
+ Code +
+ Keyboard +
+ Marked +
+ Underline +
+ Deleted +
+ Strong +
+ Italic +
+ ); +}`, + examples: [ + { + title: 'All Subcomponents', + code: `function AllSubcomponents() { + return ( +
+ Typography Components + + The Typography component includes several subcomponents for different text needs. + Use Title for headings, + Text for inline text styling, + and Paragraph for block content. + + + Learn more about Apache Superset + +
+ ); +}`, + }, + { + title: 'Text Styling Options', + code: `function TextStyles() { + return ( +
+ Code style + Keyboard style + Highlighted text + Underlined text + Deleted text + Bold text + Italic text + Success type + Warning type + Danger type +
+ ); +}`, + }, + ], + }, +}; +// Keep original for backwards compatibility export const TextStory: TextStory = { args: { children: 'Default Text', @@ -177,14 +202,169 @@ export const TextStory: TextStory = { type TitleStory = StoryObj; +export const InteractiveTitle: TitleStory = { + args: { + children: 'Sample Title', + level: 1, + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The title content.', + }, + level: { + control: { type: 'number', min: 1, max: 5 }, + description: 'Set content importance (h1-h5).', + }, + copyable: { + control: 'boolean', + description: 'Whether the title is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled content.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Title type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Title component with heading levels h1-h5.', + }, + liveExample: `function Demo() { + return ( +
+ h1. Heading + h2. Heading + h3. Heading + h4. Heading + h5. Heading +
+ ); +}`, + }, + }, +}; + export const TitleStory: TitleStory = { args: { children: 'Default Title', }, render: args => , }; + type ParagraphStory = StoryObj; +export const InteractiveParagraph: ParagraphStory = { + args: { + children: + 'This is a paragraph of text. Paragraphs are block-level elements that support various text styling options.', + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + strong: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The paragraph content.', + }, + copyable: { + control: 'boolean', + description: 'Whether the paragraph is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled content.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + strong: { + control: 'boolean', + description: 'Bold style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Paragraph type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Paragraph component for block-level text content.', + }, + liveExample: `function Demo() { + return ( + + This is a paragraph. Paragraphs are used for block-level text content. + They support features like bold, + italic, and + code styling. + + ); +}`, + }, + }, +}; + export const ParagraphStory: ParagraphStory = { args: { children: 'Default Paragraph', @@ -194,6 +374,92 @@ export const ParagraphStory: ParagraphStory = { type LinkStory = StoryObj; +export const InteractiveLink: LinkStory = { + args: { + children: 'Click here', + href: 'https://superset.apache.org', + target: '_blank', + copyable: false, + delete: false, + disabled: false, + ellipsis: false, + mark: false, + strong: false, + italic: false, + underline: false, + type: undefined, + }, + argTypes: { + children: { + control: 'text', + description: 'The link text.', + }, + href: { + control: 'text', + description: 'The URL the link points to.', + }, + target: { + control: 'select', + options: ['_blank', '_self', '_parent', '_top'], + description: 'Where to open the linked document.', + }, + copyable: { + control: 'boolean', + description: 'Whether the link is copyable.', + }, + delete: { + control: 'boolean', + description: 'Deleted line style.', + }, + disabled: { + control: 'boolean', + description: 'Disabled link.', + }, + ellipsis: { + control: 'boolean', + description: 'Display ellipsis when text overflows.', + }, + mark: { + control: 'boolean', + description: 'Marked/highlighted style.', + }, + strong: { + control: 'boolean', + description: 'Bold style.', + }, + italic: { + control: 'boolean', + description: 'Italic style.', + }, + underline: { + control: 'boolean', + description: 'Underlined style.', + }, + type: { + control: 'select', + options: [undefined, 'secondary', 'success', 'warning', 'danger'], + description: 'Link type for semantic coloring.', + }, + }, + render: args => , + parameters: { + docs: { + description: { + story: 'Link component for hyperlinks with text styling options.', + }, + liveExample: `function Demo() { + return ( +
+ + Apache Superset + +
+ ); +}`, + }, + }, +}; + export const LinkStory: LinkStory = { args: { children: 'Default Link', diff --git a/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx index 4742c4da8feb..73054a401acc 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/UnsavedChangesModal/UnsavedChangesModal.stories.tsx @@ -33,15 +33,71 @@ export const InteractiveUnsavedChangesModal = ( ); InteractiveUnsavedChangesModal.args = { - showModal: true, - onHide: () => {}, - handleSave: () => {}, - onConfirmNavigation: () => {}, + showModal: false, title: 'Unsaved Changes', }; InteractiveUnsavedChangesModal.argTypes = { + showModal: { + control: { type: 'boolean' }, + description: 'Whether the modal is visible.', + }, + title: { + control: { type: 'text' }, + description: 'Title text displayed in the modal header.', + }, onHide: { action: 'onHide' }, handleSave: { action: 'handleSave' }, onConfirmNavigation: { action: 'onConfirmNavigation' }, }; + +InteractiveUnsavedChangesModal.parameters = { + docs: { + triggerProp: 'showModal', + onHideProp: 'onHide,handleSave,onConfirmNavigation', + liveExample: `function Demo() { + const [show, setShow] = React.useState(false); + return ( +
+ + setShow(false)} + handleSave={() => { alert('Saved!'); setShow(false); }} + onConfirmNavigation={() => { alert('Discarded changes'); setShow(false); }} + title="Unsaved Changes" + > + If you don't save, changes will be lost. + +
+ ); +}`, + examples: [ + { + title: 'Custom Title', + code: `function CustomTitle() { + const [show, setShow] = React.useState(false); + return ( +
+ + setShow(false)} + handleSave={() => setShow(false)} + onConfirmNavigation={() => setShow(false)} + title="You have unsaved dashboard changes" + > + Your dashboard layout and filter changes have not been saved. + Do you want to save before leaving? + +
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx index 94b524958de3..d68b87ecf18d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Upload/Upload.stories.tsx @@ -16,104 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; import { Icons } from '../Icons'; import { Button } from '../Button'; import { Upload } from '.'; -const meta: Meta = { +export default { title: 'Components/Upload', component: Upload, - argTypes: { - accept: { - control: false, - description: 'File types that can be accepted', - defaultValue: undefined, - type: 'string', - }, - action: { - control: 'text', - description: 'Uploading URL', - defaultValue: undefined, - type: 'string', - }, - name: { - control: false, - description: 'The name of uploading file', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'file' }, - }, - }, - multiple: { - control: 'boolean', - description: 'Support multiple file selection', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - disabled: { - control: 'boolean', - description: 'Disable upload button', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - listType: { - control: 'select', - options: ['text', 'picture', 'picture-card', 'picture-circle'], - description: 'Built-in stylesheets for file list display', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'text' }, - }, - }, - showUploadList: { - control: 'boolean', - description: - 'Whether to show default upload list, could be an object to specify extra, showPreviewIcon, showRemoveIcon, showDownloadIcon, removeIcon and downloadIcon individually upload list display', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - method: { - control: false, - description: 'The HTTP method of upload request', - table: { - type: { summary: 'string' }, - defaultValue: { summary: 'post' }, - }, - }, - withCredentials: { - control: false, - description: 'Send cookies with ajax upload', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'false' }, - }, - }, - openFileDialogOnClick: { - control: 'boolean', - description: 'Click open file dialog', - table: { - type: { summary: 'boolean' }, - defaultValue: { summary: 'true' }, - }, - }, - progress: { - control: false, - description: 'Custom progress bar', - table: { - type: { summary: 'object' }, + parameters: { + docs: { + description: { + component: + 'Upload component for file selection and uploading. ' + + 'Supports drag-and-drop, multiple files, and different list display styles.', }, }, }, }; -export default meta; type Story = StoryObj; export const Default: Story = { @@ -130,3 +51,74 @@ export const Default: Story = { ), }; + +export const InteractiveUpload = (args: any) => ; + +InteractiveUpload.args = { + multiple: false, + disabled: false, + listType: 'text', + showUploadList: true, +}; + +InteractiveUpload.argTypes = { + multiple: { + control: { type: 'boolean' }, + description: 'Support multiple file selection.', + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disable the upload button.', + }, + listType: { + control: { type: 'select' }, + options: ['text', 'picture', 'picture-card', 'picture-circle'], + description: 'Built-in style for the file list display.', + }, + showUploadList: { + control: { type: 'boolean' }, + description: 'Whether to show the upload file list.', + }, +}; + +InteractiveUpload.parameters = { + docs: { + sampleChildren: [ + { + component: 'Button', + props: { children: 'Click to Upload' }, + }, + ], + liveExample: `function Demo() { + return ( + + + + ); +}`, + examples: [ + { + title: 'Picture Card Style', + code: `function PictureCard() { + return ( + + + Upload + + ); +}`, + }, + { + title: 'Drag and Drop', + code: `function DragDrop() { + return ( + +

+

+

Click or drag file to this area to upload

+

Support for single or bulk upload.

+
+ ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts index 96d17aa65471..eecfdf90f882 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts @@ -144,15 +144,31 @@ export { type ListViewCardProps, } from './ListViewCard'; export { Loading, type LoadingProps } from './Loading'; +export { default as MetadataBar, type MetadataBarProps } from './MetadataBar'; export { Progress, type ProgressProps } from './Progress'; +export { default as ProgressBar, type ProgressBarProps } from './ProgressBar'; export { Pagination, type PaginationProps } from './Pagination'; export { Skeleton, type SkeletonProps } from './Skeleton'; +export { + default as Slider, + type SliderSingleProps, + type SliderRangeProps, +} from './Slider'; export { Switch, type SwitchProps } from './Switch'; +export { + default as Tabs, + EditableTabs, + LineEditableTabs, + type TabsProps, +} from './Tabs'; + +export { default as Tree, type TreeProps, type TreeDataNode } from './Tree'; + export { TreeSelect, type TreeSelectProps } from './TreeSelect'; export { diff --git a/superset-frontend/packages/superset-ui-demo/README.md b/superset-frontend/packages/superset-ui-demo/README.md index b1a9be3a1fdd..c25c4706285a 100644 --- a/superset-frontend/packages/superset-ui-demo/README.md +++ b/superset-frontend/packages/superset-ui-demo/README.md @@ -17,55 +17,12 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/demo +# @superset-ui/demo -[![Version](https://img.shields.io/github/package-json/v/apache/superset?filename=superset-frontend%2Fpackages%2Fsuperset-ui-demo%2Fpackage.json&style=flat)](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-ui-demo/package.json) +Storybook for `@superset-ui` packages. -Storybook of `@superset-ui` packages. See it live at -[apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui) +**See it live:** [apache-superset.github.io/superset-ui](https://apache-superset.github.io/superset-ui) -### Development - -#### Run storybook - -To view the storybook locally, you should first run `npm ci && npm run bootstrap` in the -`@superset-ui` monorepo root directory, which will install all dependencies for this package and -sym-link any `@superset-ui` packages to your local system. - -After that run `npm run storybook` which will open up a dev server at http://localhost:9001. - -#### Adding new stories - -###### Existing package - -If stories already exist for the package you are adding, simply extend the `examples` already -exported for that package in the `storybook/stories//index.js` file. - -###### New package - -If you are creating stories for a package that doesn't yet have any stories, follow these steps: - -1. Add any new package dependencies (including any `@superset-ui/*` packages) via - `npm install `. - -2. Create a new folder that mirrors the package name - - > e.g., `mkdir storybook/stories/superset-ui-color/` - -3. Add an `index.js` file to that folder with a default export with the following shape: - -> you can use the `|` separator within the `storyPath` string to denote _nested_ stories e.g., -> `storyPath: '@superset-ui/package|Nested i|Nested ii'` - -```javascript - default export { - examples: [ - { - storyPath: , - storyName: , - renderStory: () => node, - }, - ... - ] - }; -``` +For documentation on running Storybook locally and adding stories, see the +[Storybook documentation](https://superset.apache.org/docs/developer_portal/testing/storybook) +in the Developer Portal.