diff --git a/superset-frontend/images/icons/transparent.svg b/superset-frontend/images/icons/transparent.svg new file mode 100644 index 00000000000..7c08d2897ab --- /dev/null +++ b/superset-frontend/images/icons/transparent.svg @@ -0,0 +1,19 @@ + + diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index aef9dd5753b..a2e46b5c8ec 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -21,7 +21,7 @@ module.exports = { moduleNameMapper: { '\\.(css|less)$': '/spec/__mocks__/styleMock.js', '\\.(gif|ttf|eot|png)$': '/spec/__mocks__/fileMock.js', - '\\.svg$': '/spec/__mocks__/svgrMock.js', + '\\.svg$': '/spec/__mocks__/svgrMock.tsx', '^src/(.*)$': '/src/$1', '^spec/(.*)$': '/spec/$1', }, diff --git a/superset-frontend/spec/__mocks__/svgrMock.js b/superset-frontend/spec/__mocks__/svgrMock.tsx similarity index 74% rename from superset-frontend/spec/__mocks__/svgrMock.js rename to superset-frontend/spec/__mocks__/svgrMock.tsx index 46bda783219..6073fd6d9f3 100644 --- a/superset-frontend/spec/__mocks__/svgrMock.js +++ b/superset-frontend/spec/__mocks__/svgrMock.tsx @@ -16,5 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -export default 'SvgrURL'; -export const ReactComponent = 'svg'; + +import React, { SVGProps } from 'react'; + +const SvgrMock = React.forwardRef>( + (props, ref) => , +); + +SvgrMock.displayName = 'SvgrMock'; + +export const ReactComponent = SvgrMock; +export default SvgrMock; diff --git a/superset-frontend/spec/javascripts/dashboard/components/FiltersBadge_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/FiltersBadge_spec.tsx index 9609c00e643..d6bd4974d54 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/FiltersBadge_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/FiltersBadge_spec.tsx @@ -31,6 +31,7 @@ import { import { sliceId } from 'spec/fixtures/mockChartQueries'; import { dashboardFilters } from 'spec/fixtures/mockDashboardFilters'; import { dashboardWithFilter } from 'spec/fixtures/mockDashboardLayout'; +import Icons from 'src/components/Icons'; import { FeatureFlag } from 'src/featureFlags'; describe('FiltersBadge', () => { @@ -129,7 +130,7 @@ describe('FiltersBadge', () => { ).toHaveText('1'); // to look at the shape of the wrapper use: // console.log(wrapper.dive().debug()) - expect(wrapper.dive().find('Icon[name="alert-solid"]')).toExist(); + expect(wrapper.dive().find(Icons.AlertSolid)).toExist(); }); }); @@ -216,7 +217,7 @@ describe('FiltersBadge', () => { expect( wrapper.dive().find('[data-test="incompatible-filter-count"]'), ).toHaveText('1'); - expect(wrapper.dive().find('Icon[name="alert-solid"]')).toExist(); + expect(wrapper.dive().find(Icons.AlertSolid)).toExist(); }); }); }); diff --git a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx index d2bec105734..5fcb77b80e9 100644 --- a/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DatasourceControl_spec.jsx @@ -24,7 +24,7 @@ import { Menu } from 'src/common/components'; import DatasourceModal from 'src/datasource/DatasourceModal'; import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal'; import DatasourceControl from 'src/explore/components/controls/DatasourceControl'; -import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import { Tooltip } from 'src/components/Tooltip'; const defaultProps = { @@ -119,8 +119,7 @@ describe('DatasourceControl', () => { it('should render health check message', () => { const wrapper = setup(); - const alert = wrapper.find(Icon); - expect(alert.at(1).prop('name')).toBe('alert-solid'); + expect(wrapper.find(Icons.AlertSolid)).toExist(); const tooltip = wrapper.find(Tooltip).at(0); expect(tooltip.prop('title')).toBe( defaultProps.datasource.health_check_message, diff --git a/superset-frontend/src/components/Alert/Alert.test.tsx b/superset-frontend/src/components/Alert/Alert.test.tsx index eeb23b3c6dd..8a59469180d 100644 --- a/superset-frontend/src/components/Alert/Alert.test.tsx +++ b/superset-frontend/src/components/Alert/Alert.test.tsx @@ -17,31 +17,37 @@ * under the License. */ import React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; +import { render, screen, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import Alert, { AlertProps } from 'src/components/Alert'; type AlertType = Pick; type AlertTypeValue = AlertType[keyof AlertType]; -test('renders with default props', () => { +test('renders with default props', async () => { render(); + expect(screen.getByRole('alert')).toHaveTextContent('Message'); - expect(screen.queryByLabelText(`info icon`)).toBeInTheDocument(); - expect(screen.queryByLabelText('close icon')).toBeInTheDocument(); + expect(await screen.findByLabelText(`info icon`)).toBeInTheDocument(); + expect(await screen.findByLabelText('close icon')).toBeInTheDocument(); }); -test('renders each type', () => { +test('renders each type', async () => { const types: AlertTypeValue[] = ['info', 'error', 'warning', 'success']; - types.forEach(type => { + for (let i = 0; i < types.length; i += 1) { + const type = types[i]; render(); - expect(screen.queryByLabelText(`${type} icon`)).toBeInTheDocument(); - }); + // eslint-disable-next-line no-await-in-loop + expect(await screen.findByLabelText(`${type} icon`)).toBeInTheDocument(); + } }); -test('renders without close button', () => { +test('renders without close button', async () => { render(); - expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByLabelText('close icon')).not.toBeInTheDocument(); + }); }); test('disappear when closed', () => { @@ -50,10 +56,12 @@ test('disappear when closed', () => { expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); -test('renders without icon', () => { +test('renders without icon', async () => { const type = 'info'; render(); - expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.queryByLabelText(`${type} icon`)).not.toBeInTheDocument(); + }); }); test('renders message', () => { diff --git a/superset-frontend/src/components/Alert/index.tsx b/superset-frontend/src/components/Alert/index.tsx index a80238a74f8..9e2e6bfce98 100644 --- a/superset-frontend/src/components/Alert/index.tsx +++ b/superset-frontend/src/components/Alert/index.tsx @@ -22,7 +22,8 @@ import { AlertProps as AntdAlertProps, } from 'src/common/components'; import { useTheme } from '@superset-ui/core'; -import Icon, { IconName } from 'src/components/Icon'; +import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; export type AlertProps = PropsWithChildren; @@ -40,23 +41,23 @@ export default function Alert(props: AlertProps) { const { alert, error, info, success } = colors; let baseColor = info; - let iconName: IconName = 'info-solid'; + let AlertIcon = Icons.InfoSolid; if (type === 'error') { baseColor = error; - iconName = 'error-solid'; + AlertIcon = Icons.ErrorSolid; } else if (type === 'warning') { baseColor = alert; - iconName = 'alert-solid'; + AlertIcon = Icons.AlertSolid; } else if (type === 'success') { baseColor = success; - iconName = 'circle-check-solid'; + AlertIcon = Icons.CircleCheckSolid; } return ( } + icon={} closeText={closable && } css={{ padding: '6px 10px', diff --git a/superset-frontend/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/src/components/Icons/AntdEnhanced.tsx index 02438fd111c..6d72e002510 100644 --- a/superset-frontend/src/components/Icons/AntdEnhanced.tsx +++ b/superset-frontend/src/components/Icons/AntdEnhanced.tsx @@ -19,12 +19,14 @@ import React from 'react'; import * as AntdIcons from '@ant-design/icons/lib/icons'; -import Icon from './Icon'; +import { StyledIcon } from './Icon'; import IconType from './IconType'; const AntdEnhancedIcons = Object.keys(AntdIcons) .map(k => ({ - [k]: (props: IconType) => , + [k]: (props: IconType) => ( + + ), })) .reduce((l, r) => ({ ...l, ...r })); diff --git a/superset-frontend/src/components/Icons/Icon.tsx b/superset-frontend/src/components/Icons/Icon.tsx index 00763172bde..af431efa445 100644 --- a/superset-frontend/src/components/Icons/Icon.tsx +++ b/superset-frontend/src/components/Icons/Icon.tsx @@ -17,34 +17,54 @@ * under the License. */ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import AntdIcon from '@ant-design/icons'; import { styled } from '@superset-ui/core'; -import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; +import { ReactComponent as TransparentIcon } from 'images/icons/transparent.svg'; import IconType from './IconType'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const EnhancedIcon = ({ iconColor, iconSize, ...rest }: IconType) => ( +const AntdIconComponent = ({ iconColor, iconSize, ...rest }: IconType) => ( ); -const Icon = styled(EnhancedIcon)` +export const StyledIcon = styled(AntdIconComponent)` ${({ iconColor }) => iconColor && `color: ${iconColor};`}; font-size: ${({ iconSize, theme }) => - iconSize ? `${theme.typography.sizes[iconSize]}px` : '24px'}; + iconSize + ? `${theme.typography.sizes[iconSize] || theme.typography.sizes.m}px` + : '24px'}; `; -export const renderIcon = ( - SVGComponent: - | React.ComponentClass< - CustomIconComponentProps | React.SVGProps, - any - > - | React.FunctionComponent< - CustomIconComponentProps | React.SVGProps - > - | undefined, - props: IconType, -) => ; +interface IconProps extends IconType { + fileName: string; +} + +export const Icon = (props: IconProps) => { + const { fileName, ...iconProps } = props; + const [, setLoaded] = useState(false); + const ImportedSVG = useRef>>(); + const name = fileName.replace('_', '-'); + + useEffect(() => { + async function importIcon(): Promise { + ImportedSVG.current = ( + await import( + `!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg` + ) + ).default; + setLoaded(true); + } + importIcon(); + }, [fileName, ImportedSVG]); + + return ( + + ); +}; export default Icon; diff --git a/superset-frontend/src/components/Icons/IconType.ts b/superset-frontend/src/components/Icons/IconType.ts index 85e0ad8ee1a..83a6e0ba6f0 100644 --- a/superset-frontend/src/components/Icons/IconType.ts +++ b/superset-frontend/src/components/Icons/IconType.ts @@ -22,6 +22,7 @@ import AntdIcon from '@ant-design/icons'; type AntdIconType = typeof AntdIcon.defaultProps; type IconType = AntdIconType & { iconColor?: string; + twoToneColor?: string; iconSize?: 's' | 'm' | 'l' | 'xl' | 'xxl'; }; diff --git a/superset-frontend/src/components/Icons/Icons.stories.tsx b/superset-frontend/src/components/Icons/Icons.stories.tsx index ca5b7f9a137..c40dbad295d 100644 --- a/superset-frontend/src/components/Icons/Icons.stories.tsx +++ b/superset-frontend/src/components/Icons/Icons.stories.tsx @@ -78,6 +78,11 @@ InteractiveIcons.argTypes = { defaultValue: null, control: { type: 'select', options: palette }, }, + // @TODO twoToneColor is being ignored + twoToneColor: { + defaultValue: null, + control: { type: 'select', options: palette }, + }, theme: { table: { disable: true, diff --git a/superset-frontend/src/components/Icons/index.ts b/superset-frontend/src/components/Icons/index.ts deleted file mode 100644 index 0bb0310fd92..00000000000 --- a/superset-frontend/src/components/Icons/index.ts +++ /dev/null @@ -1,284 +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 { ReactComponent as AlertIcon } from 'images/icons/alert.svg'; -import { ReactComponent as AlertSolidIcon } from 'images/icons/alert_solid.svg'; -import { ReactComponent as AlertSolidSmallIcon } from 'images/icons/alert_solid_small.svg'; -import { ReactComponent as BinocularsIcon } from 'images/icons/binoculars.svg'; -import { ReactComponent as BoltIcon } from 'images/icons/bolt.svg'; -import { ReactComponent as BoltSmallIcon } from 'images/icons/bolt_small.svg'; -import { ReactComponent as BoltSmallRunIcon } from 'images/icons/bolt_small_run.svg'; -import { ReactComponent as CalendarIcon } from 'images/icons/calendar.svg'; -import { ReactComponent as CancelIcon } from 'images/icons/cancel.svg'; -import { ReactComponent as CancelSolidIcon } from 'images/icons/cancel_solid.svg'; -import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg'; -import { ReactComponent as CardViewIcon } from 'images/icons/card_view.svg'; -import { ReactComponent as CardsIcon } from 'images/icons/cards.svg'; -import { ReactComponent as CardsLockedIcon } from 'images/icons/cards_locked.svg'; -import { ReactComponent as CaretDownIcon } from 'images/icons/caret_down.svg'; -import { ReactComponent as CaretLeftIcon } from 'images/icons/caret_left.svg'; -import { ReactComponent as CaretRightIcon } from 'images/icons/caret_right.svg'; -import { ReactComponent as CaretUpIcon } from 'images/icons/caret_up.svg'; -import { ReactComponent as CertifiedIcon } from 'images/icons/certified.svg'; -import { ReactComponent as CheckIcon } from 'images/icons/check.svg'; -import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg'; -import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg'; -import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg'; -import { ReactComponent as CircleCheckIcon } from 'images/icons/circle_check.svg'; -import { ReactComponent as CircleCheckSolidIcon } from 'images/icons/circle_check_solid.svg'; -import { ReactComponent as CircleIcon } from 'images/icons/circle.svg'; -import { ReactComponent as ClockIcon } from 'images/icons/clock.svg'; -import { ReactComponent as CloseIcon } from 'images/icons/close.svg'; -import { ReactComponent as CodeIcon } from 'images/icons/code.svg'; -import { ReactComponent as CogIcon } from 'images/icons/cog.svg'; -import { ReactComponent as CollapseIcon } from 'images/icons/collapse.svg'; -import { ReactComponent as ColorPaletteIcon } from 'images/icons/color_palette.svg'; -import { ReactComponent as ComponentsIcon } from 'images/icons/components.svg'; -import { ReactComponent as CopyIcon } from 'images/icons/copy.svg'; -import { ReactComponent as CursorTargeIcon } from 'images/icons/cursor_target.svg'; -import { ReactComponent as DatabaseIcon } from 'images/icons/database.svg'; -import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg'; -import { ReactComponent as DatasetVirtualGreyscaleIcon } from 'images/icons/dataset_virtual_greyscale.svg'; -import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg'; -import { ReactComponent as DownloadIcon } from 'images/icons/download.svg'; -import { ReactComponent as EditAltIcon } from 'images/icons/edit_alt.svg'; -import { ReactComponent as EditIcon } from 'images/icons/edit.svg'; -import { ReactComponent as EmailIcon } from 'images/icons/email.svg'; -import { ReactComponent as ErrorIcon } from 'images/icons/error.svg'; -import { ReactComponent as ErrorSolidIcon } from 'images/icons/error_solid.svg'; -import { ReactComponent as ErrorSolidSmallIcon } from 'images/icons/error_solid_small.svg'; -import { ReactComponent as ExclamationIcon } from 'images/icons/exclamation.svg'; -import { ReactComponent as ExpandIcon } from 'images/icons/expand.svg'; -import { ReactComponent as EyeIcon } from 'images/icons/eye.svg'; -import { ReactComponent as EyeSlashIcon } from 'images/icons/eye_slash.svg'; -import { ReactComponent as FavoriteSelectedIcon } from 'images/icons/favorite-selected.svg'; -import { ReactComponent as FavoriteSmallSelectedIcon } from 'images/icons/favorite_small_selected.svg'; -import { ReactComponent as FavoriteUnselectedIcon } from 'images/icons/favorite-unselected.svg'; -import { ReactComponent as FieldABCIcon } from 'images/icons/field_abc.svg'; -import { ReactComponent as FieldBooleanIcon } from 'images/icons/field_boolean.svg'; -import { ReactComponent as FieldDateIcon } from 'images/icons/field_date.svg'; -import { ReactComponent as FieldDerivedIcon } from 'images/icons/field_derived.svg'; -import { ReactComponent as FieldNumIcon } from 'images/icons/field_num.svg'; -import { ReactComponent as FieldStructIcon } from 'images/icons/field_struct.svg'; -import { ReactComponent as FileIcon } from 'images/icons/file.svg'; -import { ReactComponent as FilterIcon } from 'images/icons/filter.svg'; -import { ReactComponent as FilterSmallIcon } from 'images/icons/filter_small.svg'; -import { ReactComponent as FolderIcon } from 'images/icons/folder.svg'; -import { ReactComponent as FullIcon } from 'images/icons/full.svg'; -import { ReactComponent as FunctionIcon } from 'images/icons/function_x.svg'; -import { ReactComponent as GearIcon } from 'images/icons/gear.svg'; -import { ReactComponent as GridIcon } from 'images/icons/grid.svg'; -import { ReactComponent as ImageIcon } from 'images/icons/image.svg'; -import { ReactComponent as ImportIcon } from 'images/icons/import.svg'; -import { ReactComponent as InfoIcon } from 'images/icons/info.svg'; -import { ReactComponent as InfoSolidIcon } from 'images/icons/info-solid.svg'; -import { ReactComponent as InfoSolidSmallIcon } from 'images/icons/info_solid_small.svg'; -import { ReactComponent as JoinIcon } from 'images/icons/join.svg'; -import { ReactComponent as KeyboardIcon } from 'images/icons/keyboard.svg'; -import { ReactComponent as LayersIcon } from 'images/icons/layers.svg'; -import { ReactComponent as LightbulbIcon } from 'images/icons/lightbulb.svg'; -import { ReactComponent as LinkIcon } from 'images/icons/link.svg'; -import { ReactComponent as ListIcon } from 'images/icons/list.svg'; -import { ReactComponent as ListViewIcon } from 'images/icons/list_view.svg'; -import { ReactComponent as LocationIcon } from 'images/icons/location.svg'; -import { ReactComponent as LockLockedIcon } from 'images/icons/lock_locked.svg'; -import { ReactComponent as LockUnlockedIcon } from 'images/icons/lock_unlocked.svg'; -import { ReactComponent as MapIcon } from 'images/icons/map.svg'; -import { ReactComponent as MessageIcon } from 'images/icons/message.svg'; -import { ReactComponent as MinusIcon } from 'images/icons/minus.svg'; -import { ReactComponent as MinusSolidIcon } from 'images/icons/minus_solid.svg'; -import { ReactComponent as MoreHorizIcon } from 'images/icons/more_horiz.svg'; -import { ReactComponent as MoveIcon } from 'images/icons/move.svg'; -import { ReactComponent as NavChartsIcon } from 'images/icons/nav_charts.svg'; -import { ReactComponent as NavDashboardIcon } from 'images/icons/nav_dashboard.svg'; -import { ReactComponent as NavDataIcon } from 'images/icons/nav_data.svg'; -import { ReactComponent as NavExploreIcon } from 'images/icons/nav_explore.svg'; -import { ReactComponent as NavHomeIcon } from 'images/icons/nav_home.svg'; -import { ReactComponent as NavLabIcon } from 'images/icons/nav_lab.svg'; -import { ReactComponent as NoteIcon } from 'images/icons/note.svg'; -import { ReactComponent as OfflineIcon } from 'images/icons/offline.svg'; -import { ReactComponent as PaperclipIcon } from 'images/icons/paperclip.svg'; -import { ReactComponent as PlaceholderIcon } from 'images/icons/placeholder.svg'; -import { ReactComponent as PlusIcon } from 'images/icons/plus.svg'; -import { ReactComponent as PlusLargeIcon } from 'images/icons/plus_large.svg'; -import { ReactComponent as PlusSmallIcon } from 'images/icons/plus_small.svg'; -import { ReactComponent as PlusSolidIcon } from 'images/icons/plus_solid.svg'; -import { ReactComponent as QueuedIcon } from 'images/icons/queued.svg'; -import { ReactComponent as RefreshIcon } from 'images/icons/refresh.svg'; -import { ReactComponent as RunningIcon } from 'images/icons/running.svg'; -import { ReactComponent as SaveIcon } from 'images/icons/save.svg'; -import { ReactComponent as SQLIcon } from 'images/icons/sql.svg'; -import { ReactComponent as SearchIcon } from 'images/icons/search.svg'; -import { ReactComponent as ServerIcon } from 'images/icons/server.svg'; -import { ReactComponent as ShareIcon } from 'images/icons/share.svg'; -import { ReactComponent as SlackIcon } from 'images/icons/slack.svg'; -import { ReactComponent as SortAscIcon } from 'images/icons/sort_asc.svg'; -import { ReactComponent as SortDescIcon } from 'images/icons/sort_desc.svg'; -import { ReactComponent as SortIcon } from 'images/icons/sort.svg'; -import { ReactComponent as TableIcon } from 'images/icons/table.svg'; -import { ReactComponent as TagIcon } from 'images/icons/tag.svg'; -import { ReactComponent as TrashIcon } from 'images/icons/trash.svg'; -import { ReactComponent as TriangleChangeIcon } from 'images/icons/triangle_change.svg'; -import { ReactComponent as TriangleDownIcon } from 'images/icons/triangle_down.svg'; -import { ReactComponent as TriangleUpIcon } from 'images/icons/triangle_up.svg'; -import { ReactComponent as UpLevelIcon } from 'images/icons/up-level.svg'; -import { ReactComponent as UserIcon } from 'images/icons/user.svg'; -import { ReactComponent as WarningIcon } from 'images/icons/warning.svg'; -import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg'; -import { ReactComponent as XLargeIcon } from 'images/icons/x-large.svg'; -import { ReactComponent as XSmallIcon } from 'images/icons/x-small.svg'; - -import AntdEnhancedIcons from './AntdEnhanced'; -import { renderIcon } from './Icon'; -import IconType from './IconType'; - -export default { - ...AntdEnhancedIcons, - Alert: (props: IconType) => renderIcon(AlertIcon, props), - AlertSolid: (props: IconType) => renderIcon(AlertSolidIcon, props), - AlertSolidSmall: (props: IconType) => renderIcon(AlertSolidSmallIcon, props), - Binoculars: (props: IconType) => renderIcon(BinocularsIcon, props), - Bolt: (props: IconType) => renderIcon(BoltIcon, props), - BoltSmall: (props: IconType) => renderIcon(BoltSmallIcon, props), - BoltSmallRun: (props: IconType) => renderIcon(BoltSmallRunIcon, props), - Calendar: (props: IconType) => renderIcon(CalendarIcon, props), - Cancel: (props: IconType) => renderIcon(CancelIcon, props), - CancelSolid: (props: IconType) => renderIcon(CancelSolidIcon, props), - CancelX: (props: IconType) => renderIcon(CancelXIcon, props), - CardView: (props: IconType) => renderIcon(CardViewIcon, props), - Cards: (props: IconType) => renderIcon(CardsIcon, props), - CardsLocked: (props: IconType) => renderIcon(CardsLockedIcon, props), - CaretDown: (props: IconType) => renderIcon(CaretDownIcon, props), - CaretLeft: (props: IconType) => renderIcon(CaretLeftIcon, props), - CaretRight: (props: IconType) => renderIcon(CaretRightIcon, props), - CaretUp: (props: IconType) => renderIcon(CaretUpIcon, props), - Certified: (props: IconType) => renderIcon(CertifiedIcon, props), - Check: (props: IconType) => renderIcon(CheckIcon, props), - CheckboxHalf: (props: IconType) => renderIcon(CheckboxHalfIcon, props), - CheckboxOff: (props: IconType) => renderIcon(CheckboxOffIcon, props), - CheckboxOn: (props: IconType) => renderIcon(CheckboxOnIcon, props), - CircleCheck: (props: IconType) => renderIcon(CircleCheckIcon, props), - CircleCheckSolid: (props: IconType) => - renderIcon(CircleCheckSolidIcon, props), - Circle: (props: IconType) => renderIcon(CircleIcon, props), - Clock: (props: IconType) => renderIcon(ClockIcon, props), - Close: (props: IconType) => renderIcon(CloseIcon, props), - Code: (props: IconType) => renderIcon(CodeIcon, props), - Cog: (props: IconType) => renderIcon(CogIcon, props), - Collapse: (props: IconType) => renderIcon(CollapseIcon, props), - ColorPalette: (props: IconType) => renderIcon(ColorPaletteIcon, props), - Components: (props: IconType) => renderIcon(ComponentsIcon, props), - Copy: (props: IconType) => renderIcon(CopyIcon, props), - CursorTarget: (props: IconType) => renderIcon(CursorTargeIcon, props), - Database: (props: IconType) => renderIcon(DatabaseIcon, props), - DatasetPhysical: (props: IconType) => renderIcon(DatasetPhysicalIcon, props), - DatasetVirtualGreyscale: (props: IconType) => - renderIcon(DatasetVirtualGreyscaleIcon, props), - DatasetVirtual: (props: IconType) => renderIcon(DatasetVirtualIcon, props), - Download: (props: IconType) => renderIcon(DownloadIcon, props), - EditAlt: (props: IconType) => renderIcon(EditAltIcon, props), - Edit: (props: IconType) => renderIcon(EditIcon, props), - Email: (props: IconType) => renderIcon(EmailIcon, props), - Error: (props: IconType) => renderIcon(ErrorIcon, props), - ErrorSolid: (props: IconType) => renderIcon(ErrorSolidIcon, props), - ErrorSolidSmall: (props: IconType) => renderIcon(ErrorSolidSmallIcon, props), - Exclamation: (props: IconType) => renderIcon(ExclamationIcon, props), - Expand: (props: IconType) => renderIcon(ExpandIcon, props), - Eye: (props: IconType) => renderIcon(EyeIcon, props), - EyeSlash: (props: IconType) => renderIcon(EyeSlashIcon, props), - FavoriteSelected: (props: IconType) => - renderIcon(FavoriteSelectedIcon, props), - FavoriteSmallSelected: (props: IconType) => - renderIcon(FavoriteSmallSelectedIcon, props), - FavoriteUnselected: (props: IconType) => - renderIcon(FavoriteUnselectedIcon, props), - FieldABCIcon: (props: IconType) => renderIcon(FieldABCIcon, props), - FieldBoolean: (props: IconType) => renderIcon(FieldBooleanIcon, props), - FieldDate: (props: IconType) => renderIcon(FieldDateIcon, props), - FieldDerived: (props: IconType) => renderIcon(FieldDerivedIcon, props), - FieldNum: (props: IconType) => renderIcon(FieldNumIcon, props), - FieldStruct: (props: IconType) => renderIcon(FieldStructIcon, props), - File: (props: IconType) => renderIcon(FileIcon, props), - Filter: (props: IconType) => renderIcon(FilterIcon, props), - FilterSmall: (props: IconType) => renderIcon(FilterSmallIcon, props), - Folder: (props: IconType) => renderIcon(FolderIcon, props), - Full: (props: IconType) => renderIcon(FullIcon, props), - Function: (props: IconType) => renderIcon(FunctionIcon, props), - Gear: (props: IconType) => renderIcon(GearIcon, props), - Grid: (props: IconType) => renderIcon(GridIcon, props), - Image: (props: IconType) => renderIcon(ImageIcon, props), - Import: (props: IconType) => renderIcon(ImportIcon, props), - Info: (props: IconType) => renderIcon(InfoIcon, props), - InfoSolid: (props: IconType) => renderIcon(InfoSolidIcon, props), - InfoSolidSmall: (props: IconType) => renderIcon(InfoSolidSmallIcon, props), - Join: (props: IconType) => renderIcon(JoinIcon, props), - Keyboard: (props: IconType) => renderIcon(KeyboardIcon, props), - Layers: (props: IconType) => renderIcon(LayersIcon, props), - Lightbulb: (props: IconType) => renderIcon(LightbulbIcon, props), - Link: (props: IconType) => renderIcon(LinkIcon, props), - List: (props: IconType) => renderIcon(ListIcon, props), - ListView: (props: IconType) => renderIcon(ListViewIcon, props), - Location: (props: IconType) => renderIcon(LocationIcon, props), - LockLocked: (props: IconType) => renderIcon(LockLockedIcon, props), - LockUnlocked: (props: IconType) => renderIcon(LockUnlockedIcon, props), - Map: (props: IconType) => renderIcon(MapIcon, props), - Message: (props: IconType) => renderIcon(MessageIcon, props), - Minus: (props: IconType) => renderIcon(MinusIcon, props), - MinusSolid: (props: IconType) => renderIcon(MinusSolidIcon, props), - MoreHoriz: (props: IconType) => renderIcon(MoreHorizIcon, props), - Move: (props: IconType) => renderIcon(MoveIcon, props), - NavCharts: (props: IconType) => renderIcon(NavChartsIcon, props), - NavDashboard: (props: IconType) => renderIcon(NavDashboardIcon, props), - NavData: (props: IconType) => renderIcon(NavDataIcon, props), - NavExplore: (props: IconType) => renderIcon(NavExploreIcon, props), - NavHome: (props: IconType) => renderIcon(NavHomeIcon, props), - NavLab: (props: IconType) => renderIcon(NavLabIcon, props), - Note: (props: IconType) => renderIcon(NoteIcon, props), - Offline: (props: IconType) => renderIcon(OfflineIcon, props), - Paperclip: (props: IconType) => renderIcon(PaperclipIcon, props), - Placeholder: (props: IconType) => renderIcon(PlaceholderIcon, props), - Plus: (props: IconType) => renderIcon(PlusIcon, props), - PlusLarge: (props: IconType) => renderIcon(PlusLargeIcon, props), - PlusSmall: (props: IconType) => renderIcon(PlusSmallIcon, props), - PlusSolid: (props: IconType) => renderIcon(PlusSolidIcon, props), - Queued: (props: IconType) => renderIcon(QueuedIcon, props), - Refresh: (props: IconType) => renderIcon(RefreshIcon, props), - Running: (props: IconType) => renderIcon(RunningIcon, props), - Save: (props: IconType) => renderIcon(SaveIcon, props), - SQL: (props: IconType) => renderIcon(SQLIcon, props), - Search: (props: IconType) => renderIcon(SearchIcon, props), - Server: (props: IconType) => renderIcon(ServerIcon, props), - Share: (props: IconType) => renderIcon(ShareIcon, props), - Slack: (props: IconType) => renderIcon(SlackIcon, props), - SortAsc: (props: IconType) => renderIcon(SortAscIcon, props), - SortDesc: (props: IconType) => renderIcon(SortDescIcon, props), - Sort: (props: IconType) => renderIcon(SortIcon, props), - Table: (props: IconType) => renderIcon(TableIcon, props), - Tag: (props: IconType) => renderIcon(TagIcon, props), - Trash: (props: IconType) => renderIcon(TrashIcon, props), - TriangleChange: (props: IconType) => renderIcon(TriangleChangeIcon, props), - TriangleDown: (props: IconType) => renderIcon(TriangleDownIcon, props), - TriangleUp: (props: IconType) => renderIcon(TriangleUpIcon, props), - UpLevel: (props: IconType) => renderIcon(UpLevelIcon, props), - User: (props: IconType) => renderIcon(UserIcon, props), - Warning: (props: IconType) => renderIcon(WarningIcon, props), - WarningSolid: (props: IconType) => renderIcon(WarningSolidIcon, props), - XLarge: (props: IconType) => renderIcon(XLargeIcon, props), - XSmall: (props: IconType) => renderIcon(XSmallIcon, props), -}; diff --git a/superset-frontend/src/components/Icons/index.tsx b/superset-frontend/src/components/Icons/index.tsx new file mode 100644 index 00000000000..312891ca8ac --- /dev/null +++ b/superset-frontend/src/components/Icons/index.tsx @@ -0,0 +1,166 @@ +/** + * 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 _ from 'lodash'; +import AntdEnhancedIcons from './AntdEnhanced'; +import Icon from './Icon'; +import IconType from './IconType'; + +const IconFileNames = [ + 'alert', + 'alert_solid', + 'alert_solid_small', + 'binoculars', + 'bolt', + 'bolt_small', + 'bolt_small_run', + 'calendar', + 'cancel', + 'cancel_solid', + 'cancel-x', + 'card_view', + 'cards', + 'cards_locked', + 'caret_down', + 'caret_left', + 'caret_right', + 'caret_up', + 'certified', + 'check', + 'checkbox-half', + 'checkbox-off', + 'checkbox-on', + 'circle_check', + 'circle_check_solid', + 'circle', + 'clock', + 'close', + 'code', + 'cog', + 'collapse', + 'color_palette', + 'components', + 'copy', + 'cursor_target', + 'database', + 'dataset_physical', + 'dataset_virtual_greyscale', + 'dataset_virtual', + 'download', + 'edit_alt', + 'edit', + 'email', + 'error', + 'error_solid', + 'error_solid_small', + 'exclamation', + 'expand', + 'eye', + 'eye_slash', + 'favorite-selected', + 'favorite_small_selected', + 'favorite-unselected', + 'field_abc', + 'field_boolean', + 'field_date', + 'field_derived', + 'field_num', + 'field_struct', + 'file', + 'filter', + 'filter_small', + 'folder', + 'full', + 'function_x', + 'gear', + 'grid', + 'image', + 'import', + 'info', + 'info-solid', + 'info_solid_small', + 'join', + 'keyboard', + 'layers', + 'lightbulb', + 'link', + 'list', + 'list_view', + 'location', + 'lock_locked', + 'lock_unlocked', + 'map', + 'message', + 'minus', + 'minus_solid', + 'more_horiz', + 'move', + 'nav_charts', + 'nav_dashboard', + 'nav_data', + 'nav_explore', + 'nav_home', + 'nav_lab', + 'note', + 'offline', + 'paperclip', + 'placeholder', + 'plus', + 'plus_large', + 'plus_small', + 'plus_solid', + 'queued', + 'refresh', + 'running', + 'save', + 'sql', + 'search', + 'server', + 'share', + 'slack', + 'sort_asc', + 'sort_desc', + 'sort', + 'table', + 'tag', + 'trash', + 'triangle_change', + 'triangle_down', + 'triangle_up', + 'up-level', + 'user', + 'warning', + 'warning_solid', + 'x-large', + 'x-small', +]; + +const iconOverrides: Record = {}; +IconFileNames.forEach(fileName => { + const keyName = _.startCase(fileName).replace(/ /g, ''); + iconOverrides[keyName] = (props: IconType) => ( + + ); +}); + +export default { + ...AntdEnhancedIcons, + ...iconOverrides, +}; diff --git a/superset-frontend/src/components/LastUpdated/LastUpdated.test.tsx b/superset-frontend/src/components/LastUpdated/LastUpdated.test.tsx index 04cdc0ded35..18014d49cec 100644 --- a/superset-frontend/src/components/LastUpdated/LastUpdated.test.tsx +++ b/superset-frontend/src/components/LastUpdated/LastUpdated.test.tsx @@ -34,7 +34,7 @@ describe('LastUpdated', () => { it('renders a refresh action', () => { const mockAction = jest.fn(); wrapper = mount(); - const props = wrapper.find('[data-test="refresh"]').props(); + const props = wrapper.find('[data-test="refresh"]').first().props(); if (props.onClick) { props.onClick({} as React.MouseEvent); } diff --git a/superset-frontend/src/components/Popover/Popover.test.tsx b/superset-frontend/src/components/Popover/Popover.test.tsx index 88e24c1d683..bb9b21c866f 100644 --- a/superset-frontend/src/components/Popover/Popover.test.tsx +++ b/superset-frontend/src/components/Popover/Popover.test.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { supersetTheme } from '@superset-ui/core'; -import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import Button from 'src/components/Button'; import Popover from '.'; @@ -47,15 +47,13 @@ test('it should not render a title or content when not visible', () => { expect(title).not.toBeInTheDocument(); }); -test('renders with icon child', () => { +test('renders with icon child', async () => { render( - - Click me - + Click me , ); - expect(screen.getByRole('img')).toBeInTheDocument(); + expect(await screen.findByRole('img')).toBeInTheDocument(); }); test('fires an event when visibility is changed', async () => { diff --git a/superset-frontend/src/components/Tooltip/Tooltip.test.tsx b/superset-frontend/src/components/Tooltip/Tooltip.test.tsx index 1aec5a23ba2..458e442b85c 100644 --- a/superset-frontend/src/components/Tooltip/Tooltip.test.tsx +++ b/superset-frontend/src/components/Tooltip/Tooltip.test.tsx @@ -21,7 +21,7 @@ import { render, screen } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { supersetTheme } from '@superset-ui/core'; import Button from 'src/components/Button'; -import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import { Tooltip } from '.'; test('starts hidden with default props', () => { @@ -62,9 +62,7 @@ test('renders with theme', () => { test('renders with icon child', async () => { render( - - Hover me - + Hover me , ); userEvent.hover(screen.getByRole('img')); diff --git a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx index abd57d3f324..640397e5c65 100644 --- a/superset-frontend/src/components/WarningIconWithTooltip/index.tsx +++ b/superset-frontend/src/components/WarningIconWithTooltip/index.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { useTheme, SafeMarkdown } from '@superset-ui/core'; -import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import { Tooltip } from 'src/components/Tooltip'; export interface WarningIconWithTooltipProps { @@ -28,7 +28,6 @@ export interface WarningIconWithTooltipProps { function WarningIconWithTooltip({ warningMarkdown, - size = 24, }: WarningIconWithTooltipProps) { const theme = useTheme(); return ( @@ -36,12 +35,7 @@ function WarningIconWithTooltip({ id="warning-tooltip" title={} > - + ); } diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx index cad111ef1b7..506dc5216fe 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/index.tsx @@ -19,6 +19,7 @@ import React from 'react'; import cx from 'classnames'; import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import DetailsPanelPopover from './DetailsPanel'; import { Pill } from './Styles'; import { Indicator } from './selectors'; @@ -77,7 +78,7 @@ const FiltersBadge = ({ {incompatibleIndicators.length ? ( <> {' '} - + {incompatibleIndicators.length} diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx index c8f61ad3eb2..0d065958339 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx @@ -143,7 +143,7 @@ test('should search and render matching metrics', async () => { }); }); -test('should render a warning', () => { +test('should render a warning', async () => { const deprecatedDatasource = { ...datasource, extra: JSON.stringify({ warning_markdown: 'This is a warning.' }), @@ -160,5 +160,7 @@ test('should render a warning', () => { }, }), ); - expect(screen.getByTestId('alert-solid')).toBeInTheDocument(); + expect( + await screen.findByRole('img', { name: 'alert-solid' }), + ).toBeInTheDocument(); }); diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 0cadaf0d1fc..616cc79f7ca 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -23,6 +23,7 @@ import { t, styled, supersetTheme } from '@superset-ui/core'; import { Dropdown, Menu } from 'src/common/components'; import { Tooltip } from 'src/components/Tooltip'; import Icon from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal'; import DatasourceModal from 'src/datasource/DatasourceModal'; import { postForm } from 'src/explore/exploreUtils'; @@ -202,10 +203,7 @@ class DatasourceControl extends React.PureComponent { )} {healthCheckMessage && ( - + )} {extra?.warning_markdown && ( // eslint-disable-line camelcase diff --git a/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx b/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx index 712f6109e36..b769bdb981c 100644 --- a/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx +++ b/superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx @@ -16,34 +16,34 @@ * specific language governing permissions and limitations * under the License. */ -import { styled, t } from '@superset-ui/core'; +import { t, supersetTheme, withTheme } from '@superset-ui/core'; import React from 'react'; import { Tooltip } from 'src/components/Tooltip'; -import Icon, { IconName } from 'src/components/Icon'; +import Icons from 'src/components/Icons'; import { AlertState } from '../types'; -const StatusIcon = styled(Icon, { - shouldForwardProp: prop => prop !== 'status' && prop !== 'isReportEnabled', -})<{ status: string; isReportEnabled: boolean }>` - color: ${({ status, theme, isReportEnabled }) => { - switch (status) { - case AlertState.working: - return theme.colors.primary.base; - case AlertState.error: - return theme.colors.error.base; - case AlertState.success: - return isReportEnabled - ? theme.colors.success.base - : theme.colors.alert.base; - case AlertState.noop: - return theme.colors.success.base; - case AlertState.grace: - return theme.colors.alert.base; - default: - return theme.colors.grayscale.base; - } - }}; -`; +function getStatusColor( + status: string, + isReportEnabled: boolean, + theme: typeof supersetTheme, +) { + switch (status) { + case AlertState.working: + return theme.colors.primary.base; + case AlertState.error: + return theme.colors.error.base; + case AlertState.success: + return isReportEnabled + ? theme.colors.success.base + : theme.colors.alert.base; + case AlertState.noop: + return theme.colors.success.base; + case AlertState.grace: + return theme.colors.alert.base; + default: + return theme.colors.grayscale.base; + } +} export default function AlertStatusIcon({ state, @@ -53,59 +53,58 @@ export default function AlertStatusIcon({ isReportEnabled: boolean; }) { const lastStateConfig = { - name: '', + icon: Icons.Check, label: '', status: '', }; switch (state) { case AlertState.success: - lastStateConfig.name = isReportEnabled ? 'check' : 'alert-solid-small'; + lastStateConfig.icon = isReportEnabled + ? Icons.Check + : Icons.AlertSolidSmall; lastStateConfig.label = isReportEnabled ? t('Report sent') : t('Alert triggered, notification sent'); lastStateConfig.status = AlertState.success; break; case AlertState.working: - lastStateConfig.name = 'running'; + lastStateConfig.icon = Icons.Running; lastStateConfig.label = isReportEnabled ? t('Report sending') : t('Alert running'); lastStateConfig.status = AlertState.working; break; case AlertState.error: - lastStateConfig.name = 'x-small'; + lastStateConfig.icon = Icons.XSmall; lastStateConfig.label = isReportEnabled ? t('Report failed') : t('Alert failed'); lastStateConfig.status = AlertState.error; break; case AlertState.noop: - lastStateConfig.name = 'check'; + lastStateConfig.icon = Icons.Check; lastStateConfig.label = t('Nothing triggered'); lastStateConfig.status = AlertState.noop; break; case AlertState.grace: - lastStateConfig.name = 'alert-solid-small'; + lastStateConfig.icon = Icons.AlertSolidSmall; lastStateConfig.label = t('Alert Triggered, In Grace Period'); lastStateConfig.status = AlertState.grace; break; default: - lastStateConfig.name = 'check'; + lastStateConfig.icon = Icons.Check; lastStateConfig.label = t('Nothing triggered'); lastStateConfig.status = AlertState.noop; } + const Icon = lastStateConfig.icon; + const AlertIcon = withTheme(({ theme }) => ( + + )); return ( - + ); }