mirror of
https://github.com/apache/superset.git
synced 2026-07-27 00:52:33 +00:00
docs: remove screenshots from repo — should not be committed to codebase
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { mq } from '../utils';
|
||||
|
||||
const StyledBlurredSection = styled('section')`
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--ifm-border-color);
|
||||
overflow: hidden;
|
||||
.blur {
|
||||
max-width: 635px;
|
||||
width: 100%;
|
||||
margin-top: -70px;
|
||||
margin-bottom: -35px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
${mq[1]} {
|
||||
margin-top: -40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface BlurredSectionProps {
|
||||
children: ReactNode;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const BlurredSection = ({ children, id }: BlurredSectionProps) => {
|
||||
return (
|
||||
<StyledBlurredSection id={id}>
|
||||
{children}
|
||||
<img className="blur" src="/img/community/blur.png" alt="Blur" />
|
||||
</StyledBlurredSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlurredSection;
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Re-export the Button component as a default export
|
||||
import { Button } from '../../../superset-frontend/packages/superset-ui-core/src/components/Button';
|
||||
|
||||
export default Button;
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
interface FAQItem {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
interface FAQSchemaProps {
|
||||
faqs: FAQItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects FAQPage JSON-LD structured data
|
||||
* Use this on FAQ pages to enable rich snippets in search results
|
||||
*
|
||||
* @example
|
||||
* <FAQSchema faqs={[
|
||||
* { question: "What is Superset?", answer: "Apache Superset is..." },
|
||||
* { question: "How do I install it?", answer: "You can install via..." }
|
||||
* ]} />
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import { mq } from '../utils.js';
|
||||
|
||||
const getStartedMenuItems: MenuProps['items'] = [
|
||||
{ key: 'users', label: <Link to="/user-docs/">Users</Link> },
|
||||
{ key: 'admins', label: <Link to="/admin-docs/">Admins</Link> },
|
||||
{ key: 'developers', label: <Link to="/developer-docs/">Developers</Link> },
|
||||
];
|
||||
|
||||
const Root = styled.div<{ $variant: 'hero' | 'navbar' }>`
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-weight: bold;
|
||||
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `
|
||||
width: 208px;
|
||||
margin: 15px auto 0;
|
||||
font-size: 20px;
|
||||
${mq[1]} {
|
||||
font-size: 19px;
|
||||
width: 214px;
|
||||
}
|
||||
`
|
||||
: `
|
||||
width: 176px;
|
||||
margin-right: 20px;
|
||||
font-size: 18px;
|
||||
`}
|
||||
|
||||
.split-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
min-width: 0;
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `padding: 10px 10px;`
|
||||
: `padding: 7px 8px;`}
|
||||
}
|
||||
|
||||
.split-main:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.split-divider {
|
||||
width: 1px;
|
||||
flex-shrink: 0;
|
||||
align-self: stretch;
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `margin: 8px 0;`
|
||||
: `margin: 6px 0;`}
|
||||
}
|
||||
|
||||
.split-dropdown-trigger {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
${({ $variant }) =>
|
||||
$variant === 'hero'
|
||||
? `
|
||||
width: 44px;
|
||||
font-size: 11px;
|
||||
${mq[1]} {
|
||||
width: 46px;
|
||||
}
|
||||
`
|
||||
: `
|
||||
width: 38px;
|
||||
font-size: 10px;
|
||||
`}
|
||||
}
|
||||
|
||||
.split-dropdown-trigger:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
`;
|
||||
|
||||
export type GetStartedSplitButtonProps = {
|
||||
variant: 'hero' | 'navbar';
|
||||
/** Classes for the outer control (include default-button-theme get-started-split) */
|
||||
rootClassName: string;
|
||||
};
|
||||
|
||||
export default function GetStartedSplitButton({
|
||||
variant,
|
||||
rootClassName,
|
||||
}: GetStartedSplitButtonProps) {
|
||||
const menuClassName = `get-started-split-dropdown-menu get-started-split-dropdown-menu--${variant}`;
|
||||
|
||||
return (
|
||||
<Root $variant={variant} className={rootClassName}>
|
||||
<Link to="/user-docs/" className="split-main">
|
||||
Get Started
|
||||
</Link>
|
||||
<span className="split-divider" aria-hidden />
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: getStartedMenuItems,
|
||||
className: menuClassName,
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="split-dropdown-trigger"
|
||||
aria-haspopup="menu"
|
||||
aria-label="Choose documentation: Users, Admins, or Developers"
|
||||
>
|
||||
<DownOutlined />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
|
||||
import ErdSvg from '../../static/img/erd.svg';
|
||||
|
||||
function InteractiveERDSVG() {
|
||||
return (
|
||||
<UncontrolledReactSVGPanZoom
|
||||
width="100%"
|
||||
height="800"
|
||||
background="#003153"
|
||||
tool="auto"
|
||||
>
|
||||
<svg>
|
||||
<ErdSvg />
|
||||
</svg>
|
||||
</UncontrolledReactSVGPanZoom>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveERDSVG;
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { mq } from '../utils';
|
||||
|
||||
type StyledSectionHeaderProps = {
|
||||
dark: boolean;
|
||||
};
|
||||
|
||||
const StyledSectionHeader = styled('div')<StyledSectionHeaderProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 75px 20px 0;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
${mq[1]} {
|
||||
padding-top: 55px;
|
||||
}
|
||||
.title,
|
||||
.subtitle {
|
||||
color: ${props =>
|
||||
props.dark
|
||||
? 'var(--ifm-font-base-color-inverse)'
|
||||
: 'var(--ifm-font-base-color)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSectionHeaderH1 = styled(StyledSectionHeader)`
|
||||
.title {
|
||||
font-size: 96px;
|
||||
${mq[1]} {
|
||||
font-size: 46px;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
margin-top: -45px;
|
||||
margin-bottom: 15px;
|
||||
${mq[1]} {
|
||||
margin-top: -20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 30px;
|
||||
line-height: 40px;
|
||||
${mq[1]} {
|
||||
font-size: 25px;
|
||||
line-height: 29px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSectionHeaderH2 = styled(StyledSectionHeader)`
|
||||
.title {
|
||||
font-size: 48px;
|
||||
${mq[1]} {
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
margin-top: -15px;
|
||||
margin-bottom: 15px;
|
||||
${mq[1]} {
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
${mq[1]} {
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface SectionHeaderProps {
|
||||
level: 'h1' | 'h2';
|
||||
title: string;
|
||||
subtitle?: string | ReactNode;
|
||||
dark?: boolean;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
const SectionHeader = ({
|
||||
level,
|
||||
title,
|
||||
subtitle,
|
||||
dark,
|
||||
link,
|
||||
}: SectionHeaderProps) => {
|
||||
const Heading = level;
|
||||
|
||||
const StyledRoot =
|
||||
level === 'h1' ? StyledSectionHeaderH1 : StyledSectionHeaderH2;
|
||||
|
||||
const titleContent = link ? (
|
||||
<a href={link} style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
{title}
|
||||
</a>
|
||||
) : (
|
||||
title
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRoot dark={!!dark}>
|
||||
<Heading className="title">{titleContent}</Heading>
|
||||
<img className="line" src="/img/community/line.png" alt="line" />
|
||||
{subtitle && <div className="subtitle">{subtitle}</div>}
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
||||
@@ -1,530 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
// Lazy-loaded component registry - populated on first use in browser
|
||||
let componentRegistry = null;
|
||||
let SupersetProviders = null;
|
||||
|
||||
function getComponentRegistry() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {}; // SSR - return empty
|
||||
}
|
||||
|
||||
if (componentRegistry !== null) {
|
||||
return componentRegistry; // Already loaded
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const antd = require('antd');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CoreUI = require('@apache-superset/core/components');
|
||||
|
||||
// Build component registry with antd as base fallback layer.
|
||||
// Some Superset components (e.g., Typography) use styled-components that may
|
||||
// fail to initialize in the docs build. Antd originals serve as fallbacks.
|
||||
componentRegistry = { ...antd, ...SupersetComponents, ...CoreUI };
|
||||
|
||||
return componentRegistry;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load components:', error);
|
||||
componentRegistry = {};
|
||||
return componentRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
function getProviders() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ({ children }) => children; // SSR
|
||||
}
|
||||
|
||||
if (SupersetProviders !== null) {
|
||||
return SupersetProviders;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/theme');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App, ConfigProvider } = require('antd');
|
||||
|
||||
// Configure Ant Design to render portals (tooltips, dropdowns, etc.)
|
||||
// inside the closest .storybook-example container instead of document.body
|
||||
// This fixes positioning issues in the docs pages
|
||||
const getPopupContainer = (triggerNode) => {
|
||||
// Find the closest .storybook-example container
|
||||
const container = triggerNode?.closest?.('.storybook-example');
|
||||
return container || document.body;
|
||||
};
|
||||
|
||||
SupersetProviders = ({ children }) => (
|
||||
<themeObject.SupersetThemeProvider>
|
||||
<ConfigProvider
|
||||
getPopupContainer={getPopupContainer}
|
||||
getTargetContainer={() => document.body}
|
||||
>
|
||||
<App>{children}</App>
|
||||
</ConfigProvider>
|
||||
</themeObject.SupersetThemeProvider>
|
||||
);
|
||||
return SupersetProviders;
|
||||
} catch (error) {
|
||||
console.error('[StorybookWrapper] Failed to load providers:', error);
|
||||
return ({ children }) => children;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a value is a valid React component (function, forwardRef, memo, etc.)
|
||||
function isReactComponent(value) {
|
||||
if (!value) return false;
|
||||
// Function/class components
|
||||
if (typeof value === 'function') return true;
|
||||
// forwardRef, memo, lazy — React wraps these as objects with $$typeof
|
||||
if (typeof value === 'object' && value.$$typeof) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve component from string name or React component
|
||||
// Supports dot notation for nested components (e.g., 'Icons.InfoCircleOutlined')
|
||||
function resolveComponent(component) {
|
||||
if (!component) return null;
|
||||
// If already a component (function/class/forwardRef), return as-is
|
||||
if (isReactComponent(component)) return component;
|
||||
// If string, look up in registry
|
||||
if (typeof component === 'string') {
|
||||
const registry = getComponentRegistry();
|
||||
// Handle dot notation (e.g., 'Icons.InfoCircleOutlined')
|
||||
if (component.includes('.')) {
|
||||
const parts = component.split('.');
|
||||
let current = registry[parts[0]];
|
||||
for (let i = 1; i < parts.length && current; i++) {
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return isReactComponent(current) ? current : null;
|
||||
}
|
||||
return registry[component] || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loading placeholder for SSR
|
||||
function LoadingPlaceholder() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
minHeight: '100px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#999',
|
||||
}}
|
||||
>
|
||||
Loading component...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story example
|
||||
export function StoryExample({ component, props = {} }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
const { children, restProps } = extractChildren(props);
|
||||
return (
|
||||
<Providers>
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component ? (
|
||||
<Component {...restProps}>{children}</Component>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Props that should be rendered as children rather than passed as props
|
||||
const CHILDREN_PROP_NAMES = ['label', 'children', 'text', 'content'];
|
||||
|
||||
// Extract children from props based on common conventions
|
||||
function extractChildren(props) {
|
||||
for (const propName of CHILDREN_PROP_NAMES) {
|
||||
if (props[propName] !== undefined && props[propName] !== null && props[propName] !== '') {
|
||||
const { [propName]: childContent, ...restProps } = props;
|
||||
return { children: childContent, restProps };
|
||||
}
|
||||
}
|
||||
return { children: null, restProps: props };
|
||||
}
|
||||
|
||||
// Generate sample children for layout components
|
||||
// Supports:
|
||||
// - Array of strings: ['Item 1', 'Item 2'] - renders as styled divs
|
||||
// - Array of component descriptors: [{ component: 'Button', props: { children: 'Click' } }]
|
||||
// - Number: 3 - generates that many sample items
|
||||
// - String: 'content' - renders as literal content
|
||||
function generateSampleChildren(sampleChildren, sampleChildrenStyle) {
|
||||
if (!sampleChildren) return null;
|
||||
|
||||
// Default style if none provided (minimal, just enough to see items)
|
||||
const itemStyle = sampleChildrenStyle || {};
|
||||
|
||||
// If it's an array, check if items are component descriptors or strings
|
||||
if (Array.isArray(sampleChildren)) {
|
||||
return sampleChildren.map((item, i) => {
|
||||
// Component descriptor: { component: 'Button', props: { ... } }
|
||||
if (item && typeof item === 'object' && item.component) {
|
||||
const ChildComponent = resolveComponent(item.component);
|
||||
if (ChildComponent) {
|
||||
return <ChildComponent key={i} {...item.props} />;
|
||||
}
|
||||
// Fallback if component not found
|
||||
return <div key={i}>{item.props?.children || `Unknown: ${item.component}`}</div>;
|
||||
}
|
||||
// Simple string
|
||||
return (
|
||||
<div key={i} style={itemStyle}>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// If it's a number, generate that many sample items
|
||||
if (typeof sampleChildren === 'number') {
|
||||
return new Array(sampleChildren).fill(null).map((_, i) => (
|
||||
<div key={i} style={itemStyle}>
|
||||
Item {i + 1}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
// If it's a string, treat as literal content
|
||||
if (typeof sampleChildren === 'string') {
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
return sampleChildren;
|
||||
}
|
||||
|
||||
// Inner component for StoryWithControls (browser-only)
|
||||
// renderComponent allows overriding which component to actually render (useful when the named
|
||||
// component is a namespace object like Icons, not a React component)
|
||||
// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility
|
||||
function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
// Use renderComponent if provided, otherwise use the main component name
|
||||
const componentToRender = renderComponent || component;
|
||||
const Component = resolveComponent(componentToRender);
|
||||
const Providers = getProviders();
|
||||
const [stateProps, setStateProps] = React.useState(props);
|
||||
|
||||
const updateProp = (key, value) => {
|
||||
setStateProps(prev => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// Extract children from props (label, children, text, content)
|
||||
// When sampleChildren is explicitly provided, skip extraction so all props
|
||||
// (like 'content') stay as component props rather than becoming children
|
||||
const { children: propsChildren, restProps } = sampleChildren
|
||||
? { children: null, restProps: stateProps }
|
||||
: extractChildren(stateProps);
|
||||
// Filter out undefined values so they don't override component defaults
|
||||
const filteredProps = Object.fromEntries(
|
||||
Object.entries(restProps).filter(([, v]) => v !== undefined)
|
||||
);
|
||||
|
||||
// Resolve any prop values that are component descriptors
|
||||
// e.g., { component: 'Button', props: { children: 'Click' } }
|
||||
// Also resolves descriptors nested inside array items:
|
||||
// e.g., items: [{ id: 'x', element: { component: 'div', props: { children: 'text' } } }]
|
||||
Object.keys(filteredProps).forEach(key => {
|
||||
const value = filteredProps[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value) && value.component) {
|
||||
const PropComponent = resolveComponent(value.component);
|
||||
if (PropComponent) {
|
||||
filteredProps[key] = <PropComponent {...value.props} />;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
filteredProps[key] = value.map((item, idx) => {
|
||||
if (item && typeof item === 'object') {
|
||||
const resolved = { ...item };
|
||||
Object.keys(resolved).forEach(field => {
|
||||
const fieldValue = resolved[field];
|
||||
if (fieldValue && typeof fieldValue === 'object' && !Array.isArray(fieldValue) && fieldValue.component) {
|
||||
const FieldComponent = resolveComponent(fieldValue.component);
|
||||
if (FieldComponent) {
|
||||
resolved[field] = React.createElement(FieldComponent, { key: `${key}-${idx}`, ...fieldValue.props });
|
||||
}
|
||||
}
|
||||
});
|
||||
return resolved;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// For List-like components with dataSource but no renderItem, provide a default
|
||||
if (filteredProps.dataSource && !filteredProps.renderItem) {
|
||||
const ListItem = resolveComponent('List')?.Item;
|
||||
filteredProps.renderItem = (item) =>
|
||||
ListItem
|
||||
? React.createElement(ListItem, null, String(item))
|
||||
: React.createElement('div', null, String(item));
|
||||
}
|
||||
|
||||
// Use sample children if provided, otherwise use props children
|
||||
const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren;
|
||||
|
||||
// For components with a trigger (like Modal with show/onHide), add handlers.
|
||||
// onHideProp supports comma-separated names for components with multiple close
|
||||
// callbacks (e.g., "onHide,handleSave,onConfirmNavigation").
|
||||
const triggerProps = {};
|
||||
if (triggerProp && onHideProp) {
|
||||
const closeHandler = () => updateProp(triggerProp, false);
|
||||
onHideProp.split(',').forEach(prop => {
|
||||
triggerProps[prop.trim()] = closeHandler;
|
||||
});
|
||||
}
|
||||
|
||||
// Get the Button component for trigger buttons
|
||||
const ButtonComponent = resolveComponent('Button');
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<div className="storybook-with-controls">
|
||||
<div
|
||||
className="storybook-example"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
position: 'relative', // Required for portal positioning
|
||||
}}
|
||||
>
|
||||
{Component ? (
|
||||
<>
|
||||
{/* Show a trigger button for components like Modal */}
|
||||
{triggerProp && ButtonComponent && (
|
||||
<ButtonComponent onClick={() => updateProp(triggerProp, true)}>
|
||||
Open {component}
|
||||
</ButtonComponent>
|
||||
)}
|
||||
<Component {...filteredProps} {...triggerProps}>{children}</Component>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(componentToRender)}" not found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{controls.length > 0 && (
|
||||
<div
|
||||
className="storybook-controls"
|
||||
style={{
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: '4px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<h4>Controls</h4>
|
||||
{controls.map(control => (
|
||||
<div key={control.name} style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{control.label || control.name}:
|
||||
</label>
|
||||
{control.type === 'select' ? (
|
||||
<select
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value || undefined)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
>
|
||||
<option value="">— None —</option>
|
||||
{control.options?.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : control.type === 'inline-radio' || control.type === 'radio' ? (
|
||||
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
{control.options?.map(option => (
|
||||
<label
|
||||
key={option}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={control.name}
|
||||
value={option}
|
||||
checked={stateProps[control.name] === option}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
/>
|
||||
{option}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : control.type === 'boolean' ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, e.target.checked)}
|
||||
/>
|
||||
) : control.type === 'number' ? (
|
||||
<input
|
||||
type="number"
|
||||
value={stateProps[control.name]}
|
||||
onChange={e => updateProp(control.name, Number(e.target.value))}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
) : control.type === 'color' ? (
|
||||
<input
|
||||
type="color"
|
||||
value={stateProps[control.name] || '#000000'}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{
|
||||
width: '50px',
|
||||
height: '30px',
|
||||
padding: '2px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={stateProps[control.name] ?? ''}
|
||||
onChange={e => updateProp(control.name, e.target.value)}
|
||||
style={{ width: '100%', padding: '5px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A simple component to display a story with controls
|
||||
// renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons')
|
||||
// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide")
|
||||
export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<StoryWithControlsInner
|
||||
component={Component}
|
||||
renderComponent={renderComponent}
|
||||
props={props}
|
||||
controls={controls}
|
||||
sampleChildren={sampleChildren}
|
||||
sampleChildrenStyle={sampleChildrenStyle}
|
||||
triggerProp={triggerProp}
|
||||
onHideProp={onHideProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
// Inner component for ComponentGallery (browser-only)
|
||||
function ComponentGalleryInner({ component, sizes, styles, sizeProp, styleProp }) {
|
||||
const Component = resolveComponent(component);
|
||||
const Providers = getProviders();
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<div style={{ color: '#999' }}>
|
||||
Component "{String(component)}" not found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<div className="component-gallery">
|
||||
{sizes.map(size => (
|
||||
<div key={size} style={{ marginBottom: 40 }}>
|
||||
<h4 style={{ marginBottom: 16, color: '#666' }}>{size}</h4>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
|
||||
{styles.map(style => (
|
||||
<Component
|
||||
key={`${style}_${size}`}
|
||||
{...{ [sizeProp]: size, [styleProp]: style }}
|
||||
>
|
||||
{style}
|
||||
</Component>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
|
||||
// A component to display a gallery of all variants (sizes x styles)
|
||||
export function ComponentGallery({ component, sizes = [], styles = [], sizeProp = 'size', styleProp = 'variant' }) {
|
||||
return (
|
||||
<BrowserOnly fallback={<LoadingPlaceholder />}>
|
||||
{() => (
|
||||
<ComponentGalleryInner
|
||||
component={component}
|
||||
sizes={sizes}
|
||||
styles={styles}
|
||||
sizeProp={sizeProp}
|
||||
styleProp={styleProp}
|
||||
/>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
|
||||
interface TechArticleSchemaProps {
|
||||
title: string;
|
||||
description: string;
|
||||
datePublished?: string;
|
||||
dateModified?: string;
|
||||
keywords?: string[];
|
||||
proficiencyLevel?: 'Beginner' | 'Expert';
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that injects TechArticle JSON-LD structured data for documentation pages.
|
||||
* This helps search engines understand technical documentation content.
|
||||
*
|
||||
* @example
|
||||
* <TechArticleSchema
|
||||
* title="Installing Superset with Docker"
|
||||
* description="Learn how to install Apache Superset using Docker Compose"
|
||||
* keywords={['docker', 'installation', 'superset']}
|
||||
* proficiencyLevel="Beginner"
|
||||
* />
|
||||
*/
|
||||
export default function TechArticleSchema({
|
||||
title,
|
||||
description,
|
||||
datePublished,
|
||||
dateModified,
|
||||
keywords = [],
|
||||
proficiencyLevel = 'Beginner',
|
||||
}: TechArticleSchemaProps): JSX.Element {
|
||||
const location = useLocation();
|
||||
const url = `https://superset.apache.org${location.pathname}`;
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'TechArticle',
|
||||
headline: title,
|
||||
description,
|
||||
url,
|
||||
proficiencyLevel,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Superset Contributors',
|
||||
url: 'https://github.com/apache/superset/graphs/contributors',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Apache Software Foundation',
|
||||
url: 'https://www.apache.org/',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://www.apache.org/foundation/press/kit/asf_logo.png',
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': url,
|
||||
},
|
||||
...(datePublished && { datePublished }),
|
||||
...(dateModified && { dateModified }),
|
||||
...(keywords.length > 0 && { keywords: keywords.join(', ') }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<script type="application/ld+json">{JSON.stringify(schema)}</script>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, Tag, Input, Select, Tooltip } from 'antd';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
CheckCircleOutlined,
|
||||
ApiOutlined,
|
||||
KeyOutlined,
|
||||
SearchOutlined,
|
||||
LinkOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatabaseData, DatabaseInfo, TimeGrains } from './types';
|
||||
|
||||
interface DatabaseIndexProps {
|
||||
data: DatabaseData;
|
||||
}
|
||||
|
||||
// Type for table entries (includes both regular DBs and compatible DBs)
|
||||
interface TableEntry {
|
||||
name: string;
|
||||
categories: string[]; // Multiple categories supported
|
||||
score: number;
|
||||
max_score: number;
|
||||
timeGrainCount: number;
|
||||
time_grains?: TimeGrains;
|
||||
hasDrivers: boolean;
|
||||
hasAuthMethods: boolean;
|
||||
hasConnectionString: boolean;
|
||||
hasCustomErrors: boolean;
|
||||
customErrorCount: number;
|
||||
joins?: boolean;
|
||||
subqueries?: boolean;
|
||||
supports_dynamic_schema?: boolean;
|
||||
supports_catalog?: boolean;
|
||||
ssh_tunneling?: boolean;
|
||||
supports_file_upload?: boolean;
|
||||
query_cancelation?: boolean;
|
||||
query_cost_estimation?: boolean;
|
||||
user_impersonation?: boolean;
|
||||
sql_validation?: boolean;
|
||||
documentation?: DatabaseInfo['documentation'];
|
||||
// For compatible databases
|
||||
isCompatible?: boolean;
|
||||
compatibleWith?: string;
|
||||
compatibleDescription?: string;
|
||||
}
|
||||
|
||||
// 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',
|
||||
};
|
||||
|
||||
// Category colors for visual distinction
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
'Cloud - AWS': 'orange',
|
||||
'Cloud - Google': 'blue',
|
||||
'Cloud - Azure': 'cyan',
|
||||
'Cloud Data Warehouses': 'purple',
|
||||
'Apache Projects': 'red',
|
||||
'Traditional RDBMS': 'green',
|
||||
'Analytical Databases': 'magenta',
|
||||
'Search & NoSQL': 'gold',
|
||||
'Query Engines': 'lime',
|
||||
'Time Series Databases': 'volcano',
|
||||
'Other Databases': 'default',
|
||||
// Licensing categories
|
||||
'Open Source': 'geekblue',
|
||||
'Hosted Open Source': 'cyan',
|
||||
'Proprietary': 'default',
|
||||
};
|
||||
|
||||
// Convert category constant to display name
|
||||
function getCategoryDisplayName(cat: string): string {
|
||||
return CATEGORY_DISPLAY_NAMES[cat] || cat;
|
||||
}
|
||||
|
||||
// Get categories for a database - uses categories from metadata when available
|
||||
// Falls back to name-based inference for compatible databases without categories
|
||||
function getCategories(
|
||||
name: string,
|
||||
documentationCategories?: string[]
|
||||
): string[] {
|
||||
// Prefer categories from documentation metadata (computed by Python)
|
||||
if (documentationCategories && documentationCategories.length > 0) {
|
||||
return documentationCategories.map(getCategoryDisplayName);
|
||||
}
|
||||
|
||||
// Fallback: infer from name (for compatible databases without categories)
|
||||
const nameLower = name.toLowerCase();
|
||||
|
||||
if (nameLower.includes('aws') || nameLower.includes('amazon'))
|
||||
return ['Cloud - AWS'];
|
||||
if (nameLower.includes('google') || nameLower.includes('bigquery'))
|
||||
return ['Cloud - Google'];
|
||||
if (nameLower.includes('azure') || nameLower.includes('microsoft'))
|
||||
return ['Cloud - Azure'];
|
||||
if (nameLower.includes('snowflake') || nameLower.includes('databricks'))
|
||||
return ['Cloud Data Warehouses'];
|
||||
if (
|
||||
nameLower.includes('apache') ||
|
||||
nameLower.includes('druid') ||
|
||||
nameLower.includes('hive') ||
|
||||
nameLower.includes('spark')
|
||||
)
|
||||
return ['Apache Projects'];
|
||||
if (
|
||||
nameLower.includes('postgres') ||
|
||||
nameLower.includes('mysql') ||
|
||||
nameLower.includes('sqlite') ||
|
||||
nameLower.includes('mariadb')
|
||||
)
|
||||
return ['Traditional RDBMS'];
|
||||
if (
|
||||
nameLower.includes('clickhouse') ||
|
||||
nameLower.includes('vertica') ||
|
||||
nameLower.includes('starrocks')
|
||||
)
|
||||
return ['Analytical Databases'];
|
||||
if (
|
||||
nameLower.includes('elastic') ||
|
||||
nameLower.includes('solr') ||
|
||||
nameLower.includes('couchbase')
|
||||
)
|
||||
return ['Search & NoSQL'];
|
||||
if (nameLower.includes('trino') || nameLower.includes('presto'))
|
||||
return ['Query Engines'];
|
||||
|
||||
return ['Other Databases'];
|
||||
}
|
||||
|
||||
// Count supported time grains
|
||||
function countTimeGrains(db: DatabaseInfo): number {
|
||||
if (!db.time_grains) return 0;
|
||||
return Object.values(db.time_grains).filter(Boolean).length;
|
||||
}
|
||||
|
||||
// Format time grain name for display (e.g., FIVE_MINUTES -> "5 min")
|
||||
function formatTimeGrain(grain: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
SECOND: 'Second',
|
||||
FIVE_SECONDS: '5 sec',
|
||||
THIRTY_SECONDS: '30 sec',
|
||||
MINUTE: 'Minute',
|
||||
FIVE_MINUTES: '5 min',
|
||||
TEN_MINUTES: '10 min',
|
||||
FIFTEEN_MINUTES: '15 min',
|
||||
THIRTY_MINUTES: '30 min',
|
||||
HALF_HOUR: '30 min',
|
||||
HOUR: 'Hour',
|
||||
SIX_HOURS: '6 hours',
|
||||
DAY: 'Day',
|
||||
WEEK: 'Week',
|
||||
WEEK_STARTING_SUNDAY: 'Week (Sun)',
|
||||
WEEK_STARTING_MONDAY: 'Week (Mon)',
|
||||
WEEK_ENDING_SATURDAY: 'Week (→Sat)',
|
||||
WEEK_ENDING_SUNDAY: 'Week (→Sun)',
|
||||
MONTH: 'Month',
|
||||
QUARTER: 'Quarter',
|
||||
QUARTER_YEAR: 'Quarter',
|
||||
YEAR: 'Year',
|
||||
};
|
||||
return mapping[grain] || grain;
|
||||
}
|
||||
|
||||
// Get list of supported time grains for tooltip
|
||||
function getSupportedTimeGrains(timeGrains?: TimeGrains): string[] {
|
||||
if (!timeGrains) return [];
|
||||
return Object.entries(timeGrains)
|
||||
.filter(([, supported]) => supported)
|
||||
.map(([grain]) => formatTimeGrain(grain));
|
||||
}
|
||||
|
||||
const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||
|
||||
const { statistics, databases } = data;
|
||||
|
||||
// Convert databases object to array, including compatible databases
|
||||
const databaseList = useMemo(() => {
|
||||
const entries: TableEntry[] = [];
|
||||
|
||||
Object.entries(databases).forEach(([name, db]) => {
|
||||
// Add the main database
|
||||
// Use categories from documentation metadata (computed by Python) when available
|
||||
entries.push({
|
||||
...db,
|
||||
name,
|
||||
categories: getCategories(name, db.documentation?.categories),
|
||||
timeGrainCount: countTimeGrains(db),
|
||||
hasDrivers: (db.documentation?.drivers?.length ?? 0) > 0,
|
||||
hasAuthMethods: (db.documentation?.authentication_methods?.length ?? 0) > 0,
|
||||
hasConnectionString: Boolean(
|
||||
db.documentation?.connection_string ||
|
||||
(db.documentation?.drivers?.length ?? 0) > 0
|
||||
),
|
||||
hasCustomErrors: (db.documentation?.custom_errors?.length ?? 0) > 0,
|
||||
customErrorCount: db.documentation?.custom_errors?.length ?? 0,
|
||||
isCompatible: false,
|
||||
});
|
||||
|
||||
// Add compatible databases from this database's documentation
|
||||
const compatibleDbs = db.documentation?.compatible_databases ?? [];
|
||||
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()
|
||||
);
|
||||
|
||||
if (!existsAsMain) {
|
||||
// Compatible databases: use their categories if defined, or infer from name
|
||||
entries.push({
|
||||
name: compat.name,
|
||||
categories: getCategories(compat.name, compat.categories),
|
||||
// Compatible DBs inherit scores from parent
|
||||
score: db.score,
|
||||
max_score: db.max_score,
|
||||
timeGrainCount: countTimeGrains(db),
|
||||
hasDrivers: false,
|
||||
hasAuthMethods: false,
|
||||
hasConnectionString: Boolean(compat.connection_string),
|
||||
hasCustomErrors: false,
|
||||
customErrorCount: 0,
|
||||
joins: db.joins,
|
||||
subqueries: db.subqueries,
|
||||
supports_dynamic_schema: db.supports_dynamic_schema,
|
||||
supports_catalog: db.supports_catalog,
|
||||
ssh_tunneling: db.ssh_tunneling,
|
||||
documentation: {
|
||||
description: compat.description,
|
||||
connection_string: compat.connection_string,
|
||||
pypi_packages: compat.pypi_packages,
|
||||
},
|
||||
isCompatible: true,
|
||||
compatibleWith: name,
|
||||
compatibleDescription: `Uses ${name} driver`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}, [databases]);
|
||||
|
||||
// Filter and sort databases
|
||||
const filteredDatabases = useMemo(() => {
|
||||
return databaseList
|
||||
.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);
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
.sort((a, b) => b.score - a.score);
|
||||
}, [databaseList, searchText, categoryFilter]);
|
||||
|
||||
// Get unique categories and counts for filter
|
||||
const { categories, categoryCounts } = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
databaseList.forEach((db) => {
|
||||
// Count each category the database belongs to
|
||||
db.categories.forEach((cat) => {
|
||||
counts[cat] = (counts[cat] || 0) + 1;
|
||||
});
|
||||
});
|
||||
return {
|
||||
categories: Object.keys(counts).sort(),
|
||||
categoryCounts: counts,
|
||||
};
|
||||
}, [databaseList]);
|
||||
|
||||
// Table columns
|
||||
const columns = [
|
||||
{
|
||||
title: 'Database',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
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, '');
|
||||
|
||||
// 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)}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a href={linkTarget}>
|
||||
<strong>{name}</strong>
|
||||
</a>
|
||||
{record.isCompatible && record.compatibleWith && (
|
||||
<Tag
|
||||
icon={<LinkOutlined />}
|
||||
color="geekblue"
|
||||
style={{ marginLeft: 8, fontSize: '11px' }}
|
||||
>
|
||||
{record.compatibleWith} compatible
|
||||
</Tag>
|
||||
)}
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.documentation?.description?.slice(0, 80)}
|
||||
{(record.documentation?.description?.length ?? 0) > 80 ? '...' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Categories',
|
||||
dataIndex: 'categories',
|
||||
key: 'categories',
|
||||
width: 220,
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Score',
|
||||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 80,
|
||||
sorter: (a: TableEntry, b: TableEntry) => a.score - b.score,
|
||||
defaultSortOrder: 'descend' as const,
|
||||
render: (score: number, record: TableEntry) => (
|
||||
<span
|
||||
style={{
|
||||
color: score > 150 ? '#52c41a' : score > 100 ? '#1890ff' : '#666',
|
||||
fontWeight: score > 150 ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{score}/{record.max_score}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Time Grains',
|
||||
dataIndex: 'timeGrainCount',
|
||||
key: 'timeGrainCount',
|
||||
width: 100,
|
||||
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>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<span style={{ cursor: 'help', borderBottom: '1px dotted #999' }}>
|
||||
{count} grains
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Features',
|
||||
key: 'features',
|
||||
width: 280,
|
||||
filters: [
|
||||
{ text: 'JOINs', value: 'joins' },
|
||||
{ text: 'Subqueries', value: 'subqueries' },
|
||||
{ text: 'Dynamic Schema', value: 'dynamic_schema' },
|
||||
{ text: 'Catalog', value: 'catalog' },
|
||||
{ text: 'SSH Tunneling', value: 'ssh' },
|
||||
{ text: 'File Upload', value: 'file_upload' },
|
||||
{ text: 'Query Cancel', value: 'query_cancel' },
|
||||
{ text: 'Cost Estimation', value: 'cost_estimation' },
|
||||
{ text: 'User Impersonation', value: 'impersonation' },
|
||||
{ text: 'SQL Validation', value: 'sql_validation' },
|
||||
],
|
||||
onFilter: (value: React.Key | boolean, record: TableEntry) => {
|
||||
switch (value) {
|
||||
case 'joins':
|
||||
return Boolean(record.joins);
|
||||
case 'subqueries':
|
||||
return Boolean(record.subqueries);
|
||||
case 'dynamic_schema':
|
||||
return Boolean(record.supports_dynamic_schema);
|
||||
case 'catalog':
|
||||
return Boolean(record.supports_catalog);
|
||||
case 'ssh':
|
||||
return Boolean(record.ssh_tunneling);
|
||||
case 'file_upload':
|
||||
return Boolean(record.supports_file_upload);
|
||||
case 'query_cancel':
|
||||
return Boolean(record.query_cancelation);
|
||||
case 'cost_estimation':
|
||||
return Boolean(record.query_cost_estimation);
|
||||
case 'impersonation':
|
||||
return Boolean(record.user_impersonation);
|
||||
case 'sql_validation':
|
||||
return Boolean(record.sql_validation);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
render: (_: unknown, record: TableEntry) => (
|
||||
<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_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.sql_validation && <Tag color="lime">SQL Validation</Tag>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Documentation',
|
||||
key: 'docs',
|
||||
width: 180,
|
||||
render: (_: unknown, record: TableEntry) => (
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{record.hasConnectionString && (
|
||||
<Tag icon={<ApiOutlined />} color="default">
|
||||
Connection
|
||||
</Tag>
|
||||
)}
|
||||
{record.hasDrivers && (
|
||||
<Tag icon={<DatabaseOutlined />} color="default">
|
||||
Drivers
|
||||
</Tag>
|
||||
)}
|
||||
{record.hasAuthMethods && (
|
||||
<Tag icon={<KeyOutlined />} color="default">
|
||||
Auth
|
||||
</Tag>
|
||||
)}
|
||||
{record.hasCustomErrors && (
|
||||
<Tooltip title={`${record.customErrorCount} troubleshooting tips`}>
|
||||
<Tag icon={<BugOutlined />} color="volcano">
|
||||
Errors
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="database-index">
|
||||
{/* Statistics Cards */}
|
||||
{/* Statistics Cards */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total Databases"
|
||||
value={statistics.totalDatabases}
|
||||
prefix={<DatabaseOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="With Documentation"
|
||||
value={statistics.withDocumentation}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
suffix={`/ ${statistics.totalDatabases}`}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Multiple Drivers"
|
||||
value={statistics.withDrivers}
|
||||
prefix={<ApiOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Auth Methods"
|
||||
value={statistics.withAuthMethods}
|
||||
prefix={<KeyOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Filters */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
placeholder="Search databases..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Select
|
||||
placeholder="Filter by category"
|
||||
style={{ width: '100%' }}
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
allowClear
|
||||
options={categories.map((cat) => ({
|
||||
label: (
|
||||
<span>
|
||||
<Tag
|
||||
color={CATEGORY_COLORS[cat] || 'default'}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{categoryCounts[cat] || 0}
|
||||
</Tag>
|
||||
{cat}
|
||||
</span>
|
||||
),
|
||||
value: cat,
|
||||
}))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Database Table */}
|
||||
<Table
|
||||
dataSource={filteredDatabases}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
|
||||
pagination={{
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${total} databases`,
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabaseIndex;
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import databaseData from '../../data/databases.json';
|
||||
import type { DatabaseData } from './types';
|
||||
|
||||
const typedData = databaseData as DatabaseData;
|
||||
|
||||
const seenLogos = new Set<string>();
|
||||
const databases = Object.entries(typedData.databases)
|
||||
.filter(([, db]) => db.documentation?.logo && db.documentation?.homepage_url)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.filter(([, db]) => {
|
||||
const logo = db.documentation.logo!;
|
||||
if (seenLogos.has(logo)) return false;
|
||||
seenLogos.add(logo);
|
||||
return true;
|
||||
})
|
||||
.map(([name, db]) => ({
|
||||
name,
|
||||
logo: db.documentation.logo!,
|
||||
docPath: `/user-docs/databases/supported/${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`,
|
||||
}));
|
||||
|
||||
export default function DatabaseLogoWall(): React.JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
margin: '20px auto',
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
{databases.map(({ name, logo, docPath }) => (
|
||||
<a
|
||||
key={name}
|
||||
href={docPath}
|
||||
title={name}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', height: 40 }}
|
||||
>
|
||||
<img
|
||||
src={`/img/databases/${logo}`}
|
||||
alt={name}
|
||||
className="database-logo"
|
||||
style={{ height: 36, maxWidth: 120 }}
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,792 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Collapse,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Alert,
|
||||
Space,
|
||||
Divider,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
WarningOutlined,
|
||||
LinkOutlined,
|
||||
KeyOutlined,
|
||||
SettingOutlined,
|
||||
BookOutlined,
|
||||
EditOutlined,
|
||||
GithubOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatabaseInfo } from './types';
|
||||
|
||||
// Simple code block component for connection strings
|
||||
const CodeBlock: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<pre
|
||||
style={{
|
||||
background: 'var(--ifm-code-background)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto',
|
||||
fontSize: '13px',
|
||||
fontFamily: 'var(--ifm-font-family-monospace)',
|
||||
}}
|
||||
>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
);
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
interface DatabasePageProps {
|
||||
database: DatabaseInfo;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Feature badge component
|
||||
const FeatureBadge: React.FC<{ supported: boolean; label: string }> = ({
|
||||
supported,
|
||||
label,
|
||||
}) => (
|
||||
<Tag
|
||||
icon={supported ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
color={supported ? 'success' : 'default'}
|
||||
>
|
||||
{label}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
// Time grain badge
|
||||
const TimeGrainBadge: React.FC<{ supported: boolean; grain: string }> = ({
|
||||
supported,
|
||||
grain,
|
||||
}) => (
|
||||
<Tag color={supported ? 'blue' : 'default'} style={{ margin: '2px' }}>
|
||||
{grain}
|
||||
</Tag>
|
||||
);
|
||||
|
||||
const DatabasePage: React.FC<DatabasePageProps> = ({ database, name }) => {
|
||||
const { documentation: docs } = database;
|
||||
|
||||
// Helper to render connection string with copy button
|
||||
const renderConnectionString = (connStr: string, description?: string) => (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{description && (
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
<CodeBlock>{connStr}</CodeBlock>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Ensure db filename can be obtained regardless of how db doc gets generated
|
||||
// by either Flask app (superset.db_engine_specs.postgres) or fallback mode (postgres)
|
||||
const databaseModuleFilename = `${database.module?.split('.').pop()}.py`;
|
||||
|
||||
// Render driver information
|
||||
const renderDrivers = () => {
|
||||
if (!docs?.drivers?.length) return null;
|
||||
|
||||
return (
|
||||
<Card title="Drivers" style={{ marginBottom: 16 }}>
|
||||
<Tabs
|
||||
items={docs.drivers.map((driver, idx) => ({
|
||||
key: String(idx),
|
||||
label: (
|
||||
<span>
|
||||
{driver.name}
|
||||
{driver.is_recommended && (
|
||||
<Tag color="green" style={{ marginLeft: 8 }}>
|
||||
Recommended
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{driver.pypi_package && (
|
||||
<div>
|
||||
<Text strong>PyPI Package: </Text>
|
||||
<code>{driver.pypi_package}</code>
|
||||
</div>
|
||||
)}
|
||||
{driver.connection_string &&
|
||||
renderConnectionString(driver.connection_string)}
|
||||
{driver.notes && (
|
||||
<Alert message={driver.notes} type="info" showIcon />
|
||||
)}
|
||||
{driver.docs_url && (
|
||||
<a href={driver.docs_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Documentation
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render authentication methods
|
||||
const renderAuthMethods = () => {
|
||||
if (!docs?.authentication_methods?.length) return null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<KeyOutlined /> Authentication Methods
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Collapse
|
||||
accordion
|
||||
items={docs.authentication_methods.map((auth, idx) => ({
|
||||
key: String(idx),
|
||||
label: auth.name,
|
||||
children: (
|
||||
<>
|
||||
{auth.description && <Paragraph>{auth.description}</Paragraph>}
|
||||
{auth.requirements && (
|
||||
<Alert
|
||||
message="Requirements"
|
||||
description={auth.requirements}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{auth.connection_string &&
|
||||
renderConnectionString(
|
||||
auth.connection_string,
|
||||
'Connection String',
|
||||
)}
|
||||
{auth.secure_extra && (
|
||||
<div>
|
||||
<Text strong>Secure Extra Configuration:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.secure_extra, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.engine_parameters && (
|
||||
<div>
|
||||
<Text strong>Engine Parameters:</Text>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(auth.engine_parameters, null, 2)}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
)}
|
||||
{auth.notes && (
|
||||
<Alert message={auth.notes} type="info" showIcon />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render engine parameters
|
||||
const renderEngineParams = () => {
|
||||
if (!docs?.engine_parameters?.length) return null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<SettingOutlined /> Engine Parameters
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Collapse
|
||||
items={docs.engine_parameters.map((param, idx) => ({
|
||||
key: String(idx),
|
||||
label: param.name,
|
||||
children: (
|
||||
<>
|
||||
{param.description && (
|
||||
<Paragraph>{param.description}</Paragraph>
|
||||
)}
|
||||
{param.json && (
|
||||
<CodeBlock>{JSON.stringify(param.json, null, 2)}</CodeBlock>
|
||||
)}
|
||||
{param.docs_url && (
|
||||
<a href={param.docs_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Learn more
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render compatible databases (for PostgreSQL, etc.)
|
||||
const renderCompatibleDatabases = () => {
|
||||
if (!docs?.compatible_databases?.length) return null;
|
||||
|
||||
// Create array of all item keys to expand by default
|
||||
const allItemKeys = docs.compatible_databases.map((_, idx) => String(idx));
|
||||
|
||||
return (
|
||||
<Card title="Compatible Databases" style={{ marginBottom: 16 }}>
|
||||
<Paragraph>
|
||||
The following databases are compatible with the {name} driver:
|
||||
</Paragraph>
|
||||
<Collapse
|
||||
defaultActiveKey={allItemKeys}
|
||||
items={docs.compatible_databases.map((compat, idx) => ({
|
||||
key: String(idx),
|
||||
label: (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{compat.logo && (
|
||||
<img
|
||||
src={`/img/databases/${compat.logo}`}
|
||||
alt={compat.name}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>{compat.name}</span>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{compat.description && (
|
||||
<Paragraph>{compat.description}</Paragraph>
|
||||
)}
|
||||
{compat.connection_string &&
|
||||
renderConnectionString(compat.connection_string)}
|
||||
{compat.parameters && (
|
||||
<div>
|
||||
<Text strong>Parameters:</Text>
|
||||
<Table
|
||||
dataSource={Object.entries(compat.parameters).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
parameter: key,
|
||||
description: value,
|
||||
}),
|
||||
)}
|
||||
columns={[
|
||||
{
|
||||
title: 'Parameter',
|
||||
dataIndex: 'parameter',
|
||||
key: 'p',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'd',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{compat.notes && (
|
||||
<Alert
|
||||
message={compat.notes}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render feature matrix
|
||||
const renderFeatures = () => {
|
||||
const features: Array<{ key: keyof DatabaseInfo; label: string }> = [
|
||||
{ key: 'joins', label: 'JOINs' },
|
||||
{ key: 'subqueries', label: 'Subqueries' },
|
||||
{ key: 'supports_dynamic_schema', label: 'Dynamic Schema' },
|
||||
{ key: 'supports_catalog', label: 'Catalog Support' },
|
||||
{ key: 'supports_dynamic_catalog', label: 'Dynamic Catalog' },
|
||||
{ key: 'ssh_tunneling', label: 'SSH Tunneling' },
|
||||
{ key: 'query_cancelation', label: 'Query Cancellation' },
|
||||
{ key: 'supports_file_upload', label: 'File Upload' },
|
||||
{ key: 'user_impersonation', label: 'User Impersonation' },
|
||||
{ key: 'query_cost_estimation', label: 'Cost Estimation' },
|
||||
{ key: 'sql_validation', label: 'SQL Validation' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title="Supported Features" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{features.map(({ key, label }) => (
|
||||
<FeatureBadge
|
||||
key={key}
|
||||
supported={Boolean(database[key])}
|
||||
label={label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{database.score > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text>
|
||||
Feature Score:{' '}
|
||||
<Text strong>
|
||||
{database.score}/{database.max_score}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render time grains
|
||||
const renderTimeGrains = () => {
|
||||
if (!database.time_grains) return null;
|
||||
|
||||
const commonGrains = [
|
||||
'SECOND',
|
||||
'MINUTE',
|
||||
'HOUR',
|
||||
'DAY',
|
||||
'WEEK',
|
||||
'MONTH',
|
||||
'QUARTER',
|
||||
'YEAR',
|
||||
];
|
||||
const extendedGrains = Object.keys(database.time_grains).filter(
|
||||
g => !commonGrains.includes(g),
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title="Time Grains" style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>Common Time Grains:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{commonGrains.map(grain => (
|
||||
<TimeGrainBadge
|
||||
key={grain}
|
||||
grain={grain}
|
||||
supported={Boolean(
|
||||
database.time_grains[
|
||||
grain as keyof typeof database.time_grains
|
||||
],
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{extendedGrains.length > 0 && (
|
||||
<div>
|
||||
<Text strong>Extended Time Grains:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{extendedGrains.map(grain => (
|
||||
<TimeGrainBadge
|
||||
key={grain}
|
||||
grain={grain}
|
||||
supported={Boolean(
|
||||
database.time_grains[
|
||||
grain as keyof typeof database.time_grains
|
||||
],
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// Render troubleshooting / custom errors section
|
||||
const renderTroubleshooting = () => {
|
||||
if (!docs?.custom_errors?.length) return null;
|
||||
|
||||
// Group errors by category
|
||||
const errorsByCategory: Record<string, typeof docs.custom_errors> = {};
|
||||
for (const error of docs.custom_errors) {
|
||||
const category = error.category || 'General';
|
||||
if (!errorsByCategory[category]) {
|
||||
errorsByCategory[category] = [];
|
||||
}
|
||||
errorsByCategory[category].push(error);
|
||||
}
|
||||
|
||||
// Define category order for consistent display
|
||||
const categoryOrder = [
|
||||
'Authentication',
|
||||
'Connection',
|
||||
'Permissions',
|
||||
'Query',
|
||||
'Configuration',
|
||||
'General',
|
||||
];
|
||||
|
||||
const sortedCategories = Object.keys(errorsByCategory).sort((a, b) => {
|
||||
const aIdx = categoryOrder.indexOf(a);
|
||||
const bIdx = categoryOrder.indexOf(b);
|
||||
if (aIdx === -1 && bIdx === -1) return a.localeCompare(b);
|
||||
if (aIdx === -1) return 1;
|
||||
if (bIdx === -1) return -1;
|
||||
return aIdx - bIdx;
|
||||
});
|
||||
|
||||
// Category colors
|
||||
const categoryColors: Record<string, string> = {
|
||||
Authentication: 'orange',
|
||||
Connection: 'red',
|
||||
Permissions: 'purple',
|
||||
Query: 'blue',
|
||||
Configuration: 'cyan',
|
||||
General: 'default',
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<BugOutlined /> Troubleshooting
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Paragraph type="secondary">
|
||||
Common error messages you may encounter when connecting to or querying{' '}
|
||||
{name}, along with their causes and solutions.
|
||||
</Paragraph>
|
||||
<Collapse
|
||||
accordion
|
||||
items={sortedCategories.map(category => ({
|
||||
key: category,
|
||||
label: (
|
||||
<span>
|
||||
<Tag color={categoryColors[category] || 'default'}>
|
||||
{category}
|
||||
</Tag>
|
||||
{errorsByCategory[category].length} error
|
||||
{errorsByCategory[category].length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{errorsByCategory[category].map((error, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{
|
||||
marginBottom:
|
||||
idx < errorsByCategory[category].length - 1 ? 16 : 0,
|
||||
paddingBottom:
|
||||
idx < errorsByCategory[category].length - 1 ? 16 : 0,
|
||||
borderBottom:
|
||||
idx < errorsByCategory[category].length - 1
|
||||
? '1px solid var(--ifm-color-emphasis-200)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text strong>
|
||||
{error.description || error.error_type}
|
||||
</Text>
|
||||
</div>
|
||||
<Alert
|
||||
message={error.message_template}
|
||||
type="error"
|
||||
style={{ marginBottom: 8 }}
|
||||
/>
|
||||
{error.invalid_fields &&
|
||||
error.invalid_fields.length > 0 && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text type="secondary">Check these fields: </Text>
|
||||
{error.invalid_fields.map(field => (
|
||||
<Tag key={field} color="warning">
|
||||
{field}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{error.issue_codes && error.issue_codes.length > 0 && (
|
||||
<div>
|
||||
<Text type="secondary">Related issue codes: </Text>
|
||||
{error.issue_codes.map(code => (
|
||||
<Tag key={code}>
|
||||
<a
|
||||
href={`/docs/using-superset/issue-codes#issue-${code}`}
|
||||
style={{ color: 'inherit' }}
|
||||
>
|
||||
Issue {code}
|
||||
</a>
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="database-page" id={name.toLowerCase().replace(/\s+/g, '-')}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{docs?.logo && (
|
||||
<img
|
||||
src={`/img/databases/${docs.logo}`}
|
||||
alt={name}
|
||||
style={{
|
||||
height: 120,
|
||||
objectFit: 'contain',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Title level={1} style={{ margin: 0 }}>
|
||||
{name}
|
||||
</Title>
|
||||
{docs?.homepage_url && (
|
||||
<a
|
||||
href={docs.homepage_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ fontSize: 14 }}
|
||||
>
|
||||
<LinkOutlined /> {docs.homepage_url}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{docs?.description && <Paragraph>{docs.description}</Paragraph>}
|
||||
|
||||
{/* Warnings */}
|
||||
{docs?.warnings?.map((warning, idx) => (
|
||||
<Alert
|
||||
key={idx}
|
||||
message={warning}
|
||||
type="warning"
|
||||
icon={<WarningOutlined />}
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Known Limitations */}
|
||||
{docs?.limitations?.length > 0 && (
|
||||
<Card
|
||||
title="Known Limitations"
|
||||
style={{ marginBottom: 16 }}
|
||||
type="inner"
|
||||
>
|
||||
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
||||
{docs.limitations.map((limitation, idx) => (
|
||||
<li key={idx}>{limitation}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Installation */}
|
||||
{(docs?.pypi_packages?.length || docs?.install_instructions) && (
|
||||
<Card title="Installation" style={{ marginBottom: 16 }}>
|
||||
{docs.pypi_packages?.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>Required packages: </Text>
|
||||
{docs.pypi_packages.map(pkg => (
|
||||
<Tag key={pkg} color="blue">
|
||||
{pkg}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{docs.version_requirements && (
|
||||
<Alert
|
||||
message={`Version requirement: ${docs.version_requirements}`}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{docs.install_instructions && (
|
||||
<CodeBlock>{docs.install_instructions}</CodeBlock>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Basic Connection */}
|
||||
{docs?.connection_string && !docs?.drivers?.length && (
|
||||
<Card title="Connection String" style={{ marginBottom: 16 }}>
|
||||
{renderConnectionString(docs.connection_string)}
|
||||
{docs.parameters && (
|
||||
<Table
|
||||
dataSource={Object.entries(docs.parameters).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
parameter: key,
|
||||
description: value,
|
||||
}),
|
||||
)}
|
||||
columns={[
|
||||
{ title: 'Parameter', dataIndex: 'parameter', key: 'p' },
|
||||
{ title: 'Description', dataIndex: 'description', key: 'd' },
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{docs.default_port && (
|
||||
<Text type="secondary">Default port: {docs.default_port}</Text>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Drivers */}
|
||||
{renderDrivers()}
|
||||
|
||||
{/* Connection Examples */}
|
||||
{docs?.connection_examples?.length > 0 && (
|
||||
<Card title="Connection Examples" style={{ marginBottom: 16 }}>
|
||||
{docs.connection_examples.map((example, idx) => (
|
||||
<div key={idx}>
|
||||
{renderConnectionString(
|
||||
example.connection_string,
|
||||
example.description,
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Authentication Methods */}
|
||||
{renderAuthMethods()}
|
||||
|
||||
{/* Engine Parameters */}
|
||||
{renderEngineParams()}
|
||||
|
||||
{/* Features */}
|
||||
{renderFeatures()}
|
||||
|
||||
{/* Time Grains */}
|
||||
{renderTimeGrains()}
|
||||
|
||||
{/* Troubleshooting / Custom Errors */}
|
||||
{renderTroubleshooting()}
|
||||
|
||||
{/* Compatible Databases */}
|
||||
{renderCompatibleDatabases()}
|
||||
|
||||
{/* Notes */}
|
||||
{docs?.notes && (
|
||||
<Alert
|
||||
message="Notes"
|
||||
description={docs.notes}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* External Links */}
|
||||
{(docs?.docs_url || docs?.tutorials?.length) && (
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<BookOutlined /> Resources
|
||||
</>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Space direction="vertical">
|
||||
{docs.docs_url && (
|
||||
<a href={docs.docs_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Official Documentation
|
||||
</a>
|
||||
)}
|
||||
{docs.sqlalchemy_docs_url && (
|
||||
<a
|
||||
href={docs.sqlalchemy_docs_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<LinkOutlined /> SQLAlchemy Dialect Documentation
|
||||
</a>
|
||||
)}
|
||||
{docs.tutorials?.map((tutorial, idx) => (
|
||||
<a key={idx} href={tutorial} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> Tutorial {idx + 1}
|
||||
</a>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Edit link */}
|
||||
{database.module && (
|
||||
<Card
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
background: 'var(--ifm-background-surface-color)',
|
||||
borderStyle: 'dashed',
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Space>
|
||||
<GithubOutlined />
|
||||
<Text type="secondary">
|
||||
Help improve this documentation by editing the engine spec:
|
||||
</Text>
|
||||
<a
|
||||
href={`https://github.com/apache/superset/edit/master/superset/db_engine_specs/${databaseModuleFilename}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<EditOutlined /> Edit {databaseModuleFilename}
|
||||
</a>
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabasePage;
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { default as DatabaseIndex } from './DatabaseIndex';
|
||||
export { default as DatabasePage } from './DatabasePage';
|
||||
export * from './types';
|
||||
@@ -1,255 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TypeScript types for database documentation data
|
||||
* Generated from superset/db_engine_specs/lib.py
|
||||
*/
|
||||
|
||||
export interface Driver {
|
||||
name: string;
|
||||
pypi_package?: string;
|
||||
connection_string?: string;
|
||||
is_recommended?: boolean;
|
||||
notes?: string;
|
||||
docs_url?: string;
|
||||
default_port?: number;
|
||||
odbc_driver_paths?: Record<string, string>;
|
||||
environment_variables?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ConnectionExample {
|
||||
description: string;
|
||||
connection_string: string;
|
||||
}
|
||||
|
||||
export interface HostExample {
|
||||
platform: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface AuthenticationMethod {
|
||||
name: string;
|
||||
description?: string;
|
||||
requirements?: string;
|
||||
connection_string?: string;
|
||||
secure_extra?: Record<string, unknown>;
|
||||
secure_extra_body?: Record<string, unknown>;
|
||||
secure_extra_path?: Record<string, unknown>;
|
||||
engine_parameters?: Record<string, unknown>;
|
||||
config_example?: Record<string, unknown>;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface EngineParameter {
|
||||
name: string;
|
||||
description?: string;
|
||||
json?: Record<string, unknown>;
|
||||
secure_extra?: Record<string, unknown>;
|
||||
docs_url?: string;
|
||||
}
|
||||
|
||||
export interface SSLConfiguration {
|
||||
custom_certificate?: string;
|
||||
disable_ssl_verification?: {
|
||||
engine_params?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompatibleDatabase {
|
||||
name: string;
|
||||
description?: string;
|
||||
logo?: string;
|
||||
homepage_url?: string;
|
||||
categories?: string[]; // Category classifications (e.g., ["TRADITIONAL_RDBMS", "OPEN_SOURCE"])
|
||||
pypi_packages?: string[];
|
||||
connection_string?: string;
|
||||
parameters?: Record<string, string>;
|
||||
connection_examples?: ConnectionExample[];
|
||||
notes?: string;
|
||||
docs_url?: string;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export interface DatabaseDocumentation {
|
||||
description?: string;
|
||||
logo?: string;
|
||||
homepage_url?: string;
|
||||
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
|
||||
connection_examples?: ConnectionExample[];
|
||||
host_examples?: HostExample[];
|
||||
drivers?: Driver[];
|
||||
authentication_methods?: AuthenticationMethod[];
|
||||
engine_parameters?: EngineParameter[];
|
||||
ssl_configuration?: SSLConfiguration;
|
||||
version_requirements?: string;
|
||||
install_instructions?: string;
|
||||
warnings?: string[];
|
||||
tutorials?: string[];
|
||||
docs_url?: string;
|
||||
sqlalchemy_docs_url?: string;
|
||||
advanced_features?: Record<string, string>;
|
||||
compatible_databases?: CompatibleDatabase[];
|
||||
custom_errors?: CustomError[]; // Database-specific error messages and troubleshooting info
|
||||
}
|
||||
|
||||
export interface TimeGrains {
|
||||
SECOND?: boolean;
|
||||
MINUTE?: boolean;
|
||||
HOUR?: boolean;
|
||||
DAY?: boolean;
|
||||
WEEK?: boolean;
|
||||
MONTH?: boolean;
|
||||
QUARTER?: boolean;
|
||||
YEAR?: boolean;
|
||||
FIVE_SECONDS?: boolean;
|
||||
THIRTY_SECONDS?: boolean;
|
||||
FIVE_MINUTES?: boolean;
|
||||
TEN_MINUTES?: boolean;
|
||||
FIFTEEN_MINUTES?: boolean;
|
||||
THIRTY_MINUTES?: boolean;
|
||||
HALF_HOUR?: boolean;
|
||||
SIX_HOURS?: boolean;
|
||||
WEEK_STARTING_SUNDAY?: boolean;
|
||||
WEEK_STARTING_MONDAY?: boolean;
|
||||
WEEK_ENDING_SATURDAY?: boolean;
|
||||
WEEK_ENDING_SUNDAY?: boolean;
|
||||
QUARTER_YEAR?: boolean;
|
||||
}
|
||||
|
||||
export interface DatabaseInfo {
|
||||
engine: string;
|
||||
engine_name: string;
|
||||
engine_aliases?: string[];
|
||||
default_driver?: string;
|
||||
module?: string;
|
||||
documentation: DatabaseDocumentation;
|
||||
|
||||
// Diagnostics from lib.py diagnose() function
|
||||
time_grains: TimeGrains;
|
||||
score: number;
|
||||
max_score: number;
|
||||
|
||||
// SQL capabilities
|
||||
joins: boolean;
|
||||
subqueries: boolean;
|
||||
alias_in_select?: boolean;
|
||||
alias_in_orderby?: boolean;
|
||||
cte_in_subquery?: boolean;
|
||||
sql_comments?: boolean;
|
||||
escaped_colons?: boolean;
|
||||
time_groupby_inline?: boolean;
|
||||
alias_to_source_column?: boolean;
|
||||
order_by_not_in_select?: boolean;
|
||||
expressions_in_orderby?: boolean;
|
||||
|
||||
// Platform features
|
||||
limit_method?: string;
|
||||
limit_clause?: boolean;
|
||||
max_column_name?: number;
|
||||
supports_file_upload?: boolean;
|
||||
supports_dynamic_schema?: boolean;
|
||||
supports_catalog?: boolean;
|
||||
supports_dynamic_catalog?: boolean;
|
||||
|
||||
// Advanced features
|
||||
user_impersonation?: boolean;
|
||||
ssh_tunneling?: boolean;
|
||||
query_cancelation?: boolean;
|
||||
expand_data?: boolean;
|
||||
query_cost_estimation?: boolean;
|
||||
sql_validation?: boolean;
|
||||
get_metrics?: boolean;
|
||||
where_latest_partition?: boolean;
|
||||
get_extra_table_metadata?: boolean;
|
||||
dbapi_exception_mapping?: boolean;
|
||||
custom_errors?: boolean;
|
||||
masked_encrypted_extra?: boolean;
|
||||
column_type_mapping?: boolean;
|
||||
function_names?: boolean;
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
totalDatabases: number;
|
||||
withDocumentation: number;
|
||||
withConnectionString: number;
|
||||
withDrivers: number;
|
||||
withAuthMethods: number;
|
||||
supportsJoins: number;
|
||||
supportsSubqueries: number;
|
||||
supportsDynamicSchema: number;
|
||||
supportsCatalog: number;
|
||||
averageScore: number;
|
||||
maxScore: number;
|
||||
byCategory: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface DatabaseData {
|
||||
generated: string;
|
||||
statistics: Statistics;
|
||||
databases: Record<string, DatabaseInfo>;
|
||||
}
|
||||
|
||||
// Helper type for sorting databases
|
||||
export type SortField = 'name' | 'score' | 'category';
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
|
||||
// Helper to get common time grains
|
||||
export const COMMON_TIME_GRAINS = [
|
||||
'SECOND',
|
||||
'MINUTE',
|
||||
'HOUR',
|
||||
'DAY',
|
||||
'WEEK',
|
||||
'MONTH',
|
||||
'QUARTER',
|
||||
'YEAR',
|
||||
] as const;
|
||||
|
||||
export const EXTENDED_TIME_GRAINS = [
|
||||
'FIVE_SECONDS',
|
||||
'THIRTY_SECONDS',
|
||||
'FIVE_MINUTES',
|
||||
'TEN_MINUTES',
|
||||
'FIFTEEN_MINUTES',
|
||||
'THIRTY_MINUTES',
|
||||
'HALF_HOUR',
|
||||
'SIX_HOURS',
|
||||
'WEEK_STARTING_SUNDAY',
|
||||
'WEEK_STARTING_MONDAY',
|
||||
'WEEK_ENDING_SATURDAY',
|
||||
'WEEK_ENDING_SUNDAY',
|
||||
'QUARTER_YEAR',
|
||||
] as const;
|
||||
@@ -1,262 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, Tag, Input, Select } from 'antd';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ApiOutlined,
|
||||
PictureOutlined,
|
||||
PlayCircleOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ComponentData, ComponentEntry } from './types';
|
||||
|
||||
interface ComponentIndexProps {
|
||||
data: ComponentData;
|
||||
}
|
||||
|
||||
const CATEGORY_COLORS: Record<string, string> = {
|
||||
ui: 'blue',
|
||||
'design-system': 'green',
|
||||
extension: 'purple',
|
||||
};
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
ui: 'Core',
|
||||
'design-system': 'Layout',
|
||||
extension: 'Extension',
|
||||
};
|
||||
|
||||
const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
||||
|
||||
const { statistics, components } = data;
|
||||
|
||||
const filteredComponents = useMemo(() => {
|
||||
return components
|
||||
.filter((comp) => {
|
||||
const matchesSearch =
|
||||
!searchText ||
|
||||
comp.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
comp.description.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
comp.package.toLowerCase().includes(searchText.toLowerCase());
|
||||
const matchesCategory =
|
||||
!categoryFilter || comp.category === categoryFilter;
|
||||
return matchesSearch && matchesCategory;
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [components, searchText, categoryFilter]);
|
||||
|
||||
const { categories, categoryCounts } = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
components.forEach((comp) => {
|
||||
counts[comp.category] = (counts[comp.category] || 0) + 1;
|
||||
});
|
||||
return {
|
||||
categories: Object.keys(counts).sort(),
|
||||
categoryCounts: counts,
|
||||
};
|
||||
}, [components]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Component',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: (a: ComponentEntry, b: ComponentEntry) =>
|
||||
a.name.localeCompare(b.name),
|
||||
defaultSortOrder: 'ascend' as const,
|
||||
render: (name: string, record: ComponentEntry) => (
|
||||
<div>
|
||||
<a href={`/${record.docPath}`}>
|
||||
<strong>{name}</strong>
|
||||
</a>
|
||||
{record.description && (
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{record.description.slice(0, 100)}
|
||||
{record.description.length > 100 ? '...' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Category',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
filters: categories.map((cat) => ({
|
||||
text: CATEGORY_LABELS[cat] || cat,
|
||||
value: cat,
|
||||
})),
|
||||
onFilter: (value: React.Key | boolean, record: ComponentEntry) =>
|
||||
record.category === value,
|
||||
render: (cat: string) => (
|
||||
<Tag color={CATEGORY_COLORS[cat] || 'default'}>
|
||||
{CATEGORY_LABELS[cat] || cat}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Package',
|
||||
dataIndex: 'package',
|
||||
key: 'package',
|
||||
width: 220,
|
||||
render: (pkg: string) => (
|
||||
<code style={{ fontSize: '12px' }}>{pkg}</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
key: 'tags',
|
||||
width: 280,
|
||||
filters: [
|
||||
{ text: 'Extension Compatible', value: 'extension' },
|
||||
{ text: 'Gallery', value: 'gallery' },
|
||||
{ text: 'Live Demo', value: 'demo' },
|
||||
],
|
||||
onFilter: (value: React.Key | boolean, record: ComponentEntry) => {
|
||||
switch (value) {
|
||||
case 'extension':
|
||||
return record.extensionCompatible;
|
||||
case 'gallery':
|
||||
return record.hasGallery;
|
||||
case 'demo':
|
||||
return record.hasLiveExample;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
render: (_: unknown, record: ComponentEntry) => (
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{record.extensionCompatible && (
|
||||
<Tag color="purple">Extension Compatible</Tag>
|
||||
)}
|
||||
{record.hasGallery && <Tag color="cyan">Gallery</Tag>}
|
||||
{record.hasLiveExample && <Tag color="green">Demo</Tag>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Props',
|
||||
dataIndex: 'propsCount',
|
||||
key: 'propsCount',
|
||||
width: 80,
|
||||
sorter: (a: ComponentEntry, b: ComponentEntry) =>
|
||||
a.propsCount - b.propsCount,
|
||||
render: (count: number) => (
|
||||
<span style={{ color: count > 0 ? '#1890ff' : '#999' }}>{count}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="component-index">
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total Components"
|
||||
value={statistics.totalComponents}
|
||||
prefix={<AppstoreOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Extension Compatible"
|
||||
value={statistics.extensionCompatible}
|
||||
prefix={<ApiOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="With Gallery"
|
||||
value={statistics.withGallery}
|
||||
prefix={<PictureOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="With Live Demo"
|
||||
value={statistics.withLiveExample}
|
||||
prefix={<PlayCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Input
|
||||
placeholder="Search components..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<Select
|
||||
placeholder="Filter by category"
|
||||
style={{ width: '100%' }}
|
||||
value={categoryFilter}
|
||||
onChange={setCategoryFilter}
|
||||
allowClear
|
||||
options={categories.map((cat) => ({
|
||||
label: (
|
||||
<span>
|
||||
<Tag
|
||||
color={CATEGORY_COLORS[cat] || 'default'}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
{categoryCounts[cat] || 0}
|
||||
</Tag>
|
||||
{CATEGORY_LABELS[cat] || cat}
|
||||
</span>
|
||||
),
|
||||
value: cat,
|
||||
}))}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Table
|
||||
dataSource={filteredComponents}
|
||||
columns={columns}
|
||||
rowKey="name"
|
||||
pagination={{
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${total} components`,
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentIndex;
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { default as ComponentIndex } from './ComponentIndex';
|
||||
export * from './types';
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export interface ComponentEntry {
|
||||
name: string;
|
||||
category: string;
|
||||
categoryLabel: string;
|
||||
description: string;
|
||||
importPath: string;
|
||||
package: string;
|
||||
extensionCompatible: boolean;
|
||||
propsCount: number;
|
||||
controlsCount: number;
|
||||
hasGallery: boolean;
|
||||
hasLiveExample: boolean;
|
||||
docPath: string;
|
||||
storyFile: string;
|
||||
}
|
||||
|
||||
export interface ComponentStatistics {
|
||||
totalComponents: number;
|
||||
byCategory: Record<string, number>;
|
||||
extensionCompatible: number;
|
||||
withGallery: number;
|
||||
withLiveExample: number;
|
||||
}
|
||||
|
||||
export interface ComponentData {
|
||||
generated: string;
|
||||
statistics: ComponentStatistics;
|
||||
components: ComponentEntry[];
|
||||
}
|
||||
Reference in New Issue
Block a user