1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

implement dynamic icon component & migrate back to react-icons

This commit is contained in:
thatmattlove
2021-12-06 14:33:20 -07:00
parent 7c1a5bf1c3
commit 196b3e0400
23 changed files with 311 additions and 173 deletions

View File

@@ -12,11 +12,8 @@ import {
useDisclosure,
ModalCloseButton,
} from '@chakra-ui/react';
import { HiOutlineDownload as RefreshIcon } from '@meronex/icons/hi';
import { IosColorPalette as ThemeIcon } from '@meronex/icons/ios';
import { MdcCodeJson as ConfigIcon } from '@meronex/icons/mdc';
import { useConfig, useColorValue, useBreakpointValue } from '~/context';
import { CodeBlock } from '~/components';
import { CodeBlock, DynamicIcon } from '~/components';
import { useHyperglassConfig } from '~/hooks';
import type { UseDisclosureReturn } from '@chakra-ui/react';
@@ -75,16 +72,26 @@ export const Debugger: React.FC = () => {
<Tag size={tagSize} colorScheme="gray">
{colorMode.toUpperCase()}
</Tag>
<Button size={btnSize} leftIcon={<ConfigIcon />} colorScheme="cyan" onClick={onConfigOpen}>
<Button
size={btnSize}
colorScheme="cyan"
onClick={onConfigOpen}
leftIcon={<DynamicIcon icon={{ bs: 'BsBraces' }} />}
>
View Config
</Button>
<Button size={btnSize} leftIcon={<ThemeIcon />} colorScheme="blue" onClick={onThemeOpen}>
<Button
size={btnSize}
leftIcon={<DynamicIcon icon={{ io: 'IoIosColorPalette' }} />}
colorScheme="blue"
onClick={onThemeOpen}
>
View Theme
</Button>
<Button
size={btnSize}
colorScheme="purple"
leftIcon={<RefreshIcon />}
leftIcon={<DynamicIcon icon={{ hi: 'HiOutlineDownload' }} />}
onClick={() => refetch()}
>
Reload Config

View File

@@ -40,6 +40,7 @@ export const FooterButton: React.FC<TFooterButton> = (props: TFooterButton) => {
as={Button}
size={size}
variant="ghost"
lineHeight={0}
aria-label={typeof title === 'string' ? title : undefined}
>
{title}

View File

@@ -1,15 +1,11 @@
import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip } from '@chakra-ui/react';
import { If } from '~/components';
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon, If } from '~/components';
import { useColorMode, useColorValue, useBreakpointValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TColorModeToggle } from './types';
const Sun = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiSun));
const Moon = dynamic<MeronexIcon>(() => import('@meronex/icons/hi').then(i => i.HiMoon));
export const ColorModeToggle = forwardRef<HTMLButtonElement, TColorModeToggle>(
(props: TColorModeToggle, ref) => {
const { size = '1.5rem', ...rest } = props;
@@ -34,10 +30,10 @@ export const ColorModeToggle = forwardRef<HTMLButtonElement, TColorModeToggle>(
{...rest}
>
<If c={colorMode === 'light'}>
<Icon as={Moon} boxSize={size} />
<DynamicIcon icon={{ hi: 'HiMoon' }} boxSize={size} />
</If>
<If c={colorMode === 'dark'}>
<Icon as={Sun} boxSize={size} />
<DynamicIcon icon={{ hi: 'HiSun' }} boxSize={size} />
</If>
</Button>
</Tooltip>

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import dynamic from 'next/dynamic';
import { Flex, Icon, HStack, useToken } from '@chakra-ui/react';
import { If } from '~/components';
import { Flex, HStack, useToken } from '@chakra-ui/react';
import { DynamicIcon, If } from '~/components';
import { useConfig, useMobile, useColorValue, useBreakpointValue } from '~/context';
import { useStrf } from '~/hooks';
import { FooterButton } from './button';
@@ -12,9 +11,6 @@ import { isLink, isMenu } from './types';
import type { ButtonProps, LinkProps } from '@chakra-ui/react';
import type { Link, Menu } from '~/types';
const CodeIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCode));
const ExtIcon = dynamic<MeronexIcon>(() => import('@meronex/icons/go').then(i => i.GoLinkExternal));
function buildItems(links: Link[], menus: Menu[]): [(Link | Menu)[], (Link | Menu)[]] {
const leftLinks = links.filter(link => link.side === 'left');
const leftMenus = menus.filter(menu => menu.side === 'left');
@@ -61,7 +57,7 @@ export const Footer: React.FC = () => {
const icon: Partial<ButtonProps & LinkProps> = {};
if (item.showIcon) {
icon.rightIcon = <ExtIcon />;
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
}
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) {
@@ -77,7 +73,7 @@ export const Footer: React.FC = () => {
const icon: Partial<ButtonProps & LinkProps> = {};
if (item.showIcon) {
icon.rightIcon = <ExtIcon />;
icon.rightIcon = <DynamicIcon icon={{ go: 'GoLinkExternal' }} />;
}
return <FooterLink key={item.title} href={url} title={item.title} {...icon} />;
} else if (isMenu(item)) {
@@ -91,7 +87,7 @@ export const Footer: React.FC = () => {
key="credit"
side="right"
content={content.credit}
title={<Icon as={CodeIcon} boxSize={size} />}
title={<DynamicIcon icon={{ fi: 'FiCode' }} boxSize={size} />}
/>
</If>
<ColorModeToggle size={size} />

View File

@@ -1,20 +1,12 @@
import { useMemo } from 'react';
import dynamic from 'next/dynamic';
import { Button, chakra, Stack, Text, VStack } from '@chakra-ui/react';
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useStrf, useDNSQuery, useFormState } from '~/hooks';
import type { DnsOverHttps } from '~/types';
import type { ResolvedTargetProps } from './types';
const RightArrow = chakra(
dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleRight)),
);
const LeftArrow = chakra(
dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleLeft)),
);
function findAnswer(data: DnsOverHttps.Response | undefined): string {
let answer = '';
if (typeof data !== 'undefined') {
@@ -68,16 +60,12 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
}
const hasAnswer = useMemo(
() => (!isError4 || !isError6) && (answer4 !== '' || answer6 !== ''),
() => (!isError4 || !isError6) && (answer4 || answer6),
[answer4, answer6, isError4, isError6],
);
const showA = useMemo(
() => !isLoading4 && !isError4 && answer4 !== '',
[isLoading4, isError4, answer4],
);
const showA = useMemo(() => !isLoading4 && !isError4 && answer4, [isLoading4, isError4, answer4]);
const showAAAA = useMemo(
() => !isLoading6 && !isError6 && answer6 !== '',
() => !isLoading6 && !isError6 && answer6,
[isLoading6, isError6, answer6],
);
@@ -102,7 +90,7 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
colorScheme="primary"
justifyContent="space-between"
onClick={() => selectTarget(answer4)}
rightIcon={<RightArrow boxSize="18px" />}
rightIcon={<DynamicIcon icon={{ fa: 'FaArrowCircleRight' }} boxSize="18px" />}
>
{answer4}
</Button>
@@ -116,7 +104,7 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
colorScheme="secondary"
justifyContent="space-between"
onClick={() => selectTarget(answer6)}
rightIcon={<RightArrow boxSize="18px" />}
rightIcon={<DynamicIcon icon={{ fa: 'FaArrowCircleRight' }} boxSize="18px" />}
>
{answer6}
</Button>
@@ -135,7 +123,7 @@ export const ResolvedTarget = (props: ResolvedTargetProps): JSX.Element => {
variant="outline"
size="sm"
onClick={errorClose}
leftIcon={<LeftArrow />}
leftIcon={<DynamicIcon icon={{ fa: 'FaArrowCircleLeft' }} />}
>
{web.text.fqdnErrorButton}
</Button>

View File

@@ -1,16 +1,11 @@
import { useMemo } from 'react';
import dynamic from 'next/dynamic';
import { Button, chakra, Stack, Text, VStack, useDisclosure } from '@chakra-ui/react';
import { Prompt } from '~/components';
import { Button, Stack, Text, VStack, useDisclosure } from '@chakra-ui/react';
import { DynamicIcon, Prompt } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useStrf, useWtf } from '~/hooks';
import type { UserIPProps } from './types';
const RightArrow = chakra(
dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleRight)),
);
export const UserIP = (props: UserIPProps): JSX.Element => {
const { setTarget } = props;
const { onOpen, ...disclosure } = useDisclosure();
@@ -67,7 +62,7 @@ export const UserIP = (props: UserIPProps): JSX.Element => {
ipv4?.data?.ip && setTarget(ipv4.data.ip);
disclosure.onClose();
}}
rightIcon={<RightArrow boxSize="18px" />}
rightIcon={<DynamicIcon icon={{ fa: 'FaArrowCircleRight' }} boxSize="18px" />}
>
{ipv4?.data?.ip ?? noIPv4}
</Button>
@@ -85,7 +80,7 @@ export const UserIP = (props: UserIPProps): JSX.Element => {
ipv6?.data?.ip && setTarget(ipv6.data.ip);
disclosure.onClose();
}}
rightIcon={<RightArrow boxSize="18px" />}
rightIcon={<DynamicIcon icon={{ fa: 'FaArrowCircleRight' }} boxSize="18px" />}
>
{ipv6?.data?.ip ?? noIPv6}
</Button>

