| import React from 'react' | |||||
| import { render } from '@testing-library/react' | |||||
| import '@testing-library/jest-dom' | |||||
| import { OpikIconBig } from '@/app/components/base/icons/src/public/tracing' | |||||
| // Mock dependencies to isolate the SVG rendering issue | |||||
| jest.mock('react-i18next', () => ({ | |||||
| useTranslation: () => ({ | |||||
| t: (key: string) => key, | |||||
| }), | |||||
| })) | |||||
| describe('SVG Attribute Error Reproduction', () => { | |||||
| // Capture console errors | |||||
| const originalError = console.error | |||||
| let errorMessages: string[] = [] | |||||
| beforeEach(() => { | |||||
| errorMessages = [] | |||||
| console.error = jest.fn((message) => { | |||||
| errorMessages.push(message) | |||||
| originalError(message) | |||||
| }) | |||||
| }) | |||||
| afterEach(() => { | |||||
| console.error = originalError | |||||
| }) | |||||
| it('should reproduce inkscape attribute errors when rendering OpikIconBig', () => { | |||||
| console.log('\n=== TESTING OpikIconBig SVG ATTRIBUTE ERRORS ===') | |||||
| // Test multiple renders to check for inconsistency | |||||
| for (let i = 0; i < 5; i++) { | |||||
| console.log(`\nRender attempt ${i + 1}:`) | |||||
| const { unmount } = render(<OpikIconBig />) | |||||
| // Check for specific inkscape attribute errors | |||||
| const inkscapeErrors = errorMessages.filter(msg => | |||||
| typeof msg === 'string' && msg.includes('inkscape'), | |||||
| ) | |||||
| if (inkscapeErrors.length > 0) { | |||||
| console.log(`Found ${inkscapeErrors.length} inkscape errors:`) | |||||
| inkscapeErrors.forEach((error, index) => { | |||||
| console.log(` ${index + 1}. ${error.substring(0, 100)}...`) | |||||
| }) | |||||
| } | |||||
| else { | |||||
| console.log('No inkscape errors found in this render') | |||||
| } | |||||
| unmount() | |||||
| // Clear errors for next iteration | |||||
| errorMessages = [] | |||||
| } | |||||
| }) | |||||
| it('should analyze the SVG structure causing the errors', () => { | |||||
| console.log('\n=== ANALYZING SVG STRUCTURE ===') | |||||
| // Import the JSON data directly | |||||
| const iconData = require('@/app/components/base/icons/src/public/tracing/OpikIconBig.json') | |||||
| console.log('Icon structure analysis:') | |||||
| console.log('- Root element:', iconData.icon.name) | |||||
| console.log('- Children count:', iconData.icon.children?.length || 0) | |||||
| // Find problematic elements | |||||
| const findProblematicElements = (node: any, path = '') => { | |||||
| const problematicElements: any[] = [] | |||||
| if (node.name && (node.name.includes(':') || node.name.startsWith('sodipodi'))) { | |||||
| problematicElements.push({ | |||||
| path, | |||||
| name: node.name, | |||||
| attributes: Object.keys(node.attributes || {}), | |||||
| }) | |||||
| } | |||||
| // Check attributes for inkscape/sodipodi properties | |||||
| if (node.attributes) { | |||||
| const problematicAttrs = Object.keys(node.attributes).filter(attr => | |||||
| attr.startsWith('inkscape:') || attr.startsWith('sodipodi:'), | |||||
| ) | |||||
| if (problematicAttrs.length > 0) { | |||||
| problematicElements.push({ | |||||
| path, | |||||
| name: node.name, | |||||
| problematicAttributes: problematicAttrs, | |||||
| }) | |||||
| } | |||||
| } | |||||
| if (node.children) { | |||||
| node.children.forEach((child: any, index: number) => { | |||||
| problematicElements.push( | |||||
| ...findProblematicElements(child, `${path}/${node.name}[${index}]`), | |||||
| ) | |||||
| }) | |||||
| } | |||||
| return problematicElements | |||||
| } | |||||
| const problematicElements = findProblematicElements(iconData.icon, 'root') | |||||
| console.log(`\n🚨 Found ${problematicElements.length} problematic elements:`) | |||||
| problematicElements.forEach((element, index) => { | |||||
| console.log(`\n${index + 1}. Element: ${element.name}`) | |||||
| console.log(` Path: ${element.path}`) | |||||
| if (element.problematicAttributes) | |||||
| console.log(` Problematic attributes: ${element.problematicAttributes.join(', ')}`) | |||||
| }) | |||||
| }) | |||||
| it('should test the normalizeAttrs function behavior', () => { | |||||
| console.log('\n=== TESTING normalizeAttrs FUNCTION ===') | |||||
| const { normalizeAttrs } = require('@/app/components/base/icons/utils') | |||||
| const testAttributes = { | |||||
| 'inkscape:showpageshadow': '2', | |||||
| 'inkscape:pageopacity': '0.0', | |||||
| 'inkscape:pagecheckerboard': '0', | |||||
| 'inkscape:deskcolor': '#d1d1d1', | |||||
| 'sodipodi:docname': 'opik-icon-big.svg', | |||||
| 'xmlns:inkscape': 'https://www.inkscape.org/namespaces/inkscape', | |||||
| 'xmlns:sodipodi': 'https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', | |||||
| 'xmlns:svg': 'https://www.w3.org/2000/svg', | |||||
| 'data-name': 'Layer 1', | |||||
| 'normal-attr': 'value', | |||||
| 'class': 'test-class', | |||||
| } | |||||
| console.log('Input attributes:', Object.keys(testAttributes)) | |||||
| const normalized = normalizeAttrs(testAttributes) | |||||
| console.log('Normalized attributes:', Object.keys(normalized)) | |||||
| console.log('Normalized values:', normalized) | |||||
| // Check if problematic attributes are still present | |||||
| const problematicKeys = Object.keys(normalized).filter(key => | |||||
| key.toLowerCase().includes('inkscape') || key.toLowerCase().includes('sodipodi'), | |||||
| ) | |||||
| if (problematicKeys.length > 0) | |||||
| console.log(`🚨 PROBLEM: Still found problematic attributes: ${problematicKeys.join(', ')}`) | |||||
| else | |||||
| console.log('✅ No problematic attributes found after normalization') | |||||
| }) | |||||
| }) |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useCallback, useEffect, useRef, useState } from 'react' | |||||
| import { | |||||
| RiEqualizer2Line, | |||||
| } from '@remixicon/react' | |||||
| import React, { useCallback, useRef, useState } from 'react' | |||||
| import type { PopupProps } from './config-popup' | import type { PopupProps } from './config-popup' | ||||
| import ConfigPopup from './config-popup' | import ConfigPopup from './config-popup' | ||||
| import cn from '@/utils/classnames' | |||||
| import { | import { | ||||
| PortalToFollowElem, | PortalToFollowElem, | ||||
| PortalToFollowElemContent, | PortalToFollowElemContent, | ||||
| readOnly: boolean | readOnly: boolean | ||||
| className?: string | className?: string | ||||
| hasConfigured: boolean | hasConfigured: boolean | ||||
| controlShowPopup?: number | |||||
| children?: React.ReactNode | |||||
| } & PopupProps | } & PopupProps | ||||
| const ConfigBtn: FC<Props> = ({ | const ConfigBtn: FC<Props> = ({ | ||||
| className, | className, | ||||
| hasConfigured, | hasConfigured, | ||||
| controlShowPopup, | |||||
| children, | |||||
| ...popupProps | ...popupProps | ||||
| }) => { | }) => { | ||||
| const [open, doSetOpen] = useState(false) | const [open, doSetOpen] = useState(false) | ||||
| setOpen(!openRef.current) | setOpen(!openRef.current) | ||||
| }, [setOpen]) | }, [setOpen]) | ||||
| useEffect(() => { | |||||
| if (controlShowPopup) | |||||
| // setOpen(!openRef.current) | |||||
| setOpen(true) | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, [controlShowPopup]) | |||||
| if (popupProps.readOnly && !hasConfigured) | if (popupProps.readOnly && !hasConfigured) | ||||
| return null | return null | ||||
| open={open} | open={open} | ||||
| onOpenChange={setOpen} | onOpenChange={setOpen} | ||||
| placement='bottom-end' | placement='bottom-end' | ||||
| offset={{ | |||||
| mainAxis: 12, | |||||
| crossAxis: hasConfigured ? 8 : 49, | |||||
| }} | |||||
| offset={12} | |||||
| > | > | ||||
| <PortalToFollowElemTrigger onClick={handleTrigger}> | <PortalToFollowElemTrigger onClick={handleTrigger}> | ||||
| <div className={cn(className, 'rounded-md p-1')}> | |||||
| <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> | |||||
| <div className="select-none"> | |||||
| {children} | |||||
| </div> | </div> | ||||
| </PortalToFollowElemTrigger> | </PortalToFollowElemTrigger> | ||||
| <PortalToFollowElemContent className='z-[11]'> | <PortalToFollowElemContent className='z-[11]'> |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | |||||
| import React, { useEffect, useState } from 'react' | |||||
| import { | import { | ||||
| RiArrowDownDoubleLine, | RiArrowDownDoubleLine, | ||||
| RiEqualizer2Line, | |||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { usePathname } from 'next/navigation' | import { usePathname } from 'next/navigation' | ||||
| })() | })() | ||||
| }, []) | }, []) | ||||
| const [controlShowPopup, setControlShowPopup] = useState<number>(0) | |||||
| const showPopup = useCallback(() => { | |||||
| setControlShowPopup(Date.now()) | |||||
| }, [setControlShowPopup]) | |||||
| if (!isLoaded) { | if (!isLoaded) { | ||||
| return ( | return ( | ||||
| <div className='mb-3 flex items-center justify-between'> | <div className='mb-3 flex items-center justify-between'> | ||||
| return ( | return ( | ||||
| <div className={cn('flex items-center justify-between')}> | <div className={cn('flex items-center justify-between')}> | ||||
| <div | |||||
| className={cn( | |||||
| 'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter', | |||||
| controlShowPopup && 'border-effects-highlight-lightmode-off bg-background-default-lighter', | |||||
| )} | |||||
| onClick={showPopup} | |||||
| > | |||||
| {!inUseTracingProvider && ( | |||||
| <> | |||||
| {!inUseTracingProvider && ( | |||||
| <ConfigButton | |||||
| appId={appId} | |||||
| readOnly={readOnly} | |||||
| hasConfigured={false} | |||||
| enabled={enabled} | |||||
| onStatusChange={handleTracingEnabledChange} | |||||
| chosenProvider={inUseTracingProvider} | |||||
| onChooseProvider={handleChooseProvider} | |||||
| arizeConfig={arizeConfig} | |||||
| phoenixConfig={phoenixConfig} | |||||
| langSmithConfig={langSmithConfig} | |||||
| langFuseConfig={langFuseConfig} | |||||
| opikConfig={opikConfig} | |||||
| weaveConfig={weaveConfig} | |||||
| aliyunConfig={aliyunConfig} | |||||
| onConfigUpdated={handleTracingConfigUpdated} | |||||
| onConfigRemoved={handleTracingConfigRemoved} | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter', | |||||
| )} | |||||
| > | |||||
| <TracingIcon size='md' /> | <TracingIcon size='md' /> | ||||
| <div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div> | <div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div> | ||||
| <div className='flex items-center' onClick={e => e.stopPropagation()}> | |||||
| <ConfigButton | |||||
| appId={appId} | |||||
| readOnly={readOnly} | |||||
| hasConfigured={false} | |||||
| enabled={enabled} | |||||
| onStatusChange={handleTracingEnabledChange} | |||||
| chosenProvider={inUseTracingProvider} | |||||
| onChooseProvider={handleChooseProvider} | |||||
| arizeConfig={arizeConfig} | |||||
| phoenixConfig={phoenixConfig} | |||||
| langSmithConfig={langSmithConfig} | |||||
| langFuseConfig={langFuseConfig} | |||||
| opikConfig={opikConfig} | |||||
| weaveConfig={weaveConfig} | |||||
| aliyunConfig={aliyunConfig} | |||||
| onConfigUpdated={handleTracingConfigUpdated} | |||||
| onConfigRemoved={handleTracingConfigRemoved} | |||||
| controlShowPopup={controlShowPopup} | |||||
| /> | |||||
| <div className='rounded-md p-1'> | |||||
| <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> | |||||
| </div> | </div> | ||||
| <Divider type='vertical' className='h-3.5' /> | <Divider type='vertical' className='h-3.5' /> | ||||
| <div className='rounded-md p-1'> | <div className='rounded-md p-1'> | ||||
| <RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' /> | <RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' /> | ||||
| </div> | </div> | ||||
| </> | |||||
| )} | |||||
| {hasConfiguredTracing && ( | |||||
| <> | |||||
| </div> | |||||
| </ConfigButton> | |||||
| )} | |||||
| {hasConfiguredTracing && ( | |||||
| <ConfigButton | |||||
| appId={appId} | |||||
| readOnly={readOnly} | |||||
| hasConfigured | |||||
| enabled={enabled} | |||||
| onStatusChange={handleTracingEnabledChange} | |||||
| chosenProvider={inUseTracingProvider} | |||||
| onChooseProvider={handleChooseProvider} | |||||
| arizeConfig={arizeConfig} | |||||
| phoenixConfig={phoenixConfig} | |||||
| langSmithConfig={langSmithConfig} | |||||
| langFuseConfig={langFuseConfig} | |||||
| opikConfig={opikConfig} | |||||
| weaveConfig={weaveConfig} | |||||
| aliyunConfig={aliyunConfig} | |||||
| onConfigUpdated={handleTracingConfigUpdated} | |||||
| onConfigRemoved={handleTracingConfigRemoved} | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter', | |||||
| )} | |||||
| > | |||||
| <div className='ml-4 mr-1 flex items-center'> | <div className='ml-4 mr-1 flex items-center'> | ||||
| <Indicator color={enabled ? 'green' : 'gray'} /> | <Indicator color={enabled ? 'green' : 'gray'} /> | ||||
| <div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'> | <div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />} | {InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />} | ||||
| <Divider type='vertical' className='h-3.5' /> | |||||
| <div className='flex items-center' onClick={e => e.stopPropagation()}> | |||||
| <ConfigButton | |||||
| appId={appId} | |||||
| readOnly={readOnly} | |||||
| hasConfigured | |||||
| className='ml-2' | |||||
| enabled={enabled} | |||||
| onStatusChange={handleTracingEnabledChange} | |||||
| chosenProvider={inUseTracingProvider} | |||||
| onChooseProvider={handleChooseProvider} | |||||
| arizeConfig={arizeConfig} | |||||
| phoenixConfig={phoenixConfig} | |||||
| langSmithConfig={langSmithConfig} | |||||
| langFuseConfig={langFuseConfig} | |||||
| opikConfig={opikConfig} | |||||
| weaveConfig={weaveConfig} | |||||
| aliyunConfig={aliyunConfig} | |||||
| onConfigUpdated={handleTracingConfigUpdated} | |||||
| onConfigRemoved={handleTracingConfigRemoved} | |||||
| controlShowPopup={controlShowPopup} | |||||
| /> | |||||
| <div className='ml-2 rounded-md p-1'> | |||||
| <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /> | |||||
| </div> | </div> | ||||
| </> | |||||
| )} | |||||
| </div > | |||||
| </div > | |||||
| <Divider type='vertical' className='h-3.5' /> | |||||
| </div> | |||||
| </ConfigButton> | |||||
| )} | |||||
| </div> | |||||
| ) | ) | ||||
| } | } | ||||
| export default React.memo(Panel) | export default React.memo(Panel) |
| return null | return null | ||||
| return ( | return ( | ||||
| <footer className='shrink-0 grow-0 px-12 py-2 relative'> | |||||
| <footer className='relative shrink-0 grow-0 px-12 py-2'> | |||||
| <button | <button | ||||
| onClick={handleClose} | onClick={handleClose} | ||||
| className='absolute top-2 right-2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full transition-colors duration-200 ease-in-out hover:bg-gray-100 dark:hover:bg-gray-800' | |||||
| className='absolute right-2 top-2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full transition-colors duration-200 ease-in-out hover:bg-components-main-nav-nav-button-bg-active' | |||||
| aria-label="Close footer" | aria-label="Close footer" | ||||
| > | > | ||||
| <RiCloseLine className='h-4 w-4 text-text-tertiary' /> | |||||
| <RiCloseLine className='h-4 w-4 text-text-tertiary hover:text-text-secondary' /> | |||||
| </button> | </button> | ||||
| <h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3> | <h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3> | ||||
| <p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p> | <p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p> |
| export function normalizeAttrs(attrs: Attrs = {}): Attrs { | export function normalizeAttrs(attrs: Attrs = {}): Attrs { | ||||
| return Object.keys(attrs).reduce((acc: Attrs, key) => { | return Object.keys(attrs).reduce((acc: Attrs, key) => { | ||||
| // Filter out editor metadata attributes before processing | |||||
| if (key.startsWith('inkscape:') | |||||
| || key.startsWith('sodipodi:') | |||||
| || key.startsWith('xmlns:inkscape') | |||||
| || key.startsWith('xmlns:sodipodi') | |||||
| || key.startsWith('xmlns:svg') | |||||
| || key === 'data-name') | |||||
| return acc | |||||
| const val = attrs[key] | const val = attrs[key] | ||||
| key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase()) | key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase()) | ||||
| key = key.replace(/([:]\w)/g, (g: string) => g[1].toUpperCase()) | key = key.replace(/([:]\w)/g, (g: string) => g[1].toUpperCase()) | ||||
| // Additional filter after camelCase conversion | |||||
| if (key === 'xmlnsInkscape' | |||||
| || key === 'xmlnsSodipodi' | |||||
| || key === 'xmlnsSvg' | |||||
| || key === 'dataName') | |||||
| return acc | |||||
| switch (key) { | switch (key) { | ||||
| case 'class': | case 'class': | ||||
| acc.className = val | acc.className = val |
| </div> | </div> | ||||
| <div className='border-t-[0.5px] border-divider-regular' /> | <div className='border-t-[0.5px] border-divider-regular' /> | ||||
| <div className='p-1'> | <div className='p-1'> | ||||
| <div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => setShowTagManagementModal(true)}> | |||||
| <div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => { | |||||
| setShowTagManagementModal(true) | |||||
| setOpen(false) | |||||
| }}> | |||||
| <Tag03 className='h-4 w-4 text-text-tertiary' /> | <Tag03 className='h-4 w-4 text-text-tertiary' /> | ||||
| <div className='grow truncate text-sm leading-5 text-text-secondary'> | <div className='grow truncate text-sm leading-5 text-text-secondary'> | ||||
| {t('common.tag.manageTags')} | {t('common.tag.manageTags')} |