1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +02:00

refactor(icons): replace fa icons [EE-4459] (#7907)

refactor(icons): remove fontawesome EE-4459

refactor(icon) replace feather with lucide EE-4472
This commit is contained in:
Ali 2022-11-28 15:00:28 +13:00 committed by GitHub
parent 9dfac98a26
commit d78b762f7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
498 changed files with 2102 additions and 2817 deletions

View file

@ -1,11 +1,13 @@
import { PropsWithChildren } from 'react';
import clsx from 'clsx';
import { Briefcase } from 'react-feather';
import { Briefcase } from 'lucide-react';
import './BEFeatureIndicator.css';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
import { Icon } from '@@/Icon';
import { getFeatureDetails } from './utils';
export interface Props {
@ -33,8 +35,8 @@ export function BEFeatureIndicator({
rel="noopener noreferrer"
>
{children}
{showIcon && <Briefcase className="icon icon-sm vertical-center" />}
<span className="be-indicator-label break-words space-left">
{showIcon && <Icon icon={Briefcase} className="mr-1 be-indicator-icon" />}
<span className="be-indicator-label break-words">
Business Edition Feature
</span>
</a>

View file

@ -29,7 +29,7 @@ function Template({
size?: BadgeSize;
icon: string;
}) {
return <BadgeIcon icon={icon} size={size} featherIcon />;
return <BadgeIcon icon={icon} size={size} />;
}
export const Example = Template.bind({});

View file

@ -8,7 +8,7 @@ export interface Props extends IconProps {
size?: BadgeSize;
}
export function BadgeIcon({ icon, featherIcon, size = '3xl' }: Props) {
export function BadgeIcon({ icon, size = '3xl' }: Props) {
const sizeClasses = iconSizeToClasses(size);
return (
<div
@ -22,7 +22,7 @@ export function BadgeIcon({ icon, featherIcon, size = '3xl' }: Props) {
`
)}
>
<Icon icon={icon} feather={featherIcon} className="feather !flex" />
<Icon icon={icon} className="!flex" />
</div>
);
}

View file

@ -1,5 +1,6 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { User } from 'lucide-react';
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
@ -21,14 +22,14 @@ function Example() {
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
icon: User,
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
icon: User,
id: '2',
value: 4,
label: 'option 2',
@ -53,14 +54,14 @@ function LimitedFeature() {
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
icon: User,
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
icon: User,
id: '2',
value: 4,
label: 'option 2',

View file

@ -1,3 +1,5 @@
import { Rocket } from 'lucide-react';
import { render, fireEvent } from '@/react-tools/test-utils';
import { BoxSelector, Props } from './BoxSelector';
@ -23,14 +25,14 @@ test('should render with the initial value selected and call onChange when click
const options: BoxSelectorOption<number>[] = [
{
description: 'description 1',
icon: 'fa fa-rocket',
icon: Rocket,
id: '1',
value: 3,
label: 'option 1',
},
{
description: 'description 2',
icon: 'fa fa-rocket',
icon: Rocket,
id: '2',
value: 4,
label: 'option 2',

View file

@ -13,11 +13,6 @@
color: var(--text-boxselector-header);
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'],
.box-selector-item input[type='radio'] {
display: none;

View file

@ -1,8 +1,11 @@
import { Meta } from '@storybook/react';
import { User } from 'lucide-react';
import { init as initFeatureService } from '@/react/portainer/feature-flags/feature-flags.service';
import { Edition, FeatureId } from '@/react/portainer/feature-flags/enums';
import { IconProps } from '@@/Icon';
import { BoxSelectorItem } from './BoxSelectorItem';
import { BoxSelectorOption } from './types';
@ -11,7 +14,7 @@ const meta: Meta = {
args: {
selected: false,
description: 'description',
icon: 'fa-rocket',
icon: User,
label: 'label',
},
};
@ -21,7 +24,7 @@ export default meta;
interface ExampleProps {
selected?: boolean;
description?: string;
icon?: string;
icon?: IconProps['icon'];
label?: string;
feature?: FeatureId;
}
@ -35,7 +38,7 @@ function Template({
}: ExampleProps) {
const option: BoxSelectorOption<number> = {
description,
icon: `fa ${icon}`,
icon,
id: 'id',
label,
value: 1,

View file

@ -54,7 +54,6 @@ export function BoxSelectorItem<T extends number | string>({
{!!option.icon && (
<Icon
icon={option.icon}
feather={option.featherIcon}
className="boxselector_icon !flex items-center"
/>
)}

View file

@ -1,5 +1,5 @@
import ReactTooltip from 'react-tooltip';
import { HelpCircle } from 'react-feather';
import { HelpCircle } from 'lucide-react';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
@ -21,7 +21,7 @@ export function LimitedToBeIndicator({ tooltipId, featureId }: Props) {
<span className="text-warning-9">Pro Feature</span>
</a>
<HelpCircle
className="feather !text-warning-7"
className="lucide !text-warning-7"
data-tip
data-for={tooltipId}
tooltip-append-to-body="true"

View file

@ -2,15 +2,15 @@ import { Icon, IconProps } from '@@/Icon';
type Props = IconProps;
export function LogoIcon({ icon, featherIcon }: Props) {
export function LogoIcon({ icon }: Props) {
return (
<div
className={`
text-6xl h-14 w-14
inline-flex items-center justify-center
inline-flex items-center justify-center
`}
>
<Icon icon={icon} feather={featherIcon} className="feather !flex" />
<Icon icon={icon} className="!flex" />
</div>
);
}

View file

@ -1,4 +1,4 @@
import { Edit, FileText, Globe, Upload } from 'react-feather';
import { Edit, FileText, Globe, Upload } from 'lucide-react';
import GitIcon from '@/assets/ico/git.svg?c';

View file

@ -1,7 +1,8 @@
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';
import { Button } from '@@/buttons';
import { useCopy } from '@@/buttons/CopyButton/useCopy';
import { Icon } from '@@/Icon';
import styles from './Code.module.css';
@ -19,12 +20,10 @@ export function Code({ children, showCopyButton }: Props) {
{showCopyButton && (
<Button color="link" className={styles.copyButton} onClick={handleCopy}>
<i
className={clsx(
'fa',
copiedSuccessfully ? 'fa-check green-icon' : 'fa-copy '
)}
aria-hidden="true"
<Icon
icon={copiedSuccessfully ? Check : Copy}
className="!ml-1"
mode={copiedSuccessfully ? 'success' : undefined}
/>
</Button>
)}

View file

@ -1,6 +1,8 @@
import { Meta, Story } from '@storybook/react';
import { List } from 'lucide-react';
import { Link } from '@@/Link';
import { IconProps } from '@@/Icon';
import { DashboardItem } from './DashboardItem';
@ -12,7 +14,7 @@ export default meta;
interface StoryProps {
value: number;
icon: string;
icon: IconProps['icon'];
type: string;
}
@ -23,21 +25,21 @@ function Template({ value, icon, type }: StoryProps) {
export const Primary: Story<StoryProps> = Template.bind({});
Primary.args = {
value: 1,
icon: 'fa fa-th-list',
icon: List,
type: 'Example resource',
};
export function WithLink() {
return (
<Link to="example.page">
<DashboardItem value={1} icon="fa fa-th-list" type="Example resource" />
<DashboardItem value={1} icon={List} type="Example resource" />
</Link>
);
}
export function WithChildren() {
return (
<DashboardItem value={1} icon="fa fa-th-list" type="Example resource">
<DashboardItem value={1} icon={List} type="Example resource">
<div>Children</div>
</DashboardItem>
);

View file

@ -1,3 +1,5 @@
import { User } from 'lucide-react';
import { render } from '@/react-tools/test-utils';
import { DashboardItem } from './DashboardItem';
@ -11,7 +13,7 @@ test('should show provided resource value', async () => {
});
test('should show provided resource type', async () => {
const { getByLabelText } = renderComponent(0, '', 'Test');
const { getByLabelText } = renderComponent(0, User, 'Test');
const title = getByLabelText('resourceType');
expect(title).toBeVisible();
@ -19,11 +21,11 @@ test('should show provided resource type', async () => {
});
test('should have accessibility label created from the provided resource type', async () => {
const { getByLabelText } = renderComponent(0, '', 'testLabel');
const { getByLabelText } = renderComponent(0, User, 'testLabel');
expect(getByLabelText('testLabel')).toBeTruthy();
});
function renderComponent(value = 0, icon = '', type = '') {
function renderComponent(value = 0, icon = User, type = '') {
return render(<DashboardItem value={value} icon={icon} type={type} />);
}

View file

@ -10,13 +10,7 @@ interface Props extends IconProps {
children?: ReactNode;
}
export function DashboardItem({
value,
icon,
type,
children,
featherIcon,
}: Props) {
export function DashboardItem({ value, icon, type, children }: Props) {
return (
<div
className={clsx(
@ -35,7 +29,7 @@ export function DashboardItem({
'th-highcontrast:bg-blue-3 th-highcontrast:text-blue-8'
)}
>
<Icon icon={icon} feather={featherIcon} className="feather" />
<Icon icon={icon} />
</div>
<div className="flex flex-col justify-around">

View file

@ -9,8 +9,6 @@ interface Props {
alt?: string;
size?: BadgeSize;
className?: string;
// additional fallback badge props
feather?: boolean;
}
export function FallbackImage({
@ -19,7 +17,6 @@ export function FallbackImage({
alt,
size,
className,
feather,
}: Props) {
const [error, setError] = useState(false);
@ -39,5 +36,5 @@ export function FallbackImage({
}
// fallback icon if there is an error loading the image
return <BadgeIcon icon={fallbackIcon} featherIcon={feather} size={size} />;
return <BadgeIcon icon={fallbackIcon} size={size} />;
}

View file

@ -1,13 +1,12 @@
import clsx from 'clsx';
import { ComponentType, ReactNode } from 'react';
import * as featherIcons from 'react-feather';
import * as lucideIcons from 'lucide-react';
import { isValidElementType } from 'react-is';
import Svg, { SvgIcons } from './Svg';
export interface IconProps {
icon: ReactNode | ComponentType<unknown>;
featherIcon?: boolean;
}
export type IconMode =
@ -27,16 +26,15 @@ export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
interface Props {
icon: ReactNode | ComponentType<{ size?: string | number }>;
feather?: boolean;
className?: string;
size?: IconSize;
mode?: IconMode;
}
export function Icon({ icon, feather, className, mode, size }: Props) {
export function Icon({ icon, className, mode, size }: Props) {
const classes = clsx(
className,
'icon',
'icon inline-flex',
{ [`icon-${mode}`]: mode },
{ [`icon-${size}`]: size }
);
@ -44,9 +42,13 @@ export function Icon({ icon, feather, className, mode, size }: Props) {
if (typeof icon !== 'string') {
const Icon = isValidElementType(icon) ? icon : null;
if (Icon) {
return <Icon className={classes} aria-hidden="true" role="img" />;
}
return (
<span className={classes} aria-hidden="true" role="img">
{Icon == null ? <>{icon}</> : <Icon size="1em" />}
{icon}
</span>
);
}
@ -56,28 +58,25 @@ export function Icon({ icon, feather, className, mode, size }: Props) {
return (
<Svg
icon={svgIcon as keyof typeof SvgIcons}
className={clsx(classes, '!flex')}
className={classes}
aria-hidden="true"
/>
);
}
if (feather) {
const iconName = icon
.split('-')
.map((s) => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('') as keyof typeof featherIcons;
const IconComponent = featherIcons[iconName];
if (!IconComponent) {
throw new Error(`Feather icon not found: ${iconName}`);
}
return <IconComponent className={classes} />;
const iconName = icon
.split('-')
.map((s) => s.slice(0, 1).toUpperCase() + s.slice(1))
.join('') as keyof typeof lucideIcons;
const IconComponent = lucideIcons[iconName] as React.FC<
React.SVGProps<SVGSVGElement>
>;
if (!IconComponent) {
// console error so that the error is logged but no functionality is broken
// eslint-disable-next-line no-console
console.error(`Icon not found: '${icon}'`);
return null;
}
return (
<i
className={clsx(icon.startsWith('fa-') ? `fa ${icon}` : icon, classes)}
aria-hidden="true"
role="img"
/>
);
return <IconComponent className={classes} aria-hidden="true" />;
}

View file

@ -1,4 +1,5 @@
import { PropsWithChildren } from 'react';
import { X } from 'lucide-react';
import { Widget, WidgetBody } from './Widget';
import { Button } from './buttons';
@ -32,8 +33,8 @@ export function InformationPanel({
style={{ float: 'right' }}
ng-if="dismissAction"
>
<Button color="link" onClick={() => onDismiss()}>
<i className="fa fa-times" /> dismiss
<Button color="link" icon={X} onClick={() => onDismiss()}>
dismiss
</Button>
</span>
)}

View file

@ -4,7 +4,7 @@ import { ReactNode } from 'react';
import styles from './NavTabs.module.css';
export interface Option<T extends string | number = string> {
label: string | ReactNode;
label: ReactNode;
children?: ReactNode;
id: T;
}

View file

@ -1,4 +1,4 @@
import { HelpCircle } from 'react-feather';
import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import { getDocURL } from '@@/PageHeader/ContextHelp/docURLs';
@ -24,7 +24,7 @@ export function ContextHelp() {
)}
title="Help"
>
<HelpCircle className="feather" onClick={onHelpClick} />
<HelpCircle className="lucide" onClick={onHelpClick} />
</div>
</div>
);

View file

@ -9,6 +9,7 @@ import { UISrefProps, useSref } from '@uirouter/react';
import Moment from 'moment';
import { useEffect, useState } from 'react';
import { useStore } from 'zustand';
import { AlertCircle, Bell, CheckCircle, Trash2 } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { useUser } from '@/react/hooks/useUser';
@ -69,7 +70,7 @@ export function NotificationsMenu() {
'th-dark:text-gray-warm-7'
)}
>
<Icon icon="bell" feather />
<Icon icon={Bell} />
<span className={badge ? notificationStyles.badge : ''} />
</div>
</MenuButton>
@ -126,7 +127,7 @@ export function NotificationsMenu() {
</>
) : (
<div className="flex flex-col items-center">
<Icon icon="bell" feather size="xl" />
<Icon icon={Bell} size="xl" />
<p className="my-5">You have no notifications yet.</p>
</div>
)}
@ -160,9 +161,9 @@ function MenuLink({ to, params, notification, onDelete }: MenuLinkProps) {
<div className={notificationStyles.container}>
<div className={notificationStyles.notificationIcon}>
{notification.type === 'success' ? (
<Icon icon="check-circle" feather size="lg" mode="success" />
<Icon icon={CheckCircle} size="lg" mode="success" />
) : (
<Icon icon="alert-circle" feather size="lg" mode="danger" />
<Icon icon={AlertCircle} size="lg" mode="danger" />
)}
</div>
<div className={notificationStyles.notificationBody}>
@ -186,9 +187,8 @@ function MenuLink({ to, params, notification, onDelete }: MenuLinkProps) {
}}
data-cy="notification-deleteButton"
size="large"
>
<Icon icon="trash-2" feather />
</Button>
icon={Trash2}
/>
</div>
</div>
</ReachMenuLink>

View file

@ -1,5 +1,5 @@
import { useRouter } from '@uirouter/react';
import { RefreshCw } from 'react-feather';
import { RefreshCw } from 'lucide-react';
import { Button } from '../buttons';

View file

@ -6,7 +6,7 @@ import {
} from '@reach/menu-button';
import { UISrefProps, useSref } from '@uirouter/react';
import clsx from 'clsx';
import { User, ChevronDown } from 'react-feather';
import { User, ChevronDown } from 'lucide-react';
import { AutomationTestingProps } from '@/types';
import { useUser } from '@/react/hooks/useUser';
@ -34,7 +34,7 @@ export function UserMenu() {
'th-dark:text-gray-warm-7'
)}
>
<User className="feather" />
<User className="lucide" />
</div>
{user && <span>{user.Username}</span>}
<ChevronDown className={styles.arrowDown} />

View file

@ -1,3 +1,5 @@
import { AlertTriangle, Check } from 'lucide-react';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { Icon } from '@@/Icon';
@ -17,12 +19,12 @@ export function PasswordCheckHint({
return (
<div>
<p className="text-warning vertical-center">
<Icon icon="alert-triangle" className="icon-warning" feather />
<Icon icon={AlertTriangle} className="icon-warning" />
{forceChangePassword &&
'An administrator has changed your password requirements, '}
The password must be at least {minPasswordLength} characters long.
{passwordValid && (
<i className="fa fa-check green-icon space-left" aria-hidden="true" />
<Icon icon={Check} className="!ml-1" mode="success" />
)}
</p>
</div>

View file

@ -3,57 +3,19 @@ import automode from '@/assets/ico/theme/auto.svg?c';
import darkmode from '@/assets/ico/theme/darkmode.svg?c';
import lightmode from '@/assets/ico/theme/lightmode.svg?c';
import highcontrastmode from '@/assets/ico/theme/highcontrastmode.svg?c';
// wizard icons
import agent from '@/assets/ico/wizard/agent.svg?c';
import api from '@/assets/ico/wizard/api.svg?c';
import edgeagent from '@/assets/ico/wizard/edge-agent.svg?c';
import cloudimport from '@/assets/ico/wizard/import.svg?c';
import socket from '@/assets/ico/wizard/socket.svg?c';
// general icons
import arrowsupdown from '@/assets/ico/arrows-updown.svg?c';
import arrowright from '@/assets/ico/arrow-right-long.svg?c';
import bomb from '@/assets/ico/bomb.svg?c';
import checked from '@/assets/ico/checked.svg?c';
import circlenotch from '@/assets/ico/circle-notch.svg?c';
import clockrewind from '@/assets/ico/clock-rewind.svg?c';
import compress from '@/assets/ico/compress.svg?c';
import cubes from '@/assets/ico/cubes.svg?c';
import custom from '@/assets/ico/custom.svg?c';
import dataflow from '@/assets/ico/dataflow-1.svg?c';
import dataflow2 from '@/assets/ico/dataflow-2.svg?c';
import expand from '@/assets/ico/expand.svg?c';
import filecode from '@/assets/ico/file-code.svg?c';
import filesignature from '@/assets/ico/file-signature.svg?c';
import fileupload from '@/assets/ico/file-upload.svg?c';
import flask from '@/assets/ico/flask.svg?c';
import git from '@/assets/ico/git.svg?c';
import hacker from '@/assets/ico/hacker.svg?c';
import heartbeat from '@/assets/ico/heartbeat.svg?c';
import laptop from '@/assets/ico/laptop.svg?c';
import kube from '@/assets/ico/kube.svg?c';
import laptopcode from '@/assets/ico/laptop-code.svg?c';
import ldap from '@/assets/ico/ldap.svg?c';
import magic from '@/assets/ico/magic.svg?c';
import magicwand from '@/assets/ico/magic-wand.svg?c';
import linux from '@/assets/ico/linux.svg?c';
import memory from '@/assets/ico/memory.svg?c';
import objectgroup from '@/assets/ico/object-group.svg?c';
import palette from '@/assets/ico/palette.svg?c';
import plug from '@/assets/ico/plug.svg?c';
import restore from '@/assets/ico/restore.svg?c';
import restorewindow from '@/assets/ico/restore-window.svg?c';
import rocket from '@/assets/ico/rocket.svg?c';
import route from '@/assets/ico/route.svg?c';
import share from '@/assets/ico/share.svg?c';
import sort from '@/assets/ico/sort.svg?c';
import tachometer from '@/assets/ico/tachometer.svg?c';
import template from '@/assets/ico/template.svg?c';
import tag from '@/assets/ico/tag-2.svg?c';
import tag2 from '@/assets/ico/tags.svg?c';
import tools from '@/assets/ico/tools.svg?c';
import upload from '@/assets/ico/upload.svg?c';
import url from '@/assets/ico/url.svg?c';
import usercircle from '@/assets/ico/user-circle.svg?c';
import userlock from '@/assets/ico/user-lock.svg?c';
import kube from '@/assets/ico/kube.svg?c';
import subscription from '@/assets/ico/subscription.svg?c';
import Placeholder from '@/assets/ico/placeholder.svg?c'; // Placeholder is used when an icon name cant be matched
// vendor icons
import aws from '@/assets/ico/vendor/aws.svg?c';
@ -61,6 +23,7 @@ import azure from '@/assets/ico/vendor/azure.svg?c';
import civo from '@/assets/ico/vendor/civo.svg?c';
import digitalocean from '@/assets/ico/vendor/digitalocean.svg?c';
import docker from '@/assets/ico/vendor/docker.svg?c';
import dockericon from '@/assets/ico/vendor/docker-icon.svg?c';
import dockercompose from '@/assets/ico/vendor/docker-compose.svg?c';
import ecr from '@/assets/ico/vendor/ecr.svg?c';
import github from '@/assets/ico/vendor/github.svg?c';
@ -71,68 +34,33 @@ import kubernetes from '@/assets/ico/vendor/kubernetes.svg?c';
import helm from '@/assets/ico/vendor/helm.svg?c';
import linode from '@/assets/ico/vendor/linode.svg?c';
import microsoft from '@/assets/ico/vendor/microsoft.svg?c';
import microsofticon from '@/assets/ico/vendor/microsoft-icon.svg?c';
import nomad from '@/assets/ico/vendor/nomad.svg?c';
import nomadicon from '@/assets/ico/vendor/nomad-icon.svg?c';
import openldap from '@/assets/ico/vendor/openldap.svg?c';
import proget from '@/assets/ico/vendor/proget.svg?c';
import quay from '@/assets/ico/vendor/quay.svg?c';
import internal from '@/assets/ico/vendor/internal.svg?c';
const placeholder = Placeholder;
export const SvgIcons = {
agent,
api,
edgeagent,
cloudimport,
socket,
automode,
darkmode,
lightmode,
highcontrastmode,
dataflow,
dataflow2,
arrowsupdown,
arrowright,
bomb,
checked,
circlenotch,
clockrewind,
compress,
cubes,
custom,
expand,
filecode,
filesignature,
fileupload,
flask,
dockericon,
git,
hacker,
heartbeat,
laptop,
laptopcode,
ldap,
magic,
magicwand,
linux,
memory,
objectgroup,
palette,
placeholder,
plug,
restore,
restorewindow,
rocket,
route,
share,
sort,
tachometer,
template,
tag,
tag2,
tools,
upload,
url,
usercircle,
userlock,
subscription,
aws,
azure,
civo,
@ -148,11 +76,12 @@ export const SvgIcons = {
helm,
linode,
microsoft,
microsofticon,
nomad,
nomadicon,
openldap,
proget,
quay,
internal,
kube,
};

View file

@ -1,5 +1,6 @@
import clsx from 'clsx';
import _ from 'lodash';
import { Trash2 } from 'lucide-react';
import { TagId } from '@/portainer/tags/types';
import { Icon } from '@/react/components/Icon';
@ -74,7 +75,7 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
key={tag.value}
>
{tag.label}
<Icon icon="trash-2" feather />
<Icon icon={Trash2} />
</button>
))}
</FormControl>

View file

@ -1,5 +1,6 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { AlertCircle } from 'lucide-react';
import { Icon } from '@@/Icon';
@ -30,8 +31,7 @@ export function TextTip({
<p className="small vertical-center">
<i className="icon-container">
<Icon
icon="alert-circle"
feather
icon={AlertCircle}
className={clsx(`${iconClass}`, 'space-right')}
/>
</i>

View file

@ -1,5 +1,5 @@
import ReactTooltip from 'react-tooltip';
import { HelpCircle } from 'react-feather';
import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import _ from 'lodash';
@ -22,7 +22,7 @@ export function Tooltip({ message, position = 'bottom', className }: Props) {
data-for={id}
className={clsx(styles.icon, 'inline-flex text-base')}
>
<HelpCircle className="feather" aria-hidden="true" />
<HelpCircle className="lucide" aria-hidden="true" />
<ReactTooltip
id={id}
multiline

View file

@ -1,4 +1,7 @@
import clsx from 'clsx';
import { Settings } from 'lucide-react';
import { Icon } from '@@/Icon';
import styles from './ViewLoading.module.css';
@ -18,7 +21,7 @@ export function ViewLoading({ message }: Props) {
{message && (
<span className={styles.message}>
{message}
<i className="fa fa-cog fa-spin space-left" />
<Icon icon={Settings} className="animate-spin-slow !ml-1" />
</span>
)}
</div>

View file

@ -1,4 +1,6 @@
import { ReactNode } from 'react';
import type { Meta } from '@storybook/react';
import { User } from 'lucide-react';
import { Widget } from './Widget';
import { WidgetBody } from './WidgetBody';
@ -9,7 +11,7 @@ import { WidgetTaskbar } from './WidgetTaskbar';
interface WidgetProps {
loading: boolean;
title: string;
icon: string;
icon: ReactNode;
bodyText: string;
footerText: string;
}
@ -20,7 +22,7 @@ const meta: Meta<WidgetProps> = {
args: {
loading: false,
title: 'Title',
icon: 'fa-rocket',
icon: User,
bodyText: 'Body',
footerText: 'Footer',
},
@ -52,11 +54,15 @@ function WidgetWithCustomImage({
<WidgetTitle
title={title}
icon={
<img
className="custom-header-ico space-right"
src={icon}
alt="header-icon"
/>
typeof icon === 'string' ? (
<img
className="custom-header-ico space-right"
src={icon}
alt="header-icon"
/>
) : (
icon
)
}
/>
<WidgetBody loading={loading}>{bodyText}</WidgetBody>

View file

@ -8,7 +8,6 @@ import { useWidgetContext } from './Widget';
interface Props {
title: ReactNode;
icon: ReactNode;
featherIcon?: boolean;
className?: string;
}
@ -17,7 +16,6 @@ export function WidgetTitle({
icon,
className,
children,
featherIcon,
}: PropsWithChildren<Props>) {
useWidgetContext();
@ -26,11 +24,7 @@ export function WidgetTitle({
<div className="row">
<span className={clsx('pull-left vertical-center', className)}>
<div className="widget-icon">
<Icon
icon={icon}
feather={featherIcon}
className="space-right feather"
/>
<Icon icon={icon} className="space-right" />
</div>
<span>{title}</span>
</span>

View file

@ -1,4 +1,5 @@
import clsx from 'clsx';
import { PlusCircle } from 'lucide-react';
import { Icon } from '@/react/components/Icon';
@ -27,7 +28,7 @@ export function AddButton({ label, onClick, className, disabled }: Props) {
onClick={onClick}
disabled={disabled}
>
<Icon icon="plus-circle" feather />
<Icon icon={PlusCircle} />
{label}
</button>
);

View file

@ -1,6 +1,6 @@
import { Meta, Story } from '@storybook/react';
import { PropsWithChildren } from 'react';
import { Download } from 'react-feather';
import { Download } from 'lucide-react';
import { Button, Props } from './Button';

View file

@ -27,7 +27,6 @@ type Size = 'xsmall' | 'small' | 'medium' | 'large';
export interface Props extends AriaAttributes, AutomationTestingProps {
icon?: ReactNode | ComponentType<unknown>;
featherIcon?: boolean;
color?: Color;
size?: Size;
@ -47,7 +46,6 @@ export function Button({
onClick,
title,
icon,
featherIcon,
children,
...ariaProps
@ -64,12 +62,7 @@ export function Button({
{...ariaProps}
>
{icon && (
<Icon
icon={icon}
size={getIconSize(size)}
className="inline-flex"
feather={featherIcon}
/>
<Icon icon={icon} size={getIconSize(size)} className="inline-flex" />
)}
{children}
</button>

View file

@ -1,5 +1,6 @@
import { Meta, Story } from '@storybook/react';
import { PropsWithChildren } from 'react';
import { Play, RefreshCw, Square, Trash2 } from 'lucide-react';
import { Button } from './Button';
import { ButtonGroup, Props } from './ButtonGroup';
@ -14,28 +15,19 @@ function Template({
}: JSX.IntrinsicAttributes & PropsWithChildren<Props>) {
return (
<ButtonGroup size={size}>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-stop space-right" aria-hidden="true" />
<Button icon={Square} color="danger" onClick={() => {}}>
Stop
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-bomb space-right" aria-hidden="true" />
Kill
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-sync space-right" aria-hidden="true" />
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
Restart
</Button>
<Button color="primary" disabled onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" disabled onClick={() => {}}>
Resume
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-trash-alt space-right" aria-hidden="true" />
<Button icon={Trash2} color="danger" onClick={() => {}}>
Remove
</Button>
</ButtonGroup>
@ -50,20 +42,16 @@ Primary.args = {
export function Xsmall() {
return (
<ButtonGroup size="xsmall">
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-stop space-right" aria-hidden="true" />
<Button icon={Square} color="danger" onClick={() => {}}>
Stop
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-sync space-right" aria-hidden="true" />
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
Restart
</Button>
</ButtonGroup>
@ -73,20 +61,16 @@ export function Xsmall() {
export function Small() {
return (
<ButtonGroup size="small">
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-stop space-right" aria-hidden="true" />
<Button icon={Square} color="danger" onClick={() => {}}>
Stop
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-sync space-right" aria-hidden="true" />
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
Restart
</Button>
</ButtonGroup>
@ -96,20 +80,16 @@ export function Small() {
export function Large() {
return (
<ButtonGroup size="large">
<Button color="primary" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="primary" onClick={() => {}}>
Start
</Button>
<Button color="danger" onClick={() => {}}>
<i className="fa fa-stop space-right" aria-hidden="true" />
<Button icon={Square} color="danger" onClick={() => {}}>
Stop
</Button>
<Button color="light" onClick={() => {}}>
<i className="fa fa-play space-right" aria-hidden="true" />
<Button icon={Play} color="light" onClick={() => {}}>
Start
</Button>
<Button color="primary" onClick={() => {}}>
<i className="fa fa-sync space-right" aria-hidden="true" />
<Button icon={RefreshCw} color="primary" onClick={() => {}}>
Restart
</Button>
</ButtonGroup>

View file

@ -1,5 +1,6 @@
import { PropsWithChildren } from 'react';
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';
import { Icon } from '@@/Icon';
@ -33,7 +34,7 @@ export function CopyButton({
title="Copy Value"
type="button"
>
<Icon icon="copy" feather />
<Icon icon={Copy} />
{children}
</Button>
@ -45,7 +46,7 @@ export function CopyButton({
'vertical-center'
)}
>
<Icon icon="check" feather />
<Icon icon={Check} />
{displayText && <span className="space-left">{displayText}</span>}
</span>
</div>

View file

@ -1,4 +1,5 @@
import { Meta } from '@storybook/react';
import { Download } from 'lucide-react';
import { LoadingButton } from './LoadingButton';
@ -14,8 +15,12 @@ interface Args {
function Template({ loadingText, isLoading }: Args) {
return (
<LoadingButton loadingText={loadingText} isLoading={isLoading}>
<i className="fa fa-download" aria-hidden="true" /> Download
<LoadingButton
loadingText={loadingText}
isLoading={isLoading}
icon={Download}
>
Download
</LoadingButton>
);
}
@ -29,8 +34,8 @@ export const Example = Template.bind({});
export function IsLoading() {
return (
<LoadingButton loadingText="loading" isLoading>
<i className="fa fa-download" aria-hidden="true" /> Download
<LoadingButton loadingText="loading" isLoading icon={Download}>
Download
</LoadingButton>
);
}

View file

@ -6,18 +6,18 @@ test('when isLoading is true should show spinner and loading text', async () =>
const loadingText = 'loading';
const children = 'not visible';
const { findByLabelText, queryByText, findByText } = render(
const { queryByText, findByText, container } = render(
<LoadingButton loadingText={loadingText} isLoading>
{children}
</LoadingButton>
);
const spinner = container.querySelector('svg');
expect(spinner).toBeVisible();
const buttonLabel = queryByText(children);
expect(buttonLabel).toBeNull();
const spinner = await findByLabelText('loading');
expect(spinner).toBeVisible();
const loadingTextElem = await findByText(loadingText);
expect(loadingTextElem).toBeVisible();
});
@ -26,7 +26,7 @@ test('should show children when false', async () => {
const loadingText = 'loading';
const children = 'visible';
const { queryByLabelText, queryByText } = render(
const { queryByText, container } = render(
<LoadingButton loadingText={loadingText} isLoading={false}>
{children}
</LoadingButton>
@ -35,7 +35,7 @@ test('should show children when false', async () => {
const buttonLabel = queryByText(children);
expect(buttonLabel).toBeVisible();
const spinner = queryByLabelText('loading');
const spinner = container.querySelector('svg');
expect(spinner).toBeNull();
const loadingTextElem = queryByText(loadingText);

View file

@ -1,4 +1,7 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, ReactNode } from 'react';
import { Loader2 } from 'lucide-react';
import { Icon } from '@@/Icon';
import { type Props as ButtonProps, Button } from './Button';
@ -13,6 +16,7 @@ export function LoadingButton({
disabled,
type = 'submit',
children,
icon,
...buttonProps
}: PropsWithChildren<Props>) {
return (
@ -21,19 +25,22 @@ export function LoadingButton({
{...buttonProps}
type={type}
disabled={disabled || isLoading}
icon={loadingButtonIcon(isLoading, icon)}
>
{isLoading ? (
<>
<i
className="fa fa-circle-notch fa-spin space-right"
aria-label="loading"
aria-hidden="true"
/>
{loadingText}
</>
) : (
children
)}
{isLoading ? loadingText : children}
</Button>
);
}
function loadingButtonIcon(isLoading: boolean, defaultIcon: ReactNode) {
if (!isLoading) {
return defaultIcon;
}
return (
<Icon
icon={Loader2}
className="animate-spin-slow ml-1"
aria-label="loading"
/>
);
}

View file

@ -1,6 +1,9 @@
import { ReactNode } from 'react';
import clsx from 'clsx';
import { Menu, MenuList, MenuButton } from '@reach/menu-button';
import { MoreVertical } from 'lucide-react';
import { Icon } from '@@/Icon';
import styles from './ActionsMenu.module.css';
@ -19,7 +22,7 @@ export function ActionsMenu({ children }: Props) {
isExpanded && styles.actionsActive
)}
>
<i className="fa fa-ellipsis-v" aria-hidden="true" />
<Icon icon={MoreVertical} />
</MenuButton>
<MenuList>
<div className={styles.tableActionsMenuList}>{children}</div>

View file

@ -1,7 +1,7 @@
import clsx from 'clsx';
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
import { ColumnInstance } from 'react-table';
import { Columns } from 'react-feather';
import { Columns } from 'lucide-react';
import { Checkbox } from '@@/form-components/Checkbox';

View file

@ -1,5 +1,8 @@
import { PropsWithChildren } from 'react';
import { Row } from 'react-table';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Icon } from '@@/Icon';
import styles from './ExpandingCell.module.css';
@ -15,22 +18,15 @@ export function ExpandingCell<
<>
{showExpandArrow && (
<button type="button" className={styles.expandButton}>
<i
<Icon
// eslint-disable-next-line react/jsx-props-no-spreading
{...row.getToggleRowExpandedProps()}
className={`fas ${arrowClass(row.isExpanded)} space-right`}
aria-hidden="true"
icon={row.isExpanded ? ChevronDown : ChevronRight}
className="mr-1"
/>
</button>
)}
{children}
</>
);
function arrowClass(isExpanded: boolean) {
if (isExpanded) {
return 'fa-angle-down';
}
return 'fa-angle-right';
}
}

View file

@ -2,6 +2,9 @@ import clsx from 'clsx';
import { useMemo } from 'react';
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
import { ColumnInstance } from 'react-table';
import { Check, Filter } from 'lucide-react';
import { Icon } from '@@/Icon';
export const DefaultFilter = filterHOC('Filter by state');
@ -25,17 +28,12 @@ export function MultipleSelectionFilter({
<div>
<Menu>
<MenuButton
className={clsx('table-filter', { 'filter-active': enabled })}
className={clsx('table-filter flex items-center', {
'filter-active': enabled,
})}
>
Filter
<i
className={clsx(
'fa',
'space-left',
enabled ? 'fa-check' : 'fa-filter'
)}
aria-hidden="true"
/>
<Icon icon={enabled ? Check : Filter} className="!ml-1" />
</MenuButton>
<MenuPopover className="dropdown-menu">
<div className="tableMenu">

View file

@ -1,4 +1,4 @@
import { Search } from 'react-feather';
import { Search } from 'lucide-react';
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
@ -15,7 +15,7 @@ export function FilterSearchBar({
}: Props) {
return (
<div className="searchBar items-center flex h-[34px]">
<Search className="searchIcon feather" />
<Search className="searchIcon lucide" />
<input
type="text"
className="searchInput"

View file

@ -1,4 +1,4 @@
import { Search } from 'react-feather';
import { Search } from 'lucide-react';
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
import { AutomationTestingProps } from '@/types';
@ -20,7 +20,7 @@ export function SearchBar({
return (
<div className="searchBar items-center flex min-w-[90px]">
<Search className="searchIcon feather shrink-0" />
<Search className="searchIcon lucide shrink-0" />
<input
type="text"
className="searchInput"

View file

@ -76,6 +76,7 @@ function SortWrapper({
<TableHeaderSortIcons
sorted={isSorted}
descending={isSorted && !!isSortedDesc}
className="ml-1"
/>
</div>
</button>

View file

@ -7,14 +7,15 @@ import styles from './TableHeaderSortIcons.module.css';
interface Props {
sorted: boolean;
descending: boolean;
className?: string;
}
export function TableHeaderSortIcons({ sorted, descending }: Props) {
export function TableHeaderSortIcons({ sorted, descending, className }: Props) {
return (
<div className="flex flex-row no-wrap w-min-max">
<div className="flex flex-row no-wrap w-min-max align-middle">
<SortDownIcon
className={clsx(
'space-left',
className,
sorted && !descending && styles.activeSortIcon,
styles.sortIcon
)}

View file

@ -1,7 +1,7 @@
import clsx from 'clsx';
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
import { PropsWithChildren, ReactNode } from 'react';
import { MoreVertical } from 'react-feather';
import { MoreVertical } from 'lucide-react';
interface Props {
quickActions?: ReactNode;

View file

@ -1,7 +1,9 @@
import clsx from 'clsx';
import { useState } from 'react';
import { Check } from 'lucide-react';
import { Checkbox } from '@@/form-components/Checkbox';
import { Icon } from '@@/Icon';
import styles from './TableSettingsMenuAutoRefresh.module.css';
@ -46,12 +48,7 @@ export function TableSettingsMenuAutoRefresh({ onChange, value }: Props) {
)}
onTransitionEnd={() => setIsCheckVisible(false)}
>
<i
id="refreshRateChange"
className="fa fa-check green-icon"
aria-hidden="true"
style={{ marginTop: '7px' }}
/>
<Icon icon={Check} className="!ml-1" mode="success" />
</span>
</div>
)}

View file

@ -4,14 +4,12 @@ import { Icon } from '@@/Icon';
interface Props {
icon?: ReactNode | ComponentType<unknown>;
featherIcon?: boolean;
label: string;
description?: ReactNode;
}
export function TableTitle({
icon,
featherIcon,
label,
children,
description,
@ -22,11 +20,7 @@ export function TableTitle({
<div className="toolBarTitle">
{icon && (
<div className="widget-icon">
<Icon
icon={icon}
feather={featherIcon}
className="space-right feather"
/>
<Icon icon={icon} className="space-right" />
</div>
)}

View file

@ -1,4 +1,4 @@
import { ChevronDown, ChevronUp } from 'react-feather';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { CellProps, Column, HeaderProps } from 'react-table';
import { Button } from '@@/buttons';

View file

@ -1,4 +1,5 @@
import { ChangeEvent, createRef } from 'react';
import { XCircle } from 'lucide-react';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
@ -46,7 +47,7 @@ export function FileUploadField({
</Button>
<span className="vertical-center">
{value ? value.name : <Icon icon="x-circle" feather mode="danger" />}
{value ? value.name : <Icon icon={XCircle} mode="danger" />}
</span>
</div>
);

View file

@ -11,11 +11,11 @@ export type Size = 'xsmall' | 'small' | 'medium' | 'large';
export interface Props {
inputId?: string;
label: string | ReactNode;
label: ReactNode;
size?: Size;
tooltip?: string;
children: ReactNode;
errors?: string | ReactNode;
errors?: ReactNode;
required?: boolean;
}

View file

@ -1,5 +1,6 @@
import { PropsWithChildren } from 'react';
import clsx from 'clsx';
import { AlertTriangle } from 'lucide-react';
import { Icon } from '@@/Icon';
@ -10,7 +11,7 @@ interface Props {
export function FormError({ children, className }: PropsWithChildren<Props>) {
return (
<p className={clsx(`text-muted small vertical-center`, className)}>
<Icon icon="alert-triangle" className="icon-warning" feather />
<Icon icon={AlertTriangle} className="icon-warning" />
<span className="text-warning">{children}</span>
</p>
);

View file

@ -1,4 +1,5 @@
import { PropsWithChildren, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Icon } from '@@/Icon';
@ -27,9 +28,8 @@ export function FormSection({
className="border-0 mx-2 !ml-0 bg-transparent inline-flex justify-center items-center w-2"
>
<Icon
icon={isExpanded ? 'chevron-down' : 'chevron-right'}
icon={isExpanded ? ChevronDown : ChevronRight}
className="shrink-0"
feather
/>
</button>
)}

View file

@ -1,9 +1,9 @@
import { ComponentType } from 'react';
import clsx from 'clsx';
import { FormikErrors } from 'formik';
import { ArrowDown, ArrowUp, Trash2 } from 'lucide-react';
import { AddButton, Button } from '@@/buttons';
import { Icon } from '@@/Icon';
import { Tooltip } from '@@/Tip/Tooltip';
import { TextTip } from '@@/Tip/TextTip';
@ -136,18 +136,16 @@ export function InputList<T = DefaultType>({
disabled={disabled || index === 0}
onClick={() => handleMoveUp(index)}
className="vertical-center btn-only-icon"
>
<Icon icon="arrow-up" feather />
</Button>
icon={ArrowUp}
/>
<Button
size="medium"
type="button"
disabled={disabled || index === value.length - 1}
onClick={() => handleMoveDown(index)}
className="vertical-center btn-only-icon"
>
<Icon icon="arrow-down" feather />
</Button>
icon={ArrowDown}
/>
</>
)}
{!readOnly && (
@ -156,9 +154,8 @@ export function InputList<T = DefaultType>({
size="medium"
onClick={() => handleRemoveItem(key, item)}
className="vertical-center btn-only-icon"
>
<Icon icon="trash-2" feather size="md" />
</Button>
icon={Trash2}
/>
)}
</div>
</div>

View file

@ -32,6 +32,10 @@
padding: 0 8px;
}
.portainer-selector-root .portainer-selector__clear-indicator {
padding: 6px;
}
.portainer-selector-root .portainer-selector__multi-value__label {
@apply text-black;
@apply th-dark:text-white;