View File

@@ -1,4 +1,3 @@
import dynamic from 'next/dynamic';
import {
Modal,
ScaleFade,
@@ -10,14 +9,12 @@ import {
useDisclosure,
ModalCloseButton,
} from '@chakra-ui/react';
import { Markdown } from '~/components';
import { DynamicIcon, Markdown } from '~/components';
import { useColorValue } from '~/context';
import { isQueryContent } from '~/types';
import type { THelpModal } from './types';
const Info = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiInfo));
export const HelpModal: React.FC<THelpModal> = (props: THelpModal) => {
const { visible, item, name, ...rest } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -36,7 +33,7 @@ export const HelpModal: React.FC<THelpModal> = (props: THelpModal) => {
minW={3}
size="md"
variant="link"
icon={<Info />}
icon={<DynamicIcon icon={{ fi: 'FiInfo' }} />}
onClick={onOpen}
colorScheme="blue"
aria-label={`${name}_help`}

View File

@@ -1,14 +1,11 @@
import dynamic from 'next/dynamic';
import { Flex, Icon, IconButton } from '@chakra-ui/react';
import { Flex, IconButton } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import { AnimatedDiv } from '~/components';
import { AnimatedDiv, DynamicIcon } from '~/components';
import { useColorValue } from '~/context';
import { useOpposingColor, useFormState } from '~/hooks';
import type { TResetButton } from './types';
const LeftArrow = dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaAngleLeft));
export const ResetButton = (props: TResetButton): JSX.Element => {
const { developerMode, resetForm, ...rest } = props;
const status = useFormState(s => s.status);
@@ -34,11 +31,12 @@ export const ResetButton = (props: TResetButton): JSX.Element => {
>
<Flex boxSize="100%" justifyContent="center" alignItems="center" {...rest}>
<IconButton
variant="unstyled"
lineHeight={0}
color="current"
variant="unstyled"
aria-label="Reset"
onClick={resetForm}
icon={<Icon as={LeftArrow} boxSize={8} />}
icon={<DynamicIcon icon={{ fa: 'FaAngleLeft' }} boxSize={8} />}
/>
</Flex>
</AnimatedDiv>

View File

@@ -1,15 +1,9 @@
import { forwardRef } from 'react';
import { Icon, Text, Box, Tooltip, Menu, MenuButton, MenuList, Link } from '@chakra-ui/react';
import { CgMoreO as More } from '@meronex/icons/cg';
import { BisError as Warning } from '@meronex/icons/bi';
import { MdCancel as NotAllowed } from '@meronex/icons/md';
import { RiHome2Fill as End } from '@meronex/icons/ri';
import { BsQuestionCircleFill as Question } from '@meronex/icons/bs';
import { FaCheckCircle as Check, FaChevronRight as ChevronRight } from '@meronex/icons/fa';
import { Text, Box, Tooltip, Menu, MenuButton, MenuList, Link } from '@chakra-ui/react';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { If } from '~/components';
import { If, DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
@@ -41,10 +35,10 @@ export const Active: React.FC<TActive> = (props: TActive) => {
return (
<>
<If c={isActive}>
<Icon color={color[+isActive]} as={Check} />
<DynamicIcon color={color[+isActive]} icon={{ fa: 'FaCheckCircle' }} />
</If>
<If c={!isActive}>
<Icon color={color[+isActive]} as={NotAllowed} />
<DynamicIcon color={color[+isActive]} icon={{ md: 'MdCancel' }} />
</If>
</>
);
@@ -86,7 +80,7 @@ export const ASPath: React.FC<TASPath> = (props: TASPath) => {
);
if (path.length === 0) {
return <Icon as={End} />;
return <DynamicIcon icon={{ ri: 'RiHome2Fill' }} />;
}
const paths = [] as JSX.Element[];
@@ -95,7 +89,13 @@ export const ASPath: React.FC<TASPath> = (props: TASPath) => {
const asnStr = String(asn);
i !== 0 &&
paths.push(
<Icon as={ChevronRight} key={`separator-${i}`} color={color[+active]} boxSize={5} px={2} />,
<DynamicIcon
icon={{ fa: 'FaChevronRight' }}
key={`separator-${i}`}
color={color[+active]}
boxSize={5}
px={2}
/>,
);
paths.push(
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
@@ -117,14 +117,14 @@ export const Communities: React.FC<TCommunities> = (props: TCommunities) => {
<If c={communities.length === 0}>
<Tooltip placement="right" hasArrow label={web.text.noCommunities}>
<Link>
<Icon as={Question} />
<DynamicIcon icon={{ bs: 'BsQuestionCircleFill' }} />
</Link>
</Tooltip>
</If>
<If c={communities.length !== 0}>
<Menu preventOverflow>
<MenuButton>
<Icon as={More} />
<DynamicIcon icon={{ cg: 'CgMoreO' }} />
</MenuButton>
<MenuList
p={3}
@@ -163,7 +163,13 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
],
);
const color = useOpposingColor(bg[+active][state]);
const icon = [NotAllowed, Check, Warning, Question];
const icon = [
{ md: 'MdCancel' },
{ fa: 'FaCheckCircle' },
{ bi: 'BisError' },
{ bs: 'BsQuestionCircleFill' },
] as Record<string, string>[];
const text = [
web.text.rpkiInvalid,
@@ -181,7 +187,7 @@ const _RPKIState: React.ForwardRefRenderFunction<HTMLDivElement, TRPKIState> = (
color={color}
>
<Box ref={ref} boxSize={5}>
<Box as={icon[state]} color={bg[+active][state]} />
<DynamicIcon icon={icon[state]} color={bg[+active][state]} />
</Box>
</Tooltip>
);

View File

@@ -1,18 +1,14 @@
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip } from '@chakra-ui/react';
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TPathButton } from './types';
const PathIcon = dynamic<MeronexIcon>(() =>
import('@meronex/icons/bi').then(i => i.BisNetworkChart),
);
export const PathButton: React.FC<TPathButton> = (props: TPathButton) => {
export const PathButton = (props: TPathButton): JSX.Element => {
const { onOpen } = props;
return (
<Tooltip hasArrow label="View AS Path" placement="top">
<Button as="a" mx={1} size="sm" variant="ghost" onClick={onOpen} colorScheme="secondary">
<Icon as={PathIcon} boxSize="16px" />
<DynamicIcon icon={{ bi: 'BiNetworkChart' }} boxSize="16px" />
</Button>
</Tooltip>
);

View File

@@ -1,10 +1,6 @@
import dynamic from 'next/dynamic';
import { ButtonGroup, IconButton } from '@chakra-ui/react';
import { useZoomPanHelper } from 'react-flow-renderer';
const Plus = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiPlus));
const Minus = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiMinus));
const Square = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiSquare));
import { DynamicIcon } from '~/components';
export const Controls: React.FC = () => {
const { fitView, zoomIn, zoomOut } = useZoomPanHelper();
@@ -20,9 +16,21 @@ export const Controls: React.FC = () => {
variant="solid"
colorScheme="secondary"
>
<IconButton icon={<Plus />} onClick={() => zoomIn()} aria-label="Zoom In" />
<IconButton icon={<Minus />} onClick={() => zoomOut()} aria-label="Zoom Out" />
<IconButton icon={<Square />} onClick={() => fitView()} aria-label="Fit Nodes" />
<IconButton
icon={<DynamicIcon icon={{ fi: 'FiPlus' }} />}
onClick={() => zoomIn()}
aria-label="Zoom In"
/>
<IconButton
icon={<DynamicIcon icon={{ fi: 'FiMinus' }} />}
onClick={() => zoomOut()}
aria-label="Zoom Out"
/>
<IconButton
icon={<DynamicIcon icon={{ fi: 'FiSquare' }} />}
onClick={() => fitView()}
aria-label="Fit Nodes"
/>
</ButtonGroup>
);
};

