This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch what-if in repository https://gitbox.apache.org/repos/asf/superset.git
commit 15c5740b775660d975f0c0ce43e16c7d1cea30d7 Author: Kamil Gabryjelski <[email protected]> AuthorDate: Thu Dec 18 22:27:18 2025 +0100 MAGIC WAND --- .../components/WhatIfDrawer/MagicWandLoader.tsx | 287 +++++++++++++++++++++ .../dashboard/components/WhatIfDrawer/index.tsx | 9 + 2 files changed, 296 insertions(+) diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/MagicWandLoader.tsx b/superset-frontend/src/dashboard/components/WhatIfDrawer/MagicWandLoader.tsx new file mode 100644 index 0000000000..37f1491a87 --- /dev/null +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/MagicWandLoader.tsx @@ -0,0 +1,287 @@ +/** + * 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 } from 'react'; +import { keyframes } from '@emotion/react'; +import { css, styled, useTheme } from '@apache-superset/core/ui'; +import { t } from '@superset-ui/core'; + +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const waveWand = keyframes` + 0%, 100% { + transform: rotate(-15deg); + } + 50% { + transform: rotate(15deg); + } +`; + +const sparkle = keyframes` + 0%, 100% { + opacity: 0; + transform: scale(0) rotate(0deg); + } + 50% { + opacity: 1; + transform: scale(1) rotate(180deg); + } +`; + +const float = keyframes` + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +`; + +const pulse = keyframes` + 0%, 100% { + transform: scale(1); + opacity: 0.8; + } + 50% { + transform: scale(1.2); + opacity: 1; + } +`; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: ${({ theme }) => theme.colorBgMask}; + backdrop-filter: blur(2px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 9999; + animation: ${fadeIn} 0.3s ease-out; +`; + +const WandContainer = styled.div` + position: relative; + width: 200px; + height: 200px; + display: flex; + align-items: center; + justify-content: center; + animation: ${float} 2s ease-in-out infinite; +`; + +const Wand = styled.div` + position: relative; + animation: ${waveWand} 1s ease-in-out infinite; + transform-origin: bottom center; +`; + +const WandSvg = styled.svg` + width: 120px; + height: 120px; + filter: drop-shadow(0 0 20px ${({ theme }) => theme.colorWarning}); +`; + +const Sparkle = styled.div<{ delay: number; x: number; y: number }>` + position: absolute; + width: 12px; + height: 12px; + animation: ${sparkle} 1.5s ease-in-out infinite; + animation-delay: ${({ delay }) => delay}s; + left: ${({ x }) => x}%; + top: ${({ y }) => y}%; + + &::before, + &::after { + content: ''; + position: absolute; + background: linear-gradient( + 135deg, + ${({ theme }) => theme.colorWarning} 0%, + ${({ theme }) => theme.colorWhite} 50%, + ${({ theme }) => theme.colorWarning} 100% + ); + } + + &::before { + width: 100%; + height: 2px; + top: 50%; + left: 0; + transform: translateY(-50%); + } + + &::after { + width: 2px; + height: 100%; + top: 0; + left: 50%; + transform: translateX(-50%); + } +`; + +const GlowOrb = styled.div` + position: absolute; + top: 15%; + left: 50%; + transform: translateX(-50%); + width: 30px; + height: 30px; + background: radial-gradient( + circle, + ${({ theme }) => theme.colorWarning} 0%, + ${({ theme }) => theme.colorWarningBg} 40%, + transparent 70% + ); + border-radius: 50%; + animation: ${pulse} 0.8s ease-in-out infinite; +`; + +const LoadingText = styled.div` + margin-top: ${({ theme }) => theme.sizeUnit * 6}px; + color: ${({ theme }) => theme.colorWhite}; + font-size: ${({ theme }) => theme.fontSizeLG}px; + font-weight: ${({ theme }) => theme.fontWeightStrong}; + text-shadow: 0 2px 10px ${({ theme }) => theme.colorBgMask}; + text-align: center; +`; + +const SubText = styled.div` + margin-top: ${({ theme }) => theme.sizeUnit}px; + color: ${({ theme }) => theme.colorTextLightSolid}; + font-size: ${({ theme }) => theme.fontSizeSM}px; +`; + +const DismissHint = styled.div` + margin-top: ${({ theme }) => theme.sizeUnit * 4}px; + color: ${({ theme }) => theme.colorTextLightSolid}; + font-size: ${({ theme }) => theme.fontSizeXS}px; + opacity: 0.7; +`; + +const sparklePositions = [ + { x: 20, y: 10, delay: 0 }, + { x: 75, y: 15, delay: 0.3 }, + { x: 85, y: 35, delay: 0.6 }, + { x: 15, y: 30, delay: 0.9 }, + { x: 60, y: 5, delay: 0.2 }, + { x: 40, y: 20, delay: 0.5 }, + { x: 80, y: 25, delay: 0.8 }, + { x: 25, y: 18, delay: 0.4 }, +]; + +// Magic wand SVG colors - using warm brown/gold tones for the wand aesthetic +/* eslint-disable theme-colors/no-literal-colors */ +const WAND_COLORS = { + woodDark: '#654321', + woodMid: '#8B4513', + woodLight: '#A0522D', + goldDark: '#B8860B', + goldMid: '#DAA520', + starLight: '#FFF8DC', +}; +/* eslint-enable theme-colors/no-literal-colors */ + +interface MagicWandLoaderProps { + onDismiss?: () => void; +} + +const MagicWandLoader = ({ onDismiss }: MagicWandLoaderProps) => { + const theme = useTheme(); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && onDismiss) { + onDismiss(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onDismiss]); + + return ( + <Overlay data-test="magic-wand-loader" onClick={onDismiss}> + <WandContainer> + {sparklePositions.map((pos, i) => ( + <Sparkle key={i} x={pos.x} y={pos.y} delay={pos.delay} /> + ))} + <GlowOrb /> + <Wand> + <WandSvg viewBox="0 0 64 64" fill="none"> + <defs> + <linearGradient + id="wandGradient" + x1="0%" + y1="0%" + x2="100%" + y2="100%" + > + <stop offset="0%" stopColor={WAND_COLORS.woodMid} /> + <stop offset="50%" stopColor={WAND_COLORS.woodLight} /> + <stop offset="100%" stopColor={WAND_COLORS.woodDark} /> + </linearGradient> + </defs> + + {/* Thin wand body */} + <rect + x="30.5" + y="16" + width="3" + height="42" + rx="1.5" + fill="url(#wandGradient)" + css={css` + filter: drop-shadow(1px 1px 2px ${theme.colorBgMask}); + `} + /> + + {/* Small star at tip */} + <circle + cx="32" + cy="12" + r="4" + fill={theme.colorWarning} + css={css` + filter: drop-shadow(0 0 6px ${theme.colorWarning}); + `} + /> + </WandSvg> + </Wand> + </WandContainer> + <LoadingText>{t('Analyzing relationships...')}</LoadingText> + <SubText>{t('AI is finding connected columns')}</SubText> + {onDismiss && ( + <DismissHint>{t('Click anywhere or press Esc to cancel')}</DismissHint> + )} + </Overlay> + ); +}; + +export default MagicWandLoader; diff --git a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx index b0c0220948..9dee0ca6ae 100644 --- a/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx +++ b/superset-frontend/src/dashboard/components/WhatIfDrawer/index.tsx @@ -48,6 +48,7 @@ import AdhocFilterEditPopover from 'src/explore/components/controls/FilterContro import { Clauses } from 'src/explore/components/controls/FilterControl/types'; import { OPERATOR_ENUM_TO_OPERATOR_TYPE } from 'src/explore/constants'; import WhatIfAIInsights from './WhatIfAIInsights'; +import MagicWandLoader from './MagicWandLoader'; import { fetchRelatedColumnSuggestions } from './whatIfApi'; import { ExtendedWhatIfModification } from './types'; @@ -858,6 +859,14 @@ const WhatIfPanel = ({ onClose, topOffset }: WhatIfPanelProps) => { /> )} </PanelContent> + {isLoadingSuggestions && ( + <MagicWandLoader + onDismiss={() => { + suggestionsAbortControllerRef.current?.abort(); + setIsLoadingSuggestions(false); + }} + /> + )} </PanelContainer> ); };
