mirror of
https://github.com/apache/superset.git
synced 2026-08-05 13:32:41 +00:00
feat(build): migrate from Prettier to Oxfmt for performant code formatting (#42434)
This commit is contained in:
@@ -39,7 +39,9 @@ interface FAQSchemaProps {
|
||||
* { question: "How do I install it?", answer: "You can install via..." }
|
||||
* ]} />
|
||||
*/
|
||||
export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null {
|
||||
export default function FAQSchema({
|
||||
faqs,
|
||||
}: FAQSchemaProps): JSX.Element | null {
|
||||
// FAQPage schema requires a non-empty mainEntity array per schema.org specs
|
||||
if (!faqs || faqs.length === 0) {
|
||||
return null;
|
||||
@@ -48,7 +50,7 @@ export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
mainEntity: faqs.map(faq => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
|
||||
@@ -64,9 +64,7 @@ const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
|
||||
text-decoration: none;
|
||||
min-width: 0;
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `padding: 10px 10px;`
|
||||
: `padding: 7px 8px;`}
|
||||
$variant === 'hero' ? `padding: 10px 10px;` : `padding: 7px 8px;`}
|
||||
}
|
||||
|
||||
.split-main:hover {
|
||||
@@ -79,9 +77,7 @@ const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
|
||||
align-self: stretch;
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `margin: 8px 0;`
|
||||
: `margin: 6px 0;`}
|
||||
$variant === 'hero' ? `margin: 8px 0;` : `margin: 6px 0;`}
|
||||
}
|
||||
|
||||
.split-dropdown-trigger {
|
||||
|
||||
@@ -72,7 +72,7 @@ function getProviders() {
|
||||
// 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) => {
|
||||
const getPopupContainer = triggerNode => {
|
||||
// Find the closest .storybook-example container
|
||||
const container = triggerNode?.closest?.('.storybook-example');
|
||||
return container || document.body;
|
||||
@@ -190,7 +190,11 @@ 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] !== '') {
|
||||
if (
|
||||
props[propName] !== undefined &&
|
||||
props[propName] !== null &&
|
||||
props[propName] !== ''
|
||||
) {
|
||||
const { [propName]: childContent, ...restProps } = props;
|
||||
return { children: childContent, restProps };
|
||||
}
|
||||
@@ -220,7 +224,11 @@ function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
|
||||
return <ChildComponent key={i} {...item.props} />;
|
||||
}
|
||||
// Fallback if component not found
|
||||
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
|
||||
return (
|
||||
<div key={i}>
|
||||
{item.props?.children || `Unknown: ${item.component}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Simple string
|
||||
return (
|
||||
@@ -252,7 +260,16 @@ function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
|
||||
// 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 }) {
|
||||
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);
|
||||
@@ -274,7 +291,7 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
: extractChildren(stateProps);
|
||||
// Filter out undefined values so they don't override component defaults
|
||||
const filteredProps = Object.fromEntries(
|
||||
Object.entries(restProps).filter(([, v]) => v !== undefined)
|
||||
Object.entries(restProps).filter(([, v]) => v !== undefined),
|
||||
);
|
||||
|
||||
// Resolve any prop values that are component descriptors
|
||||
@@ -283,7 +300,12 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
// 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) {
|
||||
if (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
!Array.isArray(value) &&
|
||||
value.component
|
||||
) {
|
||||
const PropComponent = resolveComponent(value.component);
|
||||
if (PropComponent) {
|
||||
filteredProps[key] = <PropComponent {...value.props} />;
|
||||
@@ -295,10 +317,18 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
const resolved = { ...item };
|
||||
Object.keys(resolved).forEach(field => {
|
||||
const fieldValue = resolved[field];
|
||||
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) {
|
||||
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 });
|
||||
resolved[field] = React.createElement(FieldComponent, {
|
||||
key: `${key}-${idx}`,
|
||||
...fieldValue.props,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -312,14 +342,16 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
// 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) =>
|
||||
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;
|
||||
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
|
||||
@@ -356,7 +388,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
Open {component}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
<Component {...filteredProps} {...triggerProps}>{children}</Component>
|
||||
<Component {...filteredProps} {...triggerProps}>
|
||||
{children}
|
||||
</Component>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
@@ -384,7 +418,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
{control.type === 'select' ? (
|
||||
<select
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value || undefined)}
|
||||
onChange={e =>
|
||||
updateProp(control.name, e.target.value || undefined)
|
||||
}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
>
|
||||
<option value="">— None —</option>
|
||||
@@ -394,19 +430,28 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : control.type === 'inline-radio' || control.type === 'radio' ? (
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
) : control.type === 'inline-radio' ||
|
||||
control.type === 'radio' ? (
|
||||
<div
|
||||
style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}
|
||||
>
|
||||
{control.options?.map(option => (
|
||||
<label
|
||||
key={option}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={control.name}
|
||||
value={option}
|
||||
checked={stateProps[control.name] === option}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
onChange={e =>
|
||||
updateProp(control.name, e.target.value)
|
||||
}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
@@ -422,7 +467,9 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
<input
|
||||
type="number"
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, Number(e.target.value))}
|
||||
onChange={e =>
|
||||
updateProp(control.name, Number(e.target.value))
|
||||
}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
) : control.type === 'color' ? (
|
||||
@@ -457,7 +504,16 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s
|
||||
// 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 }) {
|
||||
export function StoryWithControls({
|
||||
component: Component,
|
||||
renderComponent,
|
||||
props = {},
|
||||
controls = [],
|
||||
sampleChildren,
|
||||
sampleChildrenStyle,
|
||||
triggerProp,
|
||||
onHideProp,
|
||||
}) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
@@ -477,7 +533,13 @@ export function StoryWithControls({ component: Component, renderComponent, props
|
||||
}
|
||||
|
||||
// Inner component for ComponentGallery (browser-only)
|
||||
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
|
||||
function ComponentGalleryInner({
|
||||
component,
|
||||
sizes,
|
||||
styles,
|
||||
sizeProp,
|
||||
styleProp,
|
||||
}) {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
|
||||
@@ -495,7 +557,14 @@ function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }
|
||||
{sizes.map(size => (
|
||||
<div key={size} style={{ marginBottom: 40 }}>
|
||||
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{styles.map(style => (
|
||||
<Component
|
||||
key={`${style}_${size}`}
|
||||
@@ -513,7 +582,13 @@ function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }
|
||||
}
|
||||
|
||||
// A component to display a gallery of all variants (sizes x styles)
|
||||
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
|
||||
export function ComponentGallery({
|
||||
component,
|
||||
sizes = [],
|
||||
styles = [],
|
||||
sizeProp = 'size',
|
||||
styleProp = 'variant',
|
||||
}) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
|
||||
@@ -18,7 +18,17 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, Tag, Input, Select, Tooltip } from 'antd';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Input,
|
||||
Select,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CheckCircleOutlined,
|
||||
@@ -37,7 +47,7 @@ interface DatabaseIndexProps {
|
||||
// Type for table entries (includes both regular DBs and compatible DBs)
|
||||
interface TableEntry {
|
||||
name: string;
|
||||
categories: string[]; // Multiple categories supported
|
||||
categories: string[]; // Multiple categories supported
|
||||
score: number;
|
||||
max_score: number;
|
||||
timeGrainCount: number;
|
||||
@@ -66,20 +76,20 @@ interface TableEntry {
|
||||
|
||||
// Map category constant names to display names
|
||||
const CATEGORY_DISPLAY_NAMES: Record<string, string> = {
|
||||
'CLOUD_AWS': 'Cloud - AWS',
|
||||
'CLOUD_GCP': 'Cloud - Google',
|
||||
'CLOUD_AZURE': 'Cloud - Azure',
|
||||
'CLOUD_DATA_WAREHOUSES': 'Cloud Data Warehouses',
|
||||
'APACHE_PROJECTS': 'Apache Projects',
|
||||
'TRADITIONAL_RDBMS': 'Traditional RDBMS',
|
||||
'ANALYTICAL_DATABASES': 'Analytical Databases',
|
||||
'SEARCH_NOSQL': 'Search & NoSQL',
|
||||
'QUERY_ENGINES': 'Query Engines',
|
||||
'TIME_SERIES': 'Time Series Databases',
|
||||
'OTHER': 'Other Databases',
|
||||
'OPEN_SOURCE': 'Open Source',
|
||||
'HOSTED_OPEN_SOURCE': 'Hosted Open Source',
|
||||
'PROPRIETARY': 'Proprietary',
|
||||
CLOUD_AWS: 'Cloud - AWS',
|
||||
CLOUD_GCP: 'Cloud - Google',
|
||||
CLOUD_AZURE: 'Cloud - Azure',
|
||||
CLOUD_DATA_WAREHOUSES: 'Cloud Data Warehouses',
|
||||
APACHE_PROJECTS: 'Apache Projects',
|
||||
TRADITIONAL_RDBMS: 'Traditional RDBMS',
|
||||
ANALYTICAL_DATABASES: 'Analytical Databases',
|
||||
SEARCH_NOSQL: 'Search & NoSQL',
|
||||
QUERY_ENGINES: 'Query Engines',
|
||||
TIME_SERIES: 'Time Series Databases',
|
||||
OTHER: 'Other Databases',
|
||||
OPEN_SOURCE: 'Open Source',
|
||||
HOSTED_OPEN_SOURCE: 'Hosted Open Source',
|
||||
PROPRIETARY: 'Proprietary',
|
||||
};
|
||||
|
||||
// Category colors for visual distinction
|
||||
@@ -98,7 +108,7 @@ const CATEGORY_COLORS: Record<string, string> = {
|
||||
// Licensing categories
|
||||
'Open Source': 'geekblue',
|
||||
'Hosted Open Source': 'cyan',
|
||||
'Proprietary': 'default',
|
||||
Proprietary: 'default',
|
||||
};
|
||||
|
||||
// Convert category constant to display name
|
||||
@@ -110,7 +120,7 @@ function getCategoryDisplayName(cat: string): string {
|
||||
// Falls back to name-based inference for compatible databases without categories
|
||||
function getCategories(
|
||||
name: string,
|
||||
documentationCategories?: string[]
|
||||
documentationCategories?: string[],
|
||||
): string[] {
|
||||
// Prefer categories from documentation metadata (computed by Python)
|
||||
if (documentationCategories && documentationCategories.length > 0) {
|
||||
@@ -221,10 +231,11 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
categories: getCategories(name, db.documentation?.categories),
|
||||
timeGrainCount: countTimeGrains(db),
|
||||
hasDrivers: (db.documentation?.drivers?.length ?? 0) > 0,
|
||||
hasAuthMethods: (db.documentation?.authentication_methods?.length ?? 0) > 0,
|
||||
hasAuthMethods:
|
||||
(db.documentation?.authentication_methods?.length ?? 0) > 0,
|
||||
hasConnectionString: Boolean(
|
||||
db.documentation?.connection_string ||
|
||||
(db.documentation?.drivers?.length ?? 0) > 0
|
||||
(db.documentation?.drivers?.length ?? 0) > 0,
|
||||
),
|
||||
hasCustomErrors: (db.documentation?.custom_errors?.length ?? 0) > 0,
|
||||
customErrorCount: db.documentation?.custom_errors?.length ?? 0,
|
||||
@@ -233,10 +244,10 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
|
||||
// Add compatible databases from this database's documentation
|
||||
const compatibleDbs = db.documentation?.compatible_databases ?? [];
|
||||
compatibleDbs.forEach((compat) => {
|
||||
compatibleDbs.forEach(compat => {
|
||||
// Check if this compatible DB already exists as a main entry
|
||||
const existsAsMain = Object.keys(databases).some(
|
||||
(dbName) => dbName.toLowerCase() === compat.name.toLowerCase()
|
||||
dbName => dbName.toLowerCase() === compat.name.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!existsAsMain) {
|
||||
@@ -277,14 +288,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
// Filter and sort databases
|
||||
const filteredDatabases = useMemo(() => {
|
||||
return databaseList
|
||||
.filter((db) => {
|
||||
.filter(db => {
|
||||
const matchesSearch =
|
||||
!searchText ||
|
||||
db.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
db.documentation?.description
|
||||
?.toLowerCase()
|
||||
.includes(searchText.toLowerCase());
|
||||
const matchesCategory = !categoryFilter || db.categories.includes(categoryFilter);
|
||||
const matchesCategory =
|
||||
!categoryFilter || db.categories.includes(categoryFilter);
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score);
|
||||
@@ -293,9 +305,9 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
// Get unique categories and counts for filter
|
||||
const { categories, categoryCounts } = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
databaseList.forEach((db) => {
|
||||
databaseList.forEach(db => {
|
||||
// Count each category the database belongs to
|
||||
db.categories.forEach((cat) => {
|
||||
db.categories.forEach(cat => {
|
||||
counts[cat] = (counts[cat] || 0) + 1;
|
||||
});
|
||||
});
|
||||
@@ -314,12 +326,17 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
sorter: (a: TableEntry, b: TableEntry) => a.name.localeCompare(b.name),
|
||||
render: (name: string, record: TableEntry) => {
|
||||
// Convert name to URL slug
|
||||
const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const toSlug = (n: string) =>
|
||||
n
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
// Link to parent for compatible DBs, otherwise to own page
|
||||
const linkTarget = record.isCompatible && record.compatibleWith
|
||||
? `/docs/databases/supported/${toSlug(record.compatibleWith)}`
|
||||
: `/docs/databases/supported/${toSlug(name)}`;
|
||||
const linkTarget =
|
||||
record.isCompatible && record.compatibleWith
|
||||
? `/docs/databases/supported/${toSlug(record.compatibleWith)}`
|
||||
: `/docs/databases/supported/${toSlug(name)}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -337,7 +354,9 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
)}
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.documentation?.description?.slice(0, 80)}
|
||||
{(record.documentation?.description?.length ?? 0) > 80 ? '...' : ''}
|
||||
{(record.documentation?.description?.length ?? 0) > 80
|
||||
? '...'
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -348,13 +367,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
dataIndex: 'categories',
|
||||
key: 'categories',
|
||||
width: 220,
|
||||
filters: categories.map((cat) => ({ text: cat, value: cat })),
|
||||
filters: categories.map(cat => ({ text: cat, value: cat })),
|
||||
onFilter: (value: React.Key | boolean, record: TableEntry) =>
|
||||
record.categories.includes(value as string),
|
||||
render: (cats: string[]) => (
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{cats.map((cat) => (
|
||||
<Tag key={cat} color={CATEGORY_COLORS[cat] || 'default'}>{cat}</Tag>
|
||||
{cats.map(cat => (
|
||||
<Tag key={cat} color={CATEGORY_COLORS[cat] || 'default'}>
|
||||
{cat}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
@@ -382,16 +403,26 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
dataIndex: 'timeGrainCount',
|
||||
key: 'timeGrainCount',
|
||||
width: 100,
|
||||
sorter: (a: TableEntry, b: TableEntry) => a.timeGrainCount - b.timeGrainCount,
|
||||
sorter: (a: TableEntry, b: TableEntry) =>
|
||||
a.timeGrainCount - b.timeGrainCount,
|
||||
render: (count: number, record: TableEntry) => {
|
||||
if (count === 0) return <span>-</span>;
|
||||
const grains = getSupportedTimeGrains(record.time_grains);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', maxWidth: 280 }}>
|
||||
{grains.map((grain) => (
|
||||
<Tag key={grain} style={{ margin: 0 }}>{grain}</Tag>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
maxWidth: 280,
|
||||
}}
|
||||
>
|
||||
{grains.map(grain => (
|
||||
<Tag key={grain} style={{ margin: 0 }}>
|
||||
{grain}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
@@ -450,13 +481,17 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{record.joins && <Tag color="green">JOINs</Tag>}
|
||||
{record.subqueries && <Tag color="green">Subqueries</Tag>}
|
||||
{record.supports_dynamic_schema && <Tag color="blue">Dynamic Schema</Tag>}
|
||||
{record.supports_dynamic_schema && (
|
||||
<Tag color="blue">Dynamic Schema</Tag>
|
||||
)}
|
||||
{record.supports_catalog && <Tag color="purple">Catalog</Tag>}
|
||||
{record.ssh_tunneling && <Tag color="cyan">SSH</Tag>}
|
||||
{record.supports_file_upload && <Tag color="orange">File Upload</Tag>}
|
||||
{record.query_cancelation && <Tag color="volcano">Query Cancel</Tag>}
|
||||
{record.query_cost_estimation && <Tag color="gold">Cost Est.</Tag>}
|
||||
{record.user_impersonation && <Tag color="magenta">Impersonation</Tag>}
|
||||
{record.user_impersonation && (
|
||||
<Tag color="magenta">Impersonation</Tag>
|
||||
)}
|
||||
{record.sql_validation && <Tag color="lime">SQL Validation</Tag>}
|
||||
</div>
|
||||
),
|
||||
@@ -545,7 +580,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
placeholder="Search databases..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
@@ -556,7 +591,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
allowClear
|
||||
options={categories.map((cat) => ({
|
||||
options={categories.map(cat => ({
|
||||
label: (
|
||||
<span>
|
||||
<Tag
|
||||
@@ -578,11 +613,15 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
<Table
|
||||
dataSource={filteredDatabases}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
|
||||
rowKey={record =>
|
||||
record.isCompatible
|
||||
? `${record.compatibleWith}-${record.name}`
|
||||
: record.name
|
||||
}
|
||||
pagination={{
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${total} databases`,
|
||||
showTotal: total => `${total} databases`,
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,10 @@ const databases = Object.entries(typedData.databases)
|
||||
.map(([name, db]) => ({
|
||||
name,
|
||||
logo: db.documentation.logo!,
|
||||
docPath: `/user-docs/databases/supported/${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
|
||||
docPath: `/user-docs/databases/supported/${name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')}`,
|
||||
}));
|
||||
|
||||
export default function DatabaseLogoWall(): React.JSX.Element {
|
||||
|
||||
@@ -77,7 +77,7 @@ export interface CompatibleDatabase {
|
||||
description?: string;
|
||||
logo?: string;
|
||||
homepage_url?: string;
|
||||
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
|
||||
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
|
||||
pypi_packages?: string[];
|
||||
connection_string?: string;
|
||||
parameters?: Record<string, string>;
|
||||
@@ -87,27 +87,27 @@ export interface CompatibleDatabase {
|
||||
}
|
||||
|
||||
export interface CustomError {
|
||||
error_type: string; // e.g., "CONNECTION_INVALID_USERNAME_ERROR"
|
||||
message_template: string; // e.g., 'The username "%(username)s" does not exist.'
|
||||
regex_pattern?: string; // The regex pattern that matches this error (optional, for reference)
|
||||
regex_name?: string; // The name of the regex constant (e.g., "CONNECTION_INVALID_USERNAME_REGEX")
|
||||
invalid_fields?: string[]; // Fields that are invalid, e.g., ["username", "password"]
|
||||
issue_codes?: number[]; // Related issue codes from ISSUE_CODES mapping
|
||||
category?: string; // Error category: "Authentication", "Connection", "Query", etc.
|
||||
description?: string; // Human-readable short description of the error type
|
||||
error_type: string; // e.g., "CONNECTION_INVALID_USERNAME_ERROR"
|
||||
message_template: string; // e.g., 'The username "%(username)s" does not exist.'
|
||||
regex_pattern?: string; // The regex pattern that matches this error (optional, for reference)
|
||||
regex_name?: string; // The name of the regex constant (e.g., "CONNECTION_INVALID_USERNAME_REGEX")
|
||||
invalid_fields?: string[]; // Fields that are invalid, e.g., ["username", "password"]
|
||||
issue_codes?: number[]; // Related issue codes from ISSUE_CODES mapping
|
||||
category?: string; // Error category: "Authentication", "Connection", "Query", etc.
|
||||
description?: string; // Human-readable short description of the error type
|
||||
}
|
||||
|
||||
export interface DatabaseDocumentation {
|
||||
description?: string;
|
||||
logo?: string;
|
||||
homepage_url?: string;
|
||||
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
|
||||
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
|
||||
pypi_packages?: string[];
|
||||
connection_string?: string;
|
||||
default_port?: number;
|
||||
parameters?: Record<string, string>;
|
||||
notes?: string;
|
||||
limitations?: string[]; // Known limitations or caveats
|
||||
limitations?: string[]; // Known limitations or caveats
|
||||
connection_examples?: ConnectionExample[];
|
||||
host_examples?: HostExample[];
|
||||
drivers?: Driver[];
|
||||
@@ -122,7 +122,7 @@ export interface DatabaseDocumentation {
|
||||
sqlalchemy_docs_url?: string;
|
||||
advanced_features?: Record<string, string>;
|
||||
compatible_databases?: CompatibleDatabase[];
|
||||
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
|
||||
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
|
||||
}
|
||||
|
||||
export interface TimeGrains {
|
||||
|
||||
@@ -52,7 +52,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
|
||||
const filteredComponents = useMemo(() => {
|
||||
return components
|
||||
.filter((comp) => {
|
||||
.filter(comp => {
|
||||
const matchesSearch =
|
||||
!searchText ||
|
||||
comp.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
@@ -67,7 +67,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
|
||||
const { categories, categoryCounts } = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
components.forEach((comp) => {
|
||||
components.forEach(comp => {
|
||||
counts[comp.category] = (counts[comp.category] || 0) + 1;
|
||||
});
|
||||
return {
|
||||
@@ -103,7 +103,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
filters: categories.map((cat) => ({
|
||||
filters: categories.map(cat => ({
|
||||
text: CATEGORY_LABELS[cat] || cat,
|
||||
value: cat,
|
||||
})),
|
||||
@@ -120,9 +120,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
dataIndex: 'package',
|
||||
key: 'package',
|
||||
width: 220,
|
||||
render: (pkg: string) => (
|
||||
<code style={{ fontSize: '12px' }}>{pkg}</code>
|
||||
),
|
||||
render: (pkg: string) => <code style={{ fontSize: '12px' }}>{pkg}</code>,
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
@@ -215,7 +213,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
placeholder="Search components..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
@@ -226,7 +224,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
allowClear
|
||||
options={categories.map((cat) => ({
|
||||
options={categories.map(cat => ({
|
||||
label: (
|
||||
<span>
|
||||
<Tag
|
||||
@@ -251,7 +249,7 @@ const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
pagination={{
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${total} components`,
|
||||
showTotal: total => `${total} components`,
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
|
||||
if (!contributors?.length) return null;
|
||||
return (
|
||||
<Avatar.Group size="small" max={{ count: 3 }}>
|
||||
{contributors.map((handle) => {
|
||||
{contributors.map(handle => {
|
||||
const username = handle.replace('@', '');
|
||||
return (
|
||||
<a
|
||||
@@ -51,7 +51,7 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
|
||||
href={`https://github.com/${username}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<Avatar
|
||||
src={`https://github.com/${username}.png?size=40`}
|
||||
@@ -69,7 +69,10 @@ const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
|
||||
|
||||
export default function InTheWild() {
|
||||
return (
|
||||
<Layout title="In the Wild" description="Organizations using Apache Superset">
|
||||
<Layout
|
||||
title="In the Wild"
|
||||
description="Organizations using Apache Superset"
|
||||
>
|
||||
<main>
|
||||
<BlurredSection>
|
||||
<SectionHeader
|
||||
@@ -87,7 +90,9 @@ export default function InTheWild() {
|
||||
</div>
|
||||
</BlurredSection>
|
||||
|
||||
<div style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}>
|
||||
<div
|
||||
style={{ maxWidth: 850, margin: '70px auto 60px', padding: '0 20px' }}
|
||||
>
|
||||
<Collapse
|
||||
bordered={false}
|
||||
defaultActiveKey={Object.keys(typedDataSet.categories)}
|
||||
@@ -96,68 +101,119 @@ export default function InTheWild() {
|
||||
border: '1px solid var(--ifm-border-color)',
|
||||
borderRadius: 10,
|
||||
}}
|
||||
items={Object.entries(typedDataSet.categories).map(([category, items]) => {
|
||||
const logoItems = items.filter(({ logo }) => logo?.trim());
|
||||
const textItems = items.filter(({ logo }) => !logo?.trim());
|
||||
items={Object.entries(typedDataSet.categories).map(
|
||||
([category, items]) => {
|
||||
const logoItems = items.filter(({ logo }) => logo?.trim());
|
||||
const textItems = items.filter(({ logo }) => !logo?.trim());
|
||||
|
||||
return {
|
||||
key: category,
|
||||
label: (
|
||||
<Text strong style={{ fontSize: 16, lineHeight: '22px', color: 'var(--ifm-font-base-color)' }}>
|
||||
{category} ({items.length})
|
||||
</Text>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{logoItems.length > 0 && (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: textItems.length > 0 ? 24 : 0 }}>
|
||||
{logoItems.map(({ name, url, logo, contributors }) => (
|
||||
<Col xs={24} sm={12} md={8} key={name}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: 150, position: 'relative' }}
|
||||
styles={{ body: { padding: 16, height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' } }}
|
||||
>
|
||||
<img
|
||||
src={`/img/logos/${logo}`}
|
||||
alt={name}
|
||||
style={{ maxHeight: 80, maxWidth: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
{contributors?.length && (
|
||||
<div style={{ position: 'absolute', bottom: 8, right: 8 }}>
|
||||
<ContributorAvatars contributors={contributors} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
return {
|
||||
key: category,
|
||||
label: (
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: 16,
|
||||
lineHeight: '22px',
|
||||
color: 'var(--ifm-font-base-color)',
|
||||
}}
|
||||
>
|
||||
{category} ({items.length})
|
||||
</Text>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{logoItems.length > 0 && (
|
||||
<Row
|
||||
gutter={[16, 16]}
|
||||
style={{
|
||||
marginBottom: textItems.length > 0 ? 24 : 0,
|
||||
}}
|
||||
>
|
||||
{logoItems.map(
|
||||
({ name, url, logo, contributors }) => (
|
||||
<Col xs={24} sm={12} md={8} key={name}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
height: 150,
|
||||
position: 'relative',
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`/img/logos/${logo}`}
|
||||
alt={name}
|
||||
style={{
|
||||
maxHeight: 80,
|
||||
maxWidth: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
{contributors?.length && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
}}
|
||||
>
|
||||
<ContributorAvatars
|
||||
contributors={contributors}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
),
|
||||
)}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{textItems.length > 0 && (
|
||||
<Row gutter={[8, 8]}>
|
||||
{textItems.map(({ name, url, contributors }) => (
|
||||
<Col xs={24} sm={12} md={8} key={name}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Card
|
||||
size="small"
|
||||
hoverable
|
||||
styles={{ body: { padding: '8px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } }}
|
||||
>
|
||||
<Text ellipsis style={{ flex: 1 }}>{name}</Text>
|
||||
<ContributorAvatars contributors={contributors} />
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
})}
|
||||
{textItems.length > 0 && (
|
||||
<Row gutter={[8, 8]}>
|
||||
{textItems.map(({ name, url, contributors }) => (
|
||||
<Col xs={24} sm={12} md={8} key={name}>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Card
|
||||
size="small"
|
||||
hoverable
|
||||
styles={{
|
||||
body: {
|
||||
padding: '8px 12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text ellipsis style={{ flex: 1 }}>
|
||||
{name}
|
||||
</Text>
|
||||
<ContributorAvatars
|
||||
contributors={contributors}
|
||||
/>
|
||||
</Card>
|
||||
</a>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
};
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -42,10 +42,13 @@ const Databases = Object.entries(typedDatabaseData.databases)
|
||||
title: name,
|
||||
href: db.documentation?.homepage_url,
|
||||
imgName: db.documentation?.logo,
|
||||
docPath: `/docs/databases/supported/${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
|
||||
docPath: `/docs/databases/supported/${name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')}`,
|
||||
}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
.filter((db) => {
|
||||
.filter(db => {
|
||||
if (seenLogos.has(db.imgName!)) return false;
|
||||
seenLogos.add(db.imgName!);
|
||||
return true;
|
||||
@@ -66,7 +69,7 @@ const typedDataSet = load(DataSet) as DataSetType;
|
||||
// Extract all organizations with logos for the carousel
|
||||
const companiesWithLogos = Object.values(typedDataSet.categories)
|
||||
.flat()
|
||||
.filter((org) => org.logo?.trim());
|
||||
.filter(org => org.logo?.trim());
|
||||
|
||||
// Fisher-Yates shuffle for fair randomization
|
||||
function shuffleArray<T>(array: T[]): T[] {
|
||||
@@ -350,7 +353,9 @@ const StyledDocSectionCard = styled(Link)<StyledDocSectionCardProps>`
|
||||
text-decoration: none;
|
||||
color: var(--ifm-font-base-color);
|
||||
background: transparent;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
@@ -595,7 +600,8 @@ export default function Home(): JSX.Element {
|
||||
const slider = useRef(null);
|
||||
|
||||
const [slideIndex, setSlideIndex] = useState(0);
|
||||
const [shuffledCompanies, setShuffledCompanies] = useState(companiesWithLogos);
|
||||
const [shuffledCompanies, setShuffledCompanies] =
|
||||
useState(companiesWithLogos);
|
||||
|
||||
const onChange = (current, next) => {
|
||||
setSlideIndex(next);
|
||||
@@ -893,7 +899,11 @@ export default function Home(): JSX.Element {
|
||||
</BlurredSection>
|
||||
<BlurredSection>
|
||||
<StyledIntegrations>
|
||||
<SectionHeader level="h2" title="Supported Databases" link="/docs/databases" />
|
||||
<SectionHeader
|
||||
level="h2"
|
||||
title="Supported Databases"
|
||||
link="/docs/databases"
|
||||
/>
|
||||
<div className="database-grid">
|
||||
{Databases.map(({ title, imgName, docPath }) => (
|
||||
<div className="item" key={title}>
|
||||
@@ -957,7 +967,11 @@ export default function Home(): JSX.Element {
|
||||
src={`/img/logos/${logo}`}
|
||||
alt={name}
|
||||
title={name}
|
||||
style={{ maxHeight: 48, maxWidth: '100%', objectFit: 'contain' }}
|
||||
style={{
|
||||
maxHeight: 48,
|
||||
maxWidth: '100%',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</a>
|
||||
|
||||
@@ -18,6 +18,7 @@ under the License.
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
|
||||
@@ -26,16 +26,18 @@
|
||||
/* You can override the default Infima variables here. */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Regular.woff2') format('woff2'),
|
||||
url('../fonts/Roboto-Regular.woff') format('woff');
|
||||
src:
|
||||
url('../fonts/Roboto-Regular.woff2') format('woff2'),
|
||||
url('../fonts/Roboto-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Bold.woff2') format('woff2'),
|
||||
url('../fonts/Roboto-Bold.woff') format('woff');
|
||||
src:
|
||||
url('../fonts/Roboto-Bold.woff2') format('woff2'),
|
||||
url('../fonts/Roboto-Bold.woff') format('woff');
|
||||
font-weight: 700;
|
||||
font-style: bold;
|
||||
}
|
||||
@@ -447,9 +449,9 @@ ul.dropdown__menu svg {
|
||||
/* Limit the code editor height and make it scrollable */
|
||||
/* Target multiple possible class names used by Docusaurus/react-live */
|
||||
.playgroundEditor,
|
||||
[class*="playgroundEditor"],
|
||||
[class*='playgroundEditor'],
|
||||
.live-editor,
|
||||
[class*="liveEditor"] {
|
||||
[class*='liveEditor'] {
|
||||
max-height: 350px !important;
|
||||
overflow: auto !important;
|
||||
}
|
||||
@@ -457,15 +459,15 @@ ul.dropdown__menu svg {
|
||||
/* The actual textarea/code area inside the editor */
|
||||
.playgroundEditor textarea,
|
||||
.playgroundEditor pre,
|
||||
[class*="playgroundEditor"] textarea,
|
||||
[class*="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"] {
|
||||
[class*='playgroundPreview'] {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -280,7 +280,9 @@ a > span > svg {
|
||||
.footer__social-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
.footer__social-links a:hover {
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
* so SSG can render the page without a store context.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import { useSelector } from "react-redux";
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
interface ServerVariable {
|
||||
default?: string;
|
||||
@@ -44,20 +44,20 @@ interface StoreState {
|
||||
|
||||
function colorForMethod(method: string) {
|
||||
switch (method.toLowerCase()) {
|
||||
case "get":
|
||||
return "primary";
|
||||
case "post":
|
||||
return "success";
|
||||
case "delete":
|
||||
return "danger";
|
||||
case "put":
|
||||
return "info";
|
||||
case "patch":
|
||||
return "warning";
|
||||
case "head":
|
||||
return "secondary";
|
||||
case "event":
|
||||
return "secondary";
|
||||
case 'get':
|
||||
return 'primary';
|
||||
case 'post':
|
||||
return 'success';
|
||||
case 'delete':
|
||||
return 'danger';
|
||||
case 'put':
|
||||
return 'info';
|
||||
case 'patch':
|
||||
return 'warning';
|
||||
case 'head':
|
||||
return 'secondary';
|
||||
case 'event':
|
||||
return 'secondary';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ function colorForMethod(method: string) {
|
||||
export interface Props {
|
||||
method: string;
|
||||
path: string;
|
||||
context?: "endpoint" | "callback";
|
||||
context?: 'endpoint' | 'callback';
|
||||
}
|
||||
|
||||
// Inner component rendered only in the browser, where the Redux store exists.
|
||||
@@ -74,11 +74,11 @@ function ServerUrl() {
|
||||
const serverValue = useSelector((state: StoreState) => state.server.value);
|
||||
|
||||
if (serverValue && serverValue.variables) {
|
||||
let serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
|
||||
Object.keys(serverValue.variables).forEach((variable) => {
|
||||
let serverUrlWithVariables = serverValue.url.replace(/\/$/, '');
|
||||
Object.keys(serverValue.variables).forEach(variable => {
|
||||
serverUrlWithVariables = serverUrlWithVariables.replace(
|
||||
`{${variable}}`,
|
||||
serverValue.variables?.[variable].default ?? ""
|
||||
serverValue.variables?.[variable].default ?? '',
|
||||
);
|
||||
});
|
||||
return <>{serverUrlWithVariables}</>;
|
||||
@@ -93,8 +93,8 @@ function ServerUrl() {
|
||||
|
||||
function MethodEndpoint({ method, path, context }: Props) {
|
||||
const renderServerUrl = () => {
|
||||
if (context === "callback") {
|
||||
return "";
|
||||
if (context === 'callback') {
|
||||
return '';
|
||||
}
|
||||
return <BrowserOnly>{() => <ServerUrl />}</BrowserOnly>;
|
||||
};
|
||||
@@ -102,13 +102,13 @@ function MethodEndpoint({ method, path, context }: Props) {
|
||||
return (
|
||||
<>
|
||||
<pre className="openapi__method-endpoint">
|
||||
<span className={"badge badge--" + colorForMethod(method)}>
|
||||
{method === "event" ? "Webhook" : method.toUpperCase()}
|
||||
</span>{" "}
|
||||
{method !== "event" && (
|
||||
<span className={'badge badge--' + colorForMethod(method)}>
|
||||
{method === 'event' ? 'Webhook' : method.toUpperCase()}
|
||||
</span>{' '}
|
||||
{method !== 'event' && (
|
||||
<h2 className="openapi__method-endpoint-path">
|
||||
{renderServerUrl()}
|
||||
{`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
|
||||
{`${path.replace(/{([a-z0-9-_]+)}/gi, ':$1')}`}
|
||||
</h2>
|
||||
)}
|
||||
</pre>
|
||||
|
||||
@@ -75,9 +75,13 @@ export default function DocVersionBadge() {
|
||||
if (afterBase.startsWith('/')) {
|
||||
const segments = afterBase.substring(1).split('/');
|
||||
// Check if first segment is a version (e.g., "1.1.0", "next")
|
||||
if (segments[0] && (segments[0].match(/^\d+\.\d+\.\d+$/) || segments[0] === 'next')) {
|
||||
if (
|
||||
segments[0] &&
|
||||
(segments[0].match(/^\d+\.\d+\.\d+$/) || segments[0] === 'next')
|
||||
) {
|
||||
// Skip the version segment
|
||||
relativePath = segments.length > 1 ? '/' + segments.slice(1).join('/') : '';
|
||||
relativePath =
|
||||
segments.length > 1 ? '/' + segments.slice(1).join('/') : '';
|
||||
} else {
|
||||
// No version in path (e.g., /docs/intro for current version with empty path)
|
||||
relativePath = afterBase;
|
||||
|
||||
@@ -75,7 +75,7 @@ function PlaygroundLivePreview(): ReactNode {
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
fallback={params => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -52,12 +52,21 @@ if (isBrowser) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Alert } = require('@apache-superset/core/components');
|
||||
|
||||
console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10));
|
||||
console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {}));
|
||||
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));
|
||||
console.log(
|
||||
'[ReactLiveScope] Final scope keys:',
|
||||
Object.keys(ReactLiveScope).slice(0, 20),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('[ReactLiveScope] Failed to load Superset components:', e);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,27 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
// File extensions to track as downloads
|
||||
const DOWNLOAD_EXTENSIONS = [
|
||||
'pdf', 'zip', 'tar', 'gz', 'tgz', 'bz2',
|
||||
'exe', 'dmg', 'pkg', 'deb', 'rpm',
|
||||
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
|
||||
'csv', 'json', 'yaml', 'yml',
|
||||
'pdf',
|
||||
'zip',
|
||||
'tar',
|
||||
'gz',
|
||||
'tgz',
|
||||
'bz2',
|
||||
'exe',
|
||||
'dmg',
|
||||
'pkg',
|
||||
'deb',
|
||||
'rpm',
|
||||
'doc',
|
||||
'docx',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'csv',
|
||||
'json',
|
||||
'yaml',
|
||||
'yml',
|
||||
];
|
||||
|
||||
// Scroll depth milestones to track
|
||||
@@ -38,7 +55,9 @@ export default function Root({ children }) {
|
||||
const { matomoUrl, matomoSiteId } = customFields;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname);
|
||||
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(
|
||||
window.location.hostname,
|
||||
);
|
||||
|
||||
// Initialize the _paq array
|
||||
window._paq = window._paq || [];
|
||||
@@ -50,7 +69,10 @@ export default function Root({ children }) {
|
||||
window._paq.push(['setSiteId', matomoSiteId]);
|
||||
|
||||
// Track downloads with custom extensions
|
||||
window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]);
|
||||
window._paq.push([
|
||||
'setDownloadExtensions',
|
||||
DOWNLOAD_EXTENSIONS.join('|'),
|
||||
]);
|
||||
|
||||
// Now load the matomo.js script
|
||||
const script = document.createElement('script');
|
||||
@@ -69,7 +91,11 @@ export default function Root({ children }) {
|
||||
// Helper to track site search
|
||||
const trackSiteSearch = (keyword, category, resultsCount) => {
|
||||
if (devMode) {
|
||||
console.log('Matomo trackSiteSearch:', { keyword, category, resultsCount });
|
||||
console.log('Matomo trackSiteSearch:', {
|
||||
keyword,
|
||||
category,
|
||||
resultsCount,
|
||||
});
|
||||
}
|
||||
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
|
||||
};
|
||||
@@ -82,9 +108,8 @@ export default function Root({ children }) {
|
||||
window._paq.push(['trackPageView']);
|
||||
};
|
||||
|
||||
|
||||
// Track external link clicks using domain as category (vendor-agnostic)
|
||||
const handleLinkClick = (event) => {
|
||||
const handleLinkClick = event => {
|
||||
const link = event.target.closest('a');
|
||||
if (!link) return;
|
||||
|
||||
@@ -106,20 +131,22 @@ export default function Root({ children }) {
|
||||
|
||||
// Track Algolia search queries
|
||||
const setupAlgoliaTracking = () => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const searchInput = node.querySelector?.('.DocSearch-Input') ||
|
||||
(node.classList?.contains('DocSearch-Input') ? node : null);
|
||||
const searchInput =
|
||||
node.querySelector?.('.DocSearch-Input') ||
|
||||
(node.classList?.contains('DocSearch-Input') ? node : null);
|
||||
if (searchInput) {
|
||||
let debounceTimer;
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
searchInput.addEventListener('input', e => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
const query = e.target.value.trim();
|
||||
if (query.length >= 3) {
|
||||
const results = document.querySelectorAll('.DocSearch-Hit');
|
||||
const results =
|
||||
document.querySelectorAll('.DocSearch-Hit');
|
||||
trackSiteSearch(query, 'Documentation', results.length);
|
||||
}
|
||||
}, 1000);
|
||||
@@ -135,21 +162,26 @@ export default function Root({ children }) {
|
||||
};
|
||||
|
||||
// Track video plays
|
||||
const handleVideoPlay = (event) => {
|
||||
const handleVideoPlay = event => {
|
||||
if (event.target.tagName === 'VIDEO') {
|
||||
const videoSrc = event.target.currentSrc || event.target.src || 'unknown';
|
||||
const videoSrc =
|
||||
event.target.currentSrc || event.target.src || 'unknown';
|
||||
trackEvent('Video', 'Play', videoSrc);
|
||||
}
|
||||
};
|
||||
|
||||
// Track CTA button clicks
|
||||
const handleCTAClick = (event) => {
|
||||
const button = event.target.closest('.get-started-button, .default-button-theme');
|
||||
const handleCTAClick = event => {
|
||||
const button = event.target.closest(
|
||||
'.get-started-button, .default-button-theme',
|
||||
);
|
||||
if (button) {
|
||||
const buttonText = button.textContent?.trim() || 'Unknown';
|
||||
const clickedLink = event.target.closest?.('a');
|
||||
const href =
|
||||
clickedLink?.getAttribute('href') || button.getAttribute('href') || '';
|
||||
clickedLink?.getAttribute('href') ||
|
||||
button.getAttribute('href') ||
|
||||
'';
|
||||
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
|
||||
}
|
||||
};
|
||||
@@ -158,15 +190,23 @@ export default function Root({ children }) {
|
||||
let scrollMilestonesReached = new Set();
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const docHeight =
|
||||
document.documentElement.scrollHeight - window.innerHeight;
|
||||
if (docHeight <= 0) return;
|
||||
|
||||
const scrollPercent = Math.round((scrollTop / docHeight) * 100);
|
||||
|
||||
SCROLL_MILESTONES.forEach(milestone => {
|
||||
if (scrollPercent >= milestone && !scrollMilestonesReached.has(milestone)) {
|
||||
if (
|
||||
scrollPercent >= milestone &&
|
||||
!scrollMilestonesReached.has(milestone)
|
||||
) {
|
||||
scrollMilestonesReached.add(milestone);
|
||||
trackEvent('Scroll Depth', `${milestone}%`, window.location.pathname);
|
||||
trackEvent(
|
||||
'Scroll Depth',
|
||||
`${milestone}%`,
|
||||
window.location.pathname,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -178,9 +218,13 @@ export default function Root({ children }) {
|
||||
|
||||
// Track 404 pages
|
||||
const track404 = () => {
|
||||
const is404 = document.querySelector('.theme-doc-404') ||
|
||||
document.title.toLowerCase().includes('not found') ||
|
||||
document.querySelector('h1')?.textContent?.toLowerCase().includes('not found');
|
||||
const is404 =
|
||||
document.querySelector('.theme-doc-404') ||
|
||||
document.title.toLowerCase().includes('not found') ||
|
||||
document
|
||||
.querySelector('h1')
|
||||
?.textContent?.toLowerCase()
|
||||
.includes('not found');
|
||||
if (is404) {
|
||||
trackEvent('Error', '404', window.location.pathname);
|
||||
if (devMode) {
|
||||
@@ -190,19 +234,27 @@ export default function Root({ children }) {
|
||||
};
|
||||
|
||||
// Track copy-to-clipboard events on code blocks
|
||||
const handleCopy = (event) => {
|
||||
const handleCopy = event => {
|
||||
const codeBlock = event.target.closest('pre, code, .prism-code');
|
||||
if (codeBlock) {
|
||||
const codeText = window.getSelection()?.toString() || '';
|
||||
const codeSnippet = codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
|
||||
trackEvent('Code', 'Copy', `${window.location.pathname}: ${codeSnippet}`);
|
||||
const codeSnippet =
|
||||
codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
|
||||
trackEvent(
|
||||
'Code',
|
||||
'Copy',
|
||||
`${window.location.pathname}: ${codeSnippet}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Track color mode preference (as event, no admin config needed)
|
||||
const trackColorMode = () => {
|
||||
const colorMode = document.documentElement.getAttribute('data-theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
const colorMode =
|
||||
document.documentElement.getAttribute('data-theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light');
|
||||
trackEvent('User Preference', 'Color Mode', colorMode);
|
||||
};
|
||||
|
||||
@@ -289,11 +341,14 @@ export default function Root({ children }) {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
// Watch for color mode changes
|
||||
const colorModeObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
const colorModeObserver = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.attributeName === 'data-theme') {
|
||||
trackEvent('User Preference', 'Color Mode Change',
|
||||
document.documentElement.getAttribute('data-theme'));
|
||||
trackEvent(
|
||||
'User Preference',
|
||||
'Color Mode Change',
|
||||
document.documentElement.getAttribute('data-theme'),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user