View File

@@ -1,12 +1,9 @@
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/react';
const Copy = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCopy));
const Check = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiCheck));
import { Button, Tooltip, useClipboard } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TCopyButton } from './types';
export const CopyButton: React.FC<TCopyButton> = (props: TCopyButton) => {
export const CopyButton = (props: TCopyButton): JSX.Element => {
const { copyValue, ...rest } = props;
const { onCopy, hasCopied } = useClipboard(copyValue);
return (
@@ -20,7 +17,7 @@ export const CopyButton: React.FC<TCopyButton> = (props: TCopyButton) => {
colorScheme="secondary"
{...rest}
>
<Icon as={hasCopied ? Check : Copy} boxSize="16px" />
<DynamicIcon icon={{ fi: hasCopied ? 'FiCheck' : 'FiCopy' }} boxSize="16px" />
</Button>
</Tooltip>
);

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { AccordionIcon, Box, Spinner, HStack, Text, Tooltip } from '@chakra-ui/react';
import { BisError as Warning } from '@meronex/icons/bi';
import { FaCheckCircle as Check } from '@meronex/icons/fa';
import { DynamicIcon } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useOpposingColor, useStrf } from '~/hooks';
@@ -43,8 +42,8 @@ export const ResultHeader: React.FC<TResultHeader> = (props: TResultHeader) => {
{loading ? (
<Spinner size="sm" mr={4} color={status} />
) : (
<Box
as={isError ? Warning : Check}
<DynamicIcon
icon={isError ? { bi: 'BisError' } : { fa: 'FaCheckCircle' }}
color={isError ? warning : defaultStatus}
mr={4}
boxSize="100%"

View File

@@ -2,7 +2,6 @@ import { forwardRef, memo, useEffect, useMemo } from 'react';
import {
Box,
Flex,
Icon,
Alert,
chakra,
HStack,
@@ -14,10 +13,9 @@ import {
useAccordionContext,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { BsLightningFill } from '@meronex/icons/bs';
import { startCase } from 'lodash';
import startCase from 'lodash/startCase';
import isEqual from 'react-fast-compare';
import { BGPTable, Countdown, TextOutput, If, Path } from '~/components';
import { BGPTable, Countdown, DynamicIcon, If, Path, TextOutput } from '~/components';
import { useColorValue, useConfig, useMobile } from '~/context';
import { useStrf, useLGQuery, useTableToString, useFormState, useDevice } from '~/hooks';
import { isStructuredOutput, isStringOutput } from '~/types';
@@ -262,7 +260,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, ResultProps> = (
</If>
<Tooltip hasArrow label={cacheLabel} placement="top">
<Box>
<Icon as={BsLightningFill} color={color} />
<DynamicIcon icon={{ bs: 'BsLightningFill' }} color={color} />
</Box>
</Tooltip>
<If c={isMobile}>

View File

@@ -1,11 +1,9 @@
import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip } from '@chakra-ui/react';
import { Button, Tooltip } from '@chakra-ui/react';
import { DynamicIcon } from '~/components';
import type { TRequeryButton } from './types';
const Repeat = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiRepeat));
const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, TRequeryButton> = (
props: TRequeryButton,
ref,
@@ -25,7 +23,7 @@ const _RequeryButton: React.ForwardRefRenderFunction<HTMLButtonElement, TRequery
colorScheme="secondary"
{...rest}
>
<Icon as={Repeat} boxSize="16px" />
<DynamicIcon icon={{ fi: 'FiRepeat' }} boxSize="16px" />
</Button>
</Tooltip>
);

View File

@@ -13,9 +13,8 @@ import {
ModalCloseButton,
PopoverCloseButton,
} from '@chakra-ui/react';
import { FiSearch } from '@meronex/icons/fi';
import { useFormContext } from 'react-hook-form';
import { If, ResolvedTarget } from '~/components';
import { DynamicIcon, If, ResolvedTarget } from '~/components';
import { useMobile, useColorValue } from '~/context';
import { useFormState } from '~/hooks';
@@ -33,7 +32,7 @@ const _SubmitIcon: React.ForwardRefRenderFunction<
size="lg"
width={16}
type="submit"
icon={<FiSearch />}
icon={<DynamicIcon icon={{ fi: 'FiSearch' }} />}
title="Submit Query"
colorScheme="primary"
isLoading={isLoading}

View File

@@ -1,11 +1,9 @@
// This rule isn't needed because react-table does this for us, for better or worse.
/* eslint react/jsx-key: 0 */
import dynamic from 'next/dynamic';
import { Flex, Icon, Text } from '@chakra-ui/react';
import { Flex, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table';
import { useMobile } from '~/context';
import { CardBody, CardFooter, CardHeader, If } from '~/components';
import { CardBody, CardFooter, CardHeader, DynamicIcon, If } from '~/components';
import { TableMain } from './table';
import { TableCell } from './cell';
import { TableHead } from './head';
@@ -18,25 +16,6 @@ import type { TableOptions, PluginHook } from 'react-table';
import type { TCellRender } from '~/types';
import type { TTable } from './types';
const ChevronRight = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fa').then(i => i.FaChevronRight),
);
const ChevronLeft = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fa').then(i => i.FaChevronLeft),
);
const ChevronDown = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fa').then(i => i.FaChevronDown),
);
const DoubleChevronRight = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fi').then(i => i.FiChevronsRight),
);
const DoubleChevronLeft = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fi').then(i => i.FiChevronsLeft),
);
export const Table: React.FC<TTable> = (props: TTable) => {
const {
data,
@@ -112,10 +91,10 @@ export const Table: React.FC<TTable> = (props: TTable) => {
</Text>
<If c={column.isSorted}>
<If c={typeof column.isSortedDesc !== 'undefined'}>
<Icon as={ChevronDown} boxSize={4} ml={1} />
<DynamicIcon icon={{ fa: 'FaChevronDown' }} boxSize={4} ml={1} />
</If>
<If c={!column.isSortedDesc}>
<Icon as={ChevronRight} boxSize={4} ml={1} />
<DynamicIcon icon={{ fa: 'FaChevronRight' }} boxSize={4} ml={1} />
</If>
</If>
<If c={!column.isSorted}>{''}</If>
@@ -163,13 +142,13 @@ export const Table: React.FC<TTable> = (props: TTable) => {
mr={2}
onClick={() => gotoPage(0)}
isDisabled={!canPreviousPage}
icon={<Icon as={DoubleChevronLeft} boxSize={4} />}
icon={<DynamicIcon icon={{ fi: 'FiChevronsLeft' }} boxSize={4} />}
/>
<TableIconButton
mr={2}
onClick={() => previousPage()}
isDisabled={!canPreviousPage}
icon={<Icon as={ChevronLeft} boxSize={3} />}
icon={<DynamicIcon icon={{ fa: 'FaChevronLeft' }} boxSize={3} />}
/>
</Flex>
<Flex justifyContent="center" alignItems="center">
@@ -193,12 +172,12 @@ export const Table: React.FC<TTable> = (props: TTable) => {
ml={2}
onClick={nextPage}
isDisabled={!canNextPage}
icon={<Icon as={ChevronRight} boxSize={3} />}
icon={<DynamicIcon icon={{ fa: 'FaChevronRight' }} boxSize={3} />}
/>
<TableIconButton
ml={2}
isDisabled={!canNextPage}
icon={<Icon as={DoubleChevronRight} boxSize={4} />}
icon={<DynamicIcon icon={{ fi: 'FiChevronsRight' }} boxSize={4} />}
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
/>
</Flex>

View File

@@ -0,0 +1,181 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react-hooks/rules-of-hooks */
import { memo, useMemo } from 'react';
import dynamic from 'next/dynamic';
import { chakra, Icon as ChakraIcon } from '@chakra-ui/react';
import isEqual from 'react-fast-compare';
import type { IconProps as ChakraIconProps, TooltipProps } from '@chakra-ui/react';
interface IconMap {
[library: string]: string;
}
interface DynamicIconProps extends Omit<ChakraIconProps, 'icon'> {
icon: IconMap;
}
interface ErrorIconProps {
message: string;
}
interface IconErrorConstructor {
original: IconMap;
library: string;
iconName: string;
}
/**
* Extend builtin `Error` for easier handling of icon rendering errors.
*/
class IconError extends Error {
/**
* Original family → icon mapping object.
*/
original: IconMap;
/**
* Determined family/icon library.
*/
library: string;
/**
* Determined icon name.
*/
iconName: string;
constructor({ original, library, iconName }: IconErrorConstructor) {
super();
this.original = original;
this.library = library;
this.iconName = iconName;
this.stack = this.stack + `\nOriginal object: '${JSON.stringify(this.original)}'`;
}
get message(): string {
return `No icon matches 'react-icons/${this.library}/${this.iconName}'`;
}
}
/**
* Derive `react-icons` icon family → icon name mapping with proper capitalization. Also handles
* existence (or not) of the family prefix.
* @param iconObj Family to icon name mapping.
*
* @example
* ```js
* iconPath({ fa: 'FaPlus' });
* iconPath({ fa: 'faplus' });
* iconPath({ fa: 'plus' });
* // all return → ['fa', 'FaPlus']
* ```
* @returns
*/
function iconPath(iconObj: IconMap): [string, string] {
// Capitalize the first character of a string.
const capitalizeFirst = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
// Get the first object key.
const [familyKey] = Object.keys(iconObj);
// Capitalize the family name.
const family = capitalizeFirst(familyKey!);
// Get the icon name.
const initialName = iconObj[familyKey!];
// Capitalize the icon name. If `faplus` is provided, it will now be `Faplus`.
let name = capitalizeFirst(initialName!);
// Create a regex pattern to determine if the family name is in the icon name. If `name` is
// `Faplus`, this will be true.
const familyPattern = new RegExp(`^${family}`, 'g');
if (name.match(familyPattern)) {
// If the icon name contains the family name, remove it and capitalize the result. If `name`
// was `Faplus`, it is now `Plus`.
name = capitalizeFirst(name.replace(familyPattern, ''));
}
// Return a tuple of [family, icon name], i.e. [fa, FaPlus].
return [family.toLowerCase(), `${family}${name}`];
}
/**
* Generic error icon to indicate that there was a problem dynamically importing or otherwise
* rendering the dynamic icon. Wraps generic icon in a tooltip that provides more detail. This
* is dynamically imported at render time in an effort to reduce load times.
*
* @param props Error message to be displayed.
*/
const ErrorIcon = (props: ErrorIconProps): JSX.Element => {
const Tooltip = dynamic<TooltipProps>(() => import('@chakra-ui/react').then(m => m.Tooltip));
return (
<Tooltip hasArrow bg="red.500" label={props.message}>
<chakra.span boxSize={8} color="red.500" p={1} textAlign="center">
&#9888;
</chakra.span>
</Tooltip>
);
};
const _DynamicIcon = (props: DynamicIconProps): JSX.Element => {
const { icon: iconObj, ...rest } = props;
// Create a string representation of the icon family and name mapping for memoization.
const key = Object.entries(iconObj).flat().join('--');
try {
const [library, iconName] = useMemo(() => {
return iconPath(iconObj);
}, [key]);
if (!library || !iconName) {
// If either the library or icon name are falsy, error out.
throw new IconError({ original: iconObj, iconName, library });
}
// Create a memoized version of the imported component, to update only when the computed
// family/icon names are changed. Attempt to dynamically import icon from formatted
// library/icon name.
const Component = useMemo(
() =>
dynamic(() =>
import(`react-icons/${library}/index.js`)
.then(i => {
if (!(iconName in i)) {
// If the icon name doesn't exist in the module, error out.
throw new IconError({ original: iconObj, iconName, library });
}
// Otherwise, return the imported icon.
return i[iconName as keyof typeof i];
})
.catch(error => {
// Handle any error that occurs during dynamic import.
console.error(error);
const CaughtError = (): JSX.Element => <ErrorIcon message={String(error)} />;
return CaughtError;
}),
),
[library, iconName],
);
// Return a Chakra-UI icon instance with the imported icon.
return <ChakraIcon as={Component} {...rest} />;
} catch (error) {
// Handle any other uncaught errors.
console.error(error);
return <ErrorIcon message={String(error)} />;
}
};
/**
* Dynamically import a `react-icons` icon by name and wrap it in a Chakra-UI icon component.
*
* @param props Icon family to icon name mapping.
*
* @throws An error icon is produced if there is any error during the dynamic import process. A
* console message is also displayed with additional details.
*
* @example
* ```js
* <>
* <Icon icon={{ fa: 'FaPlus' }} />
* // This also works:
* <Icon icon={{ fa: 'plus' }} />
* </>
* ```
*/
export const DynamicIcon = memo(_DynamicIcon, isEqual);
export default DynamicIcon;

View File

@@ -1,2 +1,3 @@
export * from './animated';
export * from './dynamic-icon';
export * from './if';

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import create from 'zustand';
import { intersectionWith } from 'lodash';
import intersectionWith from 'lodash/intersectionWith';
import plur from 'plur';
import isEqual from 'react-fast-compare';
import { all, andJoin, dedupObjectArray, withDev } from '~/util';

View File

@@ -21,7 +21,6 @@
"@emotion/styled": "^11.6.0",
"@hookform/devtools": "^4.0.1",
"@hookform/resolvers": "^2.8.4",
"@meronex/icons": "^4.0.0",
"dagre": "^0.8.5",
"dayjs": "^1.10.4",
"framer-motion": "^5.4.1",
@@ -38,6 +37,7 @@
"react-flow-renderer": "^9.6.0",
"react-ga": "^3.3.0",
"react-hook-form": "^7.21.0",
"react-icons": "^4.3.1",
"react-markdown": "^5.0.3",
"react-query": "^3.16.0",
"react-select": "^5.2.1",
@@ -45,13 +45,14 @@
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",
"vest": "^3.2.8",
"zustand": "^3.5.10"
"zustand": "^3.6.6"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"@types/dagre": "^0.7.44",
"@types/express": "^4.17.13",
"@types/lodash": "^4.14.177",
"@types/node": "^14.14.41",
"@types/react": "^17.0.3",
"@types/react-table": "^7.7.1",

View File

@@ -1,6 +1,6 @@
import { devtools } from 'zustand/middleware';
import type { StateCreator } from 'zustand';
import type { StateCreator, SetState, GetState, StoreApi } from 'zustand';
/**
* Wrap a zustand state function with devtools, if applicable.
@@ -14,7 +14,7 @@ export function withDev<T extends object = {}>(
name: string,
): StateCreator<T> {
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
return devtools<T>(store, { name });
return devtools<T, SetState<T>, GetState<T>, StoreApi<T>>(store, { name });
}
return store;
}

View File

@@ -1514,14 +1514,6 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
"@meronex/icons@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@meronex/icons/-/icons-4.0.0.tgz#26e089a8a4ec176a5b6778fd54fcdd25b4746c67"
integrity sha512-WnoxUT02qawZSvsoPSwe7YOqOk0APysIHugiD3dYdc/QNeoigN4PD8mmmtmZFKlv8/Z7eERub0BmPkWcJ1BI+w==
dependencies:
camelcase "^5.0.0"
ncp "^2.0.0"
"@napi-rs/triples@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
@@ -2094,6 +2086,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/lodash@^4.14.177":
version "4.14.177"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578"
integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==
"@types/mdast@^3.0.0", "@types/mdast@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
@@ -2944,7 +2941,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camelcase@^5.0.0, camelcase@^5.3.1:
camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -6175,11 +6172,6 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
ncp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -6957,6 +6949,11 @@ react-hook-form@^7.21.0:
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.21.0.tgz#f6f0311295b83ca768873ca9f2a787ff1525f591"
integrity sha512-aekCf+dedYFIg+7nCK2acMvZ+s6Ohw2I7UNQ+zNIadBl1SoXow2Tl6c3F49xF8GFCdn5jeK43JHH26rmtdRyLQ==
react-icons@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca"
integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==
react-is@17.0.2, "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
@@ -8433,10 +8430,10 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zustand@^3.5.10:
version "3.5.10"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.10.tgz#d2622efd64530ffda285ee5b13ff645b68ab0faf"
integrity sha512-upluvSRWrlCiExu2UbkuMIPJ9AigyjRFoO7O9eUossIj7rPPq7pcJ0NKk6t2P7KF80tg/UdPX6/pNKOSbs9DEg==
zustand@^3.6.6:
version "3.6.6"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.6.6.tgz#3b7473a15813f7af9784233abd052c3b4560bbcc"
integrity sha512-y4755cIzJHQFEHgTQ5cHrlHdmXMxm5N3DU05Q27yT6rK4lKs2336t5IsAz5q9/GRaoEz6o8SiCOPDhZd5BnneA==
zwitch@^1.0.0:
version "1.0.5"