diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index e80e58a00a0..3070e8e4092 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -117,7 +117,6 @@ "react-search-input": "^0.11.3", "react-sortable-hoc": "^2.0.0", "react-split": "^2.0.9", - "react-syntax-highlighter": "^15.4.5", "react-table": "^7.8.0", "react-transition-group": "^4.4.5", "react-virtualized-auto-sizer": "^1.0.25", @@ -194,7 +193,6 @@ "@types/react-redux": "^7.1.10", "@types/react-resizable": "^3.0.8", "@types/react-router-dom": "^5.3.3", - "@types/react-syntax-highlighter": "^15.5.13", "@types/react-transition-group": "^4.4.12", "@types/react-ultimate-pagination": "^1.2.4", "@types/react-virtualized-auto-sizer": "^1.0.4", @@ -57915,6 +57913,7 @@ "react-js-cron": "^5.2.0", "react-markdown": "^8.0.7", "react-resize-detector": "^7.1.2", + "react-syntax-highlighter": "^15.4.5", "react-ultimate-pagination": "^1.3.2", "regenerator-runtime": "^0.14.1", "rehype-raw": "^7.0.0", @@ -57937,6 +57936,7 @@ "@types/math-expression-evaluator": "^1.3.3", "@types/node": "^22.10.3", "@types/prop-types": "^15.7.2", + "@types/react-syntax-highlighter": "^15.5.13", "@types/react-table": "^7.7.20", "@types/rison": "0.1.0", "@types/seedrandom": "^3.0.8", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 6d4ba6fb26a..6745fbe7802 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -185,7 +185,6 @@ "react-search-input": "^0.11.3", "react-sortable-hoc": "^2.0.0", "react-split": "^2.0.9", - "react-syntax-highlighter": "^15.4.5", "react-table": "^7.8.0", "react-transition-group": "^4.4.5", "react-virtualized-auto-sizer": "^1.0.25", @@ -262,7 +261,6 @@ "@types/react-redux": "^7.1.10", "@types/react-resizable": "^3.0.8", "@types/react-router-dom": "^5.3.3", - "@types/react-syntax-highlighter": "^15.5.13", "@types/react-transition-group": "^4.4.12", "@types/react-ultimate-pagination": "^1.2.4", "@types/react-virtualized-auto-sizer": "^1.0.4", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 492130b2e82..e91b53e86fe 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -51,6 +51,7 @@ "react-js-cron": "^5.2.0", "react-draggable": "^4.4.6", "react-resize-detector": "^7.1.2", + "react-syntax-highlighter": "^15.4.5", "react-ultimate-pagination": "^1.3.2", "react-error-boundary": "^5.0.0", "react-markdown": "^8.0.7", @@ -72,6 +73,7 @@ "@types/d3-time": "^3.0.4", "@types/d3-time-format": "^4.0.3", "@types/react-table": "^7.7.20", + "@types/react-syntax-highlighter": "^15.5.13", "@types/jquery": "^3.5.8", "@types/lodash": "^4.17.20", "@types/math-expression-evaluator": "^1.3.3", diff --git a/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/CodeSyntaxHighlighter.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/CodeSyntaxHighlighter.stories.tsx new file mode 100644 index 00000000000..e3308e0c485 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/CodeSyntaxHighlighter.stories.tsx @@ -0,0 +1,320 @@ +/** + * 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 { Typography, Flex, Space } from '@superset-ui/core/components'; +import CodeSyntaxHighlighter from '.'; +import type { CodeSyntaxHighlighterProps, SupportedLanguage } from '.'; + +const { Title, Text, Paragraph } = Typography; + +const languages: SupportedLanguage[] = ['sql', 'json', 'htmlbars', 'markdown']; + +// Sample code for each language +const sampleCode = { + sql: `-- Complex SQL Query Example +SELECT + u.id, + u.username, + u.email, + COUNT(o.id) as total_orders, + SUM(o.amount) as total_spent, + AVG(o.amount) as avg_order_value +FROM users u +LEFT JOIN orders o ON u.id = o.user_id +WHERE u.created_at >= '2023-01-01' + AND u.status = 'active' +GROUP BY u.id, u.username, u.email +HAVING COUNT(o.id) > 0 +ORDER BY total_spent DESC, total_orders DESC +LIMIT 50;`, + + json: `{ + "user": { + "id": 12345, + "username": "john_doe", + "email": "john@example.com", + "profile": { + "firstName": "John", + "lastName": "Doe", + "age": 30, + "preferences": { + "theme": "dark", + "language": "en", + "notifications": true + } + }, + "orders": [ + { + "id": "order_001", + "amount": 99.99, + "status": "completed", + "items": ["laptop", "mouse"] + }, + { + "id": "order_002", + "amount": 49.99, + "status": "pending", + "items": ["keyboard"] + } + ] + } +}`, + + htmlbars: `{{!-- Handlebars Template Example --}} +
+

Welcome, {{user.firstName}} {{user.lastName}}!

+ + {{#if user.orders}} +
+

Your Orders ({{user.orders.length}})

+ + {{#each user.orders}} +
+

Order #{{id}}

+

\${{amount}}

+

Status: {{capitalize status}}

+ + {{#if items}} +
    + {{#each items}} +
  • {{this}}
  • + {{/each}} +
+ {{/if}} +
+ {{/each}} +
+ {{else}} +

No orders found.

+ {{/if}} +
`, + + markdown: `# CodeSyntaxHighlighter Component + +A **themed syntax highlighter** for Superset that supports multiple languages and automatic theme switching. + +## Features + +- 🎨 **Automatic theming** - Adapts to light/dark modes +- ⚡ **Lazy loading** - Languages load on-demand for better performance +- 🔧 **TypeScript support** - Full type safety +- 📱 **Responsive** - Works on all screen sizes + +## Supported Languages + +| Language | Extension | Use Case | +|----------|-----------|----------| +| SQL | \`.sql\` | Database queries | +| JSON | \`.json\` | Data interchange | +| HTML/Handlebars | \`.hbs\` | Templates | +| Markdown | \`.md\` | Documentation | + +## Usage + +\`\`\`typescript +import CodeSyntaxHighlighter from '@superset-ui/core/components/CodeSyntaxHighlighter'; + + + SELECT * FROM users WHERE active = true; + +\`\`\` + +> **Note**: Languages are loaded lazily for optimal performance!`, +}; + +export default { + title: 'Components/CodeSyntaxHighlighter', + component: CodeSyntaxHighlighter, + parameters: { + docs: { + description: { + component: + "A themed syntax highlighter component that automatically adapts to Superset's light/dark themes and supports lazy loading of languages.", + }, + }, + }, +}; + +// Gallery showing all supported languages +export const LanguageGallery = () => ( + + {languages.map(language => ( +
+ + {language.toUpperCase()} Example + + + {sampleCode[language]} + +
+ ))} +
+); + +// Interactive playground +export const InteractivePlayground = (args: CodeSyntaxHighlighterProps) => ( + + {args.children || sampleCode[args.language || 'sql']} + +); + +InteractivePlayground.args = { + language: 'sql', + showLineNumbers: false, + wrapLines: true, + children: sampleCode.sql, +}; + +InteractivePlayground.argTypes = { + language: { + control: { type: 'select' }, + options: languages, + description: 'Programming language for syntax highlighting', + }, + showLineNumbers: { + control: { type: 'boolean' }, + description: 'Display line numbers alongside the code', + }, + wrapLines: { + control: { type: 'boolean' }, + description: 'Wrap long lines instead of showing horizontal scroll', + }, + children: { + control: { type: 'text' }, + description: 'Code content to highlight', + }, + customStyle: { + control: { type: 'object' }, + description: 'Custom CSS styles to apply to the syntax highlighter', + }, +}; + +// Showcase different styling options +export const StylingExamples = () => ( + + {/* Default styling */} +
+ Default Styling + + SELECT id, name FROM users WHERE active = true; + +
+ + {/* With line numbers */} +
+ With Line Numbers + + {sampleCode.sql} + +
+ + {/* Custom styling */} +
+ Custom Styling (Compact) + + {sampleCode.json} + +
+ + {/* No line wrapping */} +
+ No Line Wrapping + + {`SELECT very_long_column_name, another_very_long_column_name, yet_another_extremely_long_column_name FROM very_long_table_name WHERE condition = 'this is a very long condition';`} + +
+
+); + +// Performance and edge cases +export const EdgeCases = () => ( + + {/* Very long single line */} +
+ Very Long Single Line + + {`SELECT ${'very_long_column_name, '.repeat(20)}id FROM users;`} + +
+ + {/* Special characters */} +
+ Special Characters & Escaping + + {`SELECT * FROM "table-name" WHERE field = 'O\\'Brien' AND data = '{"key": "value"}';`} + +
+ + {/* Multiple languages showcase */} +
+ Quick Language Comparison + +
+ SQL + + SELECT id, name FROM users; + +
+
+ JSON + + {`{"users": [{"id": 1, "name": "John"}]}`} + +
+
+
+
+); + +// Theme testing helper +export const ThemeShowcase = () => ( + + + Theme Testing: Switch between light and dark themes in + Storybook to see automatic adaptation. + + + + {languages.map(language => ( +
+ + {language} + + + {sampleCode[language].split('\n').slice(0, 5).join('\n')} + +
+ ))} +
+
+); diff --git a/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.test.tsx new file mode 100644 index 00000000000..f4c04a90069 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.test.tsx @@ -0,0 +1,156 @@ +/** + * 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 { render, screen } from '../../spec'; +import CodeSyntaxHighlighter from './index'; + +// Simple mock that just returns the content +jest.mock( + 'react-syntax-highlighter/dist/cjs/light', + () => + function MockSyntaxHighlighter({ children, ...props }: any) { + return ( +
+          {children}
+        
+ ); + }, +); + +// Mock the language modules +jest.mock( + 'react-syntax-highlighter/dist/cjs/languages/hljs/sql', + () => 'sql-mock', +); +jest.mock( + 'react-syntax-highlighter/dist/cjs/languages/hljs/json', + () => 'json-mock', +); +jest.mock( + 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars', + () => 'html-mock', +); +jest.mock( + 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown', + () => 'md-mock', +); + +// Mock the styles +jest.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({})); +jest.mock( + 'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark', + () => ({}), +); + +describe('CodeSyntaxHighlighter', () => { + it('renders code content', () => { + render(SELECT * FROM users;); + + expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument(); + }); + + it('renders with default SQL language', () => { + render(SELECT * FROM users;); + + // Should show content (the important thing is content is visible) + expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument(); + }); + + it('renders with specified language', () => { + render( + + {`{ "key": "value" }`} + , + ); + + // Should show content regardless of which element renders it + expect(screen.getByText('{ "key": "value" }')).toBeInTheDocument(); + }); + + it('supports all expected languages', () => { + const languages = ['sql', 'json', 'htmlbars', 'markdown'] as const; + + languages.forEach(language => { + const { unmount } = render( + + {`Test content for ${language}`} + , + ); + + // Should render the content (either in fallback or syntax highlighter) + expect( + screen.getByText(`Test content for ${language}`), + ).toBeInTheDocument(); + + unmount(); + }); + }); + + it('renders fallback pre element initially', () => { + render( + + SELECT COUNT(*) FROM table; + , + ); + + // Should render the content in some form + expect(screen.getByText('SELECT COUNT(*) FROM table;')).toBeInTheDocument(); + }); + + it('handles special characters', () => { + const specialContent = "SELECT * FROM `users` WHERE name = 'O\\'Brien';"; + + render( + + {specialContent} + , + ); + + expect(screen.getByText(specialContent)).toBeInTheDocument(); + }); + + it('accepts custom styles', () => { + render( + + SELECT * FROM users; + , + ); + + expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument(); + }); + + it('accepts showLineNumbers prop', () => { + render( + + SELECT * FROM users; + , + ); + + expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument(); + }); + + it('accepts wrapLines prop', () => { + render( + + SELECT * FROM users; + , + ); + + expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument(); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.tsx new file mode 100644 index 00000000000..b52ba362543 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/CodeSyntaxHighlighter/index.tsx @@ -0,0 +1,149 @@ +/** + * 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 { useEffect, useState } from 'react'; +import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; +import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night'; +import { themeObject } from '@superset-ui/core'; + +export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json'; + +export interface CodeSyntaxHighlighterProps { + children: string; + language?: SupportedLanguage; + customStyle?: React.CSSProperties; + showLineNumbers?: boolean; + wrapLines?: boolean; + style?: any; // Override theme style if needed +} + +// Track which languages have been registered to avoid duplicate registrations +const registeredLanguages = new Set(); + +// Language import functions - these will be called lazily +const languageImporters = { + sql: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/sql'), + htmlbars: () => + import('react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'), + markdown: () => + import('react-syntax-highlighter/dist/cjs/languages/hljs/markdown'), + json: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/json'), +}; + +/** + * Lazily register a language for syntax highlighting + */ +const registerLanguage = async (language: SupportedLanguage): Promise => { + if (registeredLanguages.has(language)) { + return; // Already registered + } + + try { + const languageModule = await languageImporters[language](); + SyntaxHighlighterBase.registerLanguage(language, languageModule.default); + registeredLanguages.add(language); + } catch (error) { + console.warn(`Failed to load language ${language}:`, error); + } +}; + +/** + * A themed syntax highlighter component that automatically adapts to Superset's current theme. + * Supports light/dark mode switching and provides consistent styling across the application. + * Languages are loaded lazily to improve initial page load performance. + * Uses ultra-neutral themes for professional, consistent appearance. + */ +export const CodeSyntaxHighlighter: React.FC = ({ + children, + language = 'sql', + customStyle = {}, + showLineNumbers = false, + wrapLines = true, + style: overrideStyle, +}) => { + const [isLanguageReady, setIsLanguageReady] = useState( + registeredLanguages.has(language), + ); + + useEffect(() => { + const loadLanguage = async () => { + if (!registeredLanguages.has(language)) { + await registerLanguage(language); + setIsLanguageReady(true); + } + }; + + loadLanguage(); + }, [language]); + + const isDark = themeObject.isThemeDark(); + const themeStyle = overrideStyle || (isDark ? tomorrow : github); + + const defaultCustomStyle: React.CSSProperties = { + background: themeObject.theme.colorBgElevated, + padding: themeObject.theme.sizeUnit * 4, + border: 0, + borderRadius: themeObject.theme.borderRadius, + ...customStyle, + }; + + // Show a simple pre-formatted text while language is loading + if (!isLanguageReady) { + return ( +
+        {children}
+      
+ ); + } + + return ( + + {children} + + ); +}; + +/** + * Utility function to preload specific languages if needed + * This can be called strategically in components that know they'll need certain languages + */ +export const preloadLanguages = async ( + languages: SupportedLanguage[], +): Promise => { + const promises = languages + .filter(lang => !registeredLanguages.has(lang)) + .map(registerLanguage); + + await Promise.all(promises); +}; + +export default CodeSyntaxHighlighter; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Modal/types.ts b/superset-frontend/packages/superset-ui-core/src/components/Modal/types.ts index f111eab60e6..38447fd0a3b 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Modal/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/Modal/types.ts @@ -34,7 +34,7 @@ export interface ModalProps { show: boolean; name?: string; title: ReactNode; - width?: string; + width?: string | number; maxWidth?: string; responsive?: boolean; hideFooter?: boolean; diff --git a/superset-frontend/src/types/react-syntax-highlighter.d.ts b/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts similarity index 100% rename from superset-frontend/src/types/react-syntax-highlighter.d.ts rename to superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts diff --git a/superset-frontend/src/SqlLab/components/HighlightedSql/index.tsx b/superset-frontend/src/SqlLab/components/HighlightedSql/index.tsx index 2733b4ae81e..b0c864927bc 100644 --- a/superset-frontend/src/SqlLab/components/HighlightedSql/index.tsx +++ b/superset-frontend/src/SqlLab/components/HighlightedSql/index.tsx @@ -16,34 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light'; -import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; -import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; -import atomOneDark from 'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark'; -import { t, themeObject } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ModalTrigger } from '@superset-ui/core/components'; - -SyntaxHighlighterBase.registerLanguage('sql', sql); - -const ThemedSyntaxHighlighter = ({ - children, - style, -}: { - children: string; - style: any; -}) => ( - - {children} - -); +import CodeSyntaxHighlighter from '@superset-ui/core/components/CodeSyntaxHighlighter'; export interface HighlightedSqlProps { sql: string; @@ -56,7 +31,6 @@ export interface HighlightedSqlProps { interface HighlightedSqlModalTypes { rawSql?: string; sql: string; - syntaxTheme: any; } interface TriggerNodeProps { @@ -64,7 +38,6 @@ interface TriggerNodeProps { sql: string; maxLines: number; maxWidth: number; - syntaxTheme: any; } const shrinkSql = (sql: string, maxLines: number, maxWidth: number) => { @@ -81,37 +54,23 @@ const shrinkSql = (sql: string, maxLines: number, maxWidth: number) => { .join('\n'); }; -function TriggerNode({ - shrink, - sql, - maxLines, - maxWidth, - syntaxTheme, -}: TriggerNodeProps) { +function TriggerNode({ shrink, sql, maxLines, maxWidth }: TriggerNodeProps) { return ( - + {shrink ? shrinkSql(sql, maxLines, maxWidth) : sql} - + ); } -function HighlightSqlModal({ - rawSql, - sql, - syntaxTheme, -}: HighlightedSqlModalTypes) { +function HighlightSqlModal({ rawSql, sql }: HighlightedSqlModalTypes) { return (

{t('Source SQL')}

- - {sql} - + {sql} {rawSql && rawSql !== sql && (

{t('Executed SQL')}

- - {rawSql} - + {rawSql}
)}
@@ -125,26 +84,16 @@ function HighlightedSql({ maxLines = 5, shrink = false, }: HighlightedSqlProps) { - const isDark = themeObject.isThemeDark(); - const syntaxTheme = isDark ? atomOneDark : github; - return ( - } + modalBody={} triggerNode={ } /> diff --git a/superset-frontend/src/SqlLab/components/ShowSQL/index.tsx b/superset-frontend/src/SqlLab/components/ShowSQL/index.tsx index a9739faacee..267f735c507 100644 --- a/superset-frontend/src/SqlLab/components/ShowSQL/index.tsx +++ b/superset-frontend/src/SqlLab/components/ShowSQL/index.tsx @@ -16,13 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; -import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; -import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; +import { useEffect } from 'react'; import { IconTooltip, ModalTrigger } from '@superset-ui/core/components'; import { Icons } from '@superset-ui/core/components/Icons'; - -SyntaxHighlighter.registerLanguage('sql', sql); +import CodeSyntaxHighlighter, { + preloadLanguages, +} from '@superset-ui/core/components/CodeSyntaxHighlighter'; interface ShowSQLProps { sql: string; @@ -37,6 +36,11 @@ export default function ShowSQL({ sql: sqlString, triggerNode, }: ShowSQLProps) { + // Preload SQL language since this component will definitely use it when modal opens + useEffect(() => { + preloadLanguages(['sql']); + }, []); + return ( - + {sqlString} - + } /> diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.tsx index da5e7d04546..dca607e8461 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.tsx @@ -26,15 +26,13 @@ import { } from 'react'; import rison from 'rison'; import { styled, SupersetClient, t } from '@superset-ui/core'; -import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; -import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import { Icons, Switch, Button, Skeleton } from '@superset-ui/core/components'; import { CopyToClipboard } from 'src/components'; import { CopyButton } from 'src/explore/components/DataTableControl'; -import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown'; -import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'; -import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; -import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'; +import CodeSyntaxHighlighter, { + SupportedLanguage, + preloadLanguages, +} from '@superset-ui/core/components/CodeSyntaxHighlighter'; import { useHistory } from 'react-router-dom'; const CopyButtonViewQuery = styled(CopyButton)` @@ -45,15 +43,10 @@ const CopyButtonViewQuery = styled(CopyButton)` `} `; -SyntaxHighlighter.registerLanguage('markdown', markdownSyntax); -SyntaxHighlighter.registerLanguage('html', htmlSyntax); -SyntaxHighlighter.registerLanguage('sql', sqlSyntax); -SyntaxHighlighter.registerLanguage('json', jsonSyntax); - export interface ViewQueryProps { sql: string; datasource: string; - language?: string; + language?: SupportedLanguage; } const StyledSyntaxContainer = styled.div` @@ -76,7 +69,7 @@ const StyledHeaderActionContainer = styled.div` column-gap: ${({ theme }) => theme.sizeUnit * 2}px; `; -const StyledSyntaxHighlighter = styled(SyntaxHighlighter)` +const StyledThemedSyntaxHighlighter = styled(CodeSyntaxHighlighter)` flex: 1; `; @@ -97,6 +90,11 @@ const ViewQuery: FC = props => { const history = useHistory(); const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql; + // Preload the language when component mounts to ensure smooth experience + useEffect(() => { + preloadLanguages([language]); + }, [language]); + const formatCurrentQuery = useCallback(() => { if (formattedSQL) { setShowFormatSQL(val => !val); @@ -179,9 +177,12 @@ const ViewQuery: FC = props => { {!formattedSQL && } {formattedSQL && ( - + {currentSQL} - + )} ); diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx index 5eb8688dd74..061ef2adc2c 100644 --- a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx @@ -91,7 +91,7 @@ const ViewQueryModal: FC = ({ latestQueryFormData }) => { ) : null, )} diff --git a/superset-frontend/src/features/home/SavedQueries.tsx b/superset-frontend/src/features/home/SavedQueries.tsx index 0ddf1e8ab27..53dbb7220f4 100644 --- a/superset-frontend/src/features/home/SavedQueries.tsx +++ b/superset-frontend/src/features/home/SavedQueries.tsx @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { styled, SupersetClient, t, useTheme, css } from '@superset-ui/core'; -import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; -import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; -import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; +import CodeSyntaxHighlighter, { + preloadLanguages, +} from '@superset-ui/core/components/CodeSyntaxHighlighter'; import { LoadingCards } from 'src/pages/Home'; import { TableTab } from 'src/views/CRUD/types'; import withToasts from 'src/components/MessageToasts/withToasts'; @@ -49,8 +49,6 @@ import SubMenu from './SubMenu'; import EmptyState from './EmptyState'; import { WelcomeTable } from './types'; -SyntaxHighlighter.registerLanguage('sql', sql); - interface Query { id?: number; sql_tables?: Array; @@ -110,13 +108,20 @@ const QueryData = styled.div` `; const QueryContainer = styled.div` - pre { + /* Custom styles for the syntax highlighter in cards */ + & > div { height: ${({ theme }) => theme.sizeUnit * 40}px; border: none !important; - background-color: ${({ theme }) => - theme.colors.grayscale.light5} !important; - overflow: hidden; - padding: ${({ theme }) => theme.sizeUnit * 4}px !important; + overflow: hidden !important; + + pre { + height: 100%; + margin: 0; + border: none; + overflow: hidden; + word-break: break-all; + white-space: pre-wrap; + } } `; @@ -151,6 +156,13 @@ export const SavedQueries = ({ const theme = useTheme(); + // Preload SQL language since we'll likely show SQL snippets + useEffect(() => { + if (showThumbnails && featureFlag) { + preloadLanguages(['sql']); + } + }, [showThumbnails, featureFlag]); + const handleQueryDelete = ({ id, label }: Query) => { SupersetClient.delete({ endpoint: `/api/v1/saved_query/${id}`, @@ -315,24 +327,21 @@ export const SavedQueries = ({ cover={ q?.sql?.length && showThumbnails && featureFlag ? ( - {shortenSQL(q.sql, 25)} - + ) : showThumbnails && !q?.sql?.length ? ( false diff --git a/superset-frontend/src/features/queries/QueryPreviewModal.tsx b/superset-frontend/src/features/queries/QueryPreviewModal.tsx index 39ab18cfb70..f4f9e1cf35a 100644 --- a/superset-frontend/src/features/queries/QueryPreviewModal.tsx +++ b/superset-frontend/src/features/queries/QueryPreviewModal.tsx @@ -68,14 +68,6 @@ const StyledModal = styled(Modal)` .ant-modal-body { padding: ${({ theme }) => theme.sizeUnit * 6}px; } - - pre { - font-size: ${({ theme }) => theme.fontSizeXS}px; - font-weight: ${({ theme }) => theme.fontWeightNormal}; - line-height: ${({ theme }) => theme.fontSizeLG}px; - height: 375px; - border: none; - } `; interface QueryPreviewModalProps extends ToastProps { diff --git a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx index e4a9388a1f5..ce888945e0c 100644 --- a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx +++ b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx @@ -41,14 +41,6 @@ const StyledModal = styled(Modal)` .ant-modal-body { padding: 24px; } - - pre { - font-size: ${({ theme }) => theme.fontSizeXS}px; - font-weight: ${({ theme }) => theme.fontWeightNormal}; - line-height: ${({ theme }) => theme.fontSizeLG}px; - height: 375px; - border: none; - } `; type SavedQueryObject = { @@ -91,6 +83,7 @@ const SavedQueryPreviewModal: FunctionComponent< onHide={onHide} show={show} title={t('Query preview')} + width={800} footer={ <>