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

continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads
2020-11-29 01:25:48 -07:00
parent e0eb51012d
commit e823c3416b
105 changed files with 2900 additions and 2308 deletions

View File

@@ -1,199 +0,0 @@
import * as React from 'react';
import {
Flex,
Icon,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
Text,
Tooltip,
useColorMode,
} from '@chakra-ui/core';
import { MdLastPage } from '@meronex/icons/md';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { useConfig } from 'app/context';
import { Table } from 'app/components';
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
const isActiveColor = {
true: { dark: 'green.300', light: 'green.500' },
false: { dark: 'gray.300', light: 'gray.500' },
};
const arrowColor = {
true: { dark: 'blackAlpha.500', light: 'blackAlpha.500' },
false: { dark: 'whiteAlpha.300', light: 'blackAlpha.500' },
};
const rpkiIcon = ['not-allowed', 'check-circle', 'warning', 'question'];
const rpkiColor = {
true: {
dark: ['red.500', 'green.600', 'yellow.500', 'gray.800'],
light: ['red.500', 'green.500', 'yellow.500', 'gray.600'],
},
false: {
dark: ['red.300', 'green.300', 'yellow.300', 'gray.300'],
light: ['red.400', 'green.500', 'yellow.400', 'gray.500'],
},
};
const makeColumns = fields => {
return fields.map(pair => {
const [header, accessor, align] = pair;
let columnConfig = {
Header: header,
accessor: accessor,
align: align,
hidden: false,
};
if (align === null) {
columnConfig.hidden = true;
}
return columnConfig;
});
};
const MonoField = ({ v, ...props }) => (
<Text fontSize="sm" fontFamily="mono" {...props}>
{v}
</Text>
);
const Active = ({ isActive }) => {
const { colorMode } = useColorMode();
return (
<Icon name={isActive ? 'check-circle' : 'warning'} color={isActiveColor[isActive][colorMode]} />
);
};
const Age = ({ inSeconds }) => {
const now = dayjs.utc();
const then = now.subtract(inSeconds, 'seconds');
return (
<Tooltip hasArrow label={then.toString().replace('GMT', 'UTC')} placement="right">
<Text fontSize="sm">{now.to(then, true)}</Text>
</Tooltip>
);
};
const Weight = ({ weight, winningWeight }) => {
const fixMeText =
winningWeight === 'low' ? 'Lower Weight is Preferred' : 'Higher Weight is Preferred';
return (
<Tooltip hasArrow label={fixMeText} placement="right">
<Text fontSize="sm" fontFamily="mono">
{weight}
</Text>
</Tooltip>
);
};
const ASPath = ({ path, active }) => {
const { colorMode } = useColorMode();
if (path.length === 0) {
return <Icon as={MdLastPage} />;
}
let paths = [];
path.map((asn, i) => {
const asnStr = String(asn);
i !== 0 &&
paths.push(
<Icon name="chevron-right" key={`separator-${i}`} color={arrowColor[active][colorMode]} />,
);
paths.push(
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
{asnStr}
</Text>,
);
});
return paths;
};
const Communities = ({ communities }) => {
const { colorMode } = useColorMode();
let component;
communities.length === 0
? (component = (
<Tooltip placement="right" hasArrow label="No Communities">
<Icon name="question-outline" />
</Tooltip>
))
: (component = (
<Popover trigger="hover" placement="right">
<PopoverTrigger>
<Icon name="view" />
</PopoverTrigger>
<PopoverContent
textAlign="left"
p={4}
width="unset"
color={colorMode === 'dark' ? 'white' : 'black'}
fontFamily="mono"
fontWeight="normal"
whiteSpace="pre-wrap">
<PopoverArrow />
{communities.join('\n')}
</PopoverContent>
</Popover>
));
return component;
};
const RPKIState = ({ state, active }) => {
const { web } = useConfig();
const { colorMode } = useColorMode();
const stateText = [
web.text.rpki_invalid,
web.text.rpki_valid,
web.text.rpki_unknown,
web.text.rpki_unverified,
];
return (
<Tooltip hasArrow placement="right" label={stateText[state] ?? stateText[3]}>
<Icon name={rpkiIcon[state]} color={rpkiColor[active][colorMode][state]} />
</Tooltip>
);
};
const Cell = ({ data, rawData, longestASN }) => {
const component = {
prefix: <MonoField v={data.value} />,
active: <Active isActive={data.value} />,
age: <Age inSeconds={data.value} />,
weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />,
med: <MonoField v={data.value} />,
local_preference: <MonoField v={data.value} />,
as_path: <ASPath path={data.value} active={data.row.values.active} longestASN={longestASN} />,
communities: <Communities communities={data.value} />,
next_hop: <MonoField v={data.value} />,
source_as: <MonoField v={data.value} />,
source_rid: <MonoField v={data.value} />,
peer_rid: <MonoField v={data.value} />,
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />,
};
return component[data.column.id] ?? <> </>;
};
export const BGPTable = ({ children: data, ...props }) => {
const config = useConfig();
const columns = makeColumns(config.parsed_data_fields);
return (
<Flex my={8} maxW={['100%', '100%', '100%', '100%']} w="100%" {...props}>
<Table
columns={columns}
data={data.routes}
rowHighlightProp="active"
cellRender={d => <Cell data={d} rawData={data} />}
bordersHorizontal
rowHighlightBg="green"
/>
</Flex>
);
};

View File

@@ -1,12 +0,0 @@
namespace ReactCountdown {
type CountdownRender = import('react-countdown').CountdownRenderProps;
}
interface IRenderer extends ReactCountdown.CountdownRender {
text: string;
}
interface ICountdown {
timeout: number;
text: string;
}

View File

@@ -1 +0,0 @@
export * from './Countdown';

View File

@@ -1,11 +0,0 @@
namespace Chakra {
type FlexProps = import('@chakra-ui/core').FlexProps;
}
interface ICardBody extends Omit<Chakra.FlexProps, 'onClick'> {
onClick?: () => boolean;
}
interface ICardFooter extends Chakra.FlexProps {}
interface ICardHeader extends Chakra.FlexProps {}

View File

@@ -1,6 +1,8 @@
import { Flex } from '@chakra-ui/core'; import { Flex } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { ICardBody } from './types';
export const CardBody = (props: ICardBody) => { export const CardBody = (props: ICardBody) => {
const { onClick, ...rest } = props; const { onClick, ...rest } = props;
const bg = useColorValue('white', 'original.dark'); const bg = useColorValue('white', 'original.dark');

View File

@@ -1,4 +1,6 @@
import { Flex } from '@chakra-ui/core'; import { Flex } from '@chakra-ui/react';
import type { ICardFooter } from './types';
export const CardFooter = (props: ICardFooter) => ( export const CardFooter = (props: ICardFooter) => (
<Flex <Flex

View File

@@ -1,6 +1,8 @@
import { Flex, Text } from '@chakra-ui/core'; import { Flex, Text } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { ICardHeader } from './types';
export const CardHeader = (props: ICardHeader) => { export const CardHeader = (props: ICardHeader) => {
const { children, ...rest } = props; const { children, ...rest } = props;
const bg = useColorValue('blackAlpha.50', 'whiteAlpha.100'); const bg = useColorValue('blackAlpha.50', 'whiteAlpha.100');

View File

@@ -1,3 +1,3 @@
export * from './CardBody'; export * from './body';
export * from './CardFooter'; export * from './footer';
export * from './CardHeader'; export * from './header';

View File

@@ -0,0 +1,9 @@
import type { FlexProps } from '@chakra-ui/react';
export interface ICardBody extends Omit<FlexProps, 'onClick'> {
onClick?: () => boolean;
}
export interface ICardFooter extends FlexProps {}
export interface ICardHeader extends FlexProps {}

View File

@@ -1,24 +0,0 @@
import * as React from 'react';
import { Box, useColorMode } from '@chakra-ui/core';
export const CodeBlock = ({ children }) => {
const { colorMode } = useColorMode();
const bg = { dark: 'gray.800', light: 'blackAlpha.100' };
const color = { dark: 'white', light: 'black' };
return (
<Box
fontFamily="mono"
mt={5}
p={3}
border="1px"
borderColor="inherit"
rounded="md"
bg={bg[colorMode]}
color={color[colorMode]}
fontSize="sm"
whiteSpace="pre-wrap"
as="pre">
{children}
</Box>
);
};

View File

@@ -1,20 +0,0 @@
import * as React from 'react';
import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/core';
export const CopyButton = ({ bg = 'secondary', copyValue, ...props }) => {
const { onCopy, hasCopied } = useClipboard(copyValue);
return (
<Tooltip hasArrow label="Copy Output" placement="top">
<Button
as="a"
size="sm"
variantColor={bg}
zIndex="dropdown"
onClick={onCopy}
mx={1}
{...props}>
{hasCopied ? <Icon name="check" size="16px" /> : <Icon name="copy" size="16px" />}
</Button>
</Tooltip>
);
};

View File

@@ -1,20 +0,0 @@
namespace Chakra {
type ButtonProps = import('@chakra-ui/core').ButtonProps;
type CollapseProps = import('@chakra-ui/core').CollapseProps;
}
type TFooterSide = 'left' | 'right';
interface IFooterButton extends Chakra.ButtonProps {
side: TFooterSide;
href?: string;
}
interface IFooterContent extends Omit<Chakra.CollapseProps, 'children'> {
isOpen: boolean;
content: string;
side: TFooterSide;
children?: undefined;
}
type TFooterItems = 'help' | 'credit' | 'terms';

View File

@@ -1,23 +0,0 @@
import { Box, Collapse } from '@chakra-ui/core';
import { Markdown } from '~/components';
export const FooterContent = (props: IFooterContent) => {
const { isOpen = false, content, side = 'left', children: _, ...rest } = props;
return (
<Collapse
px={6}
py={4}
w="auto"
borderBottom="1px"
display="flex"
maxWidth="100%"
isOpen={isOpen}
flexBasis="auto"
justifyContent={side === 'left' ? 'flex-start' : 'flex-end'}
{...rest}>
<Box textAlign={side}>
<Markdown content={content} />
</Box>
</Collapse>
);
};

View File

@@ -1,126 +0,0 @@
import { useState } from 'react';
import { Box, Flex } from '@chakra-ui/core';
import { FiCode } from '@meronex/icons/fi';
import { GoLinkExternal } from '@meronex/icons/go';
import stringFormat from 'string-format';
import { useConfig, useColorValue } from '~/context';
import { FooterButton } from './FooterButton';
import { FooterContent } from './FooterContent';
export const Footer = () => {
const config = useConfig();
const [helpVisible, showHelp] = useState(false);
const [termsVisible, showTerms] = useState(false);
const [creditVisible, showCredit] = useState(false);
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
const footerColor = useColorValue('black', 'white');
const contentBorder = useColorValue('blackAlpha.100', 'whiteAlpha.200');
const handleCollapse = (i: TFooterItems) => {
if (i === 'help') {
showTerms(false);
showCredit(false);
showHelp(!helpVisible);
} else if (i === 'credit') {
showTerms(false);
showHelp(false);
showCredit(!creditVisible);
} else if (i === 'terms') {
showHelp(false);
showCredit(false);
showTerms(!termsVisible);
}
};
const extUrl = config.web.external_link.url.includes('{primary_asn}')
? stringFormat(config.web.external_link.url, { primary_asn: config.primary_asn })
: config.web.external_link.url || '/';
return (
<>
{config.web.help_menu.enable && (
<FooterContent
isOpen={helpVisible}
content={config.content.help_menu}
bg={footerBg}
borderColor={contentBorder}
side="left"
/>
)}
{config.web.terms.enable && (
<FooterContent
isOpen={termsVisible}
content={config.content.terms}
bg={footerBg}
borderColor={contentBorder}
side="left"
/>
)}
{config.web.credit.enable && (
<FooterContent
isOpen={creditVisible}
content={config.content.credit}
bg={footerBg}
borderColor={contentBorder}
side="right"
/>
)}
<Flex
py={[4, 4, 2, 2]}
px={6}
w="100%"
as="footer"
flexWrap="wrap"
textAlign="center"
alignItems="center"
bg={footerBg}
color={footerColor}
justifyContent="space-between">
{config.web.terms.enable && (
<FooterButton
side="left"
onClick={() => handleCollapse('terms')}
aria-label={config.web.terms.title}>
{config.web.terms.title}
</FooterButton>
)}
{config.web.help_menu.enable && (
<FooterButton
side="left"
onClick={() => handleCollapse('help')}
aria-label={config.web.help_menu.title}>
{config.web.help_menu.title}
</FooterButton>
)}
<Flex
flexBasis="auto"
flexGrow={0}
flexShrink={0}
maxWidth="100%"
marginRight="auto"
p={0}
/>
{config.web.credit.enable && (
<FooterButton
side="right"
onClick={() => handleCollapse('credit')}
aria-label="Powered by hyperglass">
<FiCode />
</FooterButton>
)}
{config.web.external_link.enable && (
<FooterButton
href={extUrl}
side="right"
aria-label={config.web.external_link.title}
variant="ghost"
rightIcon={<Box as={GoLinkExternal} />}
size="xs">
{config.web.external_link.title}
</FooterButton>
)}
</Flex>
</>
);
};

View File

@@ -1,9 +1,9 @@
import { Button, Flex, FlexProps } from '@chakra-ui/core'; import { Button } from '@chakra-ui/react';
import { withAnimation } from '~/components'; import { AnimatedDiv } from '~/components';
const AnimatedFlex = withAnimation<FlexProps>(Flex); import type { TFooterButton } from './types';
export const FooterButton = (props: IFooterButton) => { export const FooterButton = (props: TFooterButton) => {
const { side, href, ...rest } = props; const { side, href, ...rest } = props;
let buttonProps = Object(); let buttonProps = Object();
@@ -12,9 +12,10 @@ export const FooterButton = (props: IFooterButton) => {
} }
return ( return (
<AnimatedFlex <AnimatedDiv
p={0} p={0}
w="auto" w="auto"
d="flex"
flexGrow={0} flexGrow={0}
float={side} float={side}
flexShrink={0} flexShrink={0}
@@ -24,6 +25,6 @@ export const FooterButton = (props: IFooterButton) => {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}> transition={{ duration: 0.6 }}>
<Button size="xs" variant="ghost" {...buttonProps} {...rest} /> <Button size="xs" variant="ghost" {...buttonProps} {...rest} />
</AnimatedFlex> </AnimatedDiv>
); );
}; };

View File

@@ -0,0 +1,28 @@
import { Drawer, DrawerBody, DrawerOverlay, DrawerContent } from '@chakra-ui/react';
import { Markdown } from '~/components';
import type { TFooterContent } from './types';
export const FooterContent = (props: TFooterContent) => {
const { isOpen, onClose, content, side = 'left', ...rest } = props;
return (
<Drawer placement="bottom" isOpen={isOpen} onClose={onClose}>
<DrawerOverlay>
<DrawerContent
px={6}
py={4}
w="auto"
borderBottom="1px"
display="flex"
maxWidth="100%"
flexBasis="auto"
justifyContent={side === 'left' ? 'flex-start' : 'flex-end'}
{...rest}>
<DrawerBody textAlign={side}>
<Markdown content={content} />
</DrawerBody>
</DrawerContent>
</DrawerOverlay>
</Drawer>
);
};

View File

@@ -0,0 +1,101 @@
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import { FiCode } from '@meronex/icons/fi';
import { GoLinkExternal } from '@meronex/icons/go';
import stringFormat from 'string-format';
import { If } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { FooterButton } from './button';
import { FooterContent } from './content';
export const Footer = () => {
const { web, content, primary_asn } = useConfig();
const help = useDisclosure();
const terms = useDisclosure();
const credit = useDisclosure();
const footerBg = useColorValue('blackAlpha.50', 'whiteAlpha.100');
const footerColor = useColorValue('black', 'white');
const contentBorder = useColorValue('blackAlpha.100', 'whiteAlpha.200');
const extUrl = web.external_link.url.includes('{primary_asn}')
? stringFormat(web.external_link.url, { primary_asn })
: web.external_link.url ?? '/';
return (
<>
{web.help_menu.enable && (
<FooterContent
content={content.help_menu}
borderColor={contentBorder}
bg={footerBg}
side="left"
{...help}
/>
)}
{web.terms.enable && (
<FooterContent
content={content.terms}
borderColor={contentBorder}
bg={footerBg}
side="left"
{...terms}
/>
)}
{web.credit.enable && (
<FooterContent
borderColor={contentBorder}
content={content.credit}
bg={footerBg}
side="right"
{...credit}
/>
)}
<Flex
px={6}
w="100%"
as="footer"
bg={footerBg}
flexWrap="wrap"
textAlign="center"
alignItems="center"
color={footerColor}
py={{ base: 4, lg: 2 }}
justifyContent="space-between">
<If c={web.terms.enable}>
<FooterButton side="left" onClick={terms.onToggle} aria-label={web.terms.title}>
{web.terms.title}
</FooterButton>
</If>
<If c={web.help_menu.enable}>
<FooterButton side="left" onClick={help.onToggle} aria-label={web.help_menu.title}>
{web.help_menu.title}
</FooterButton>
</If>
<Flex
p={0}
flexGrow={0}
flexShrink={0}
maxWidth="100%"
flexBasis="auto"
marginRight="auto"
/>
<If c={web.credit.enable}>
<FooterButton side="right" onClick={credit.onToggle} aria-label="Powered by hyperglass">
<FiCode />
</FooterButton>
</If>
<If c={web.external_link.enable}>
<FooterButton
size="xs"
side="right"
href={extUrl}
variant="ghost"
aria-label={web.external_link.title}
rightIcon={<Box as={GoLinkExternal} />}>
{web.external_link.title}
</FooterButton>
</If>
</Flex>
</>
);
};

View File

@@ -1 +1 @@
export * from './FooterMain'; export * from './footer';

View File

@@ -0,0 +1,16 @@
import type { ButtonProps, DrawerProps, DrawerContentProps } from '@chakra-ui/react';
type TFooterSide = 'left' | 'right';
export interface TFooterButton extends ButtonProps {
side: TFooterSide;
href?: string;
}
export interface TFooterContent extends Omit<DrawerProps, 'children'>, DrawerContentProps {
isOpen: boolean;
content: string;
side: TFooterSide;
}
export type TFooterItems = 'help' | 'credit' | 'terms';

View File

@@ -1,149 +0,0 @@
import * as React from 'react';
import { Flex, useColorMode } from '@chakra-ui/core';
import { motion, AnimatePresence } from 'framer-motion';
import { useConfig, useHyperglassState, useMedia } from 'app/context';
import { Title, ResetButton, ColorModeToggle } from 'app/components';
const titleVariants = {
sm: {
fullSize: { scale: 1, marginLeft: 0 },
smallLogo: { marginLeft: 'auto' },
smallText: { marginLeft: 'auto' },
},
md: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
lg: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
xl: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
};
const bg = { light: 'white', dark: 'black' };
const headerTransition = {
type: 'spring',
ease: 'anticipate',
damping: 15,
stiffness: 100,
};
const titleJustify = {
true: ['flex-end', 'flex-end', 'center', 'center'],
false: ['flex-start', 'flex-start', 'center', 'center'],
};
const titleHeight = {
true: null,
false: [null, '20vh', '20vh', '20vh'],
};
const resetButtonMl = { true: [null, 2, 2, 2], false: null };
const widthMap = {
text_only: '100%',
logo_only: ['90%', '90%', '50%', '50%'],
logo_subtitle: ['90%', '90%', '50%', '50%'],
all: ['90%', '90%', '50%', '50%'],
};
export const Header = ({ layoutRef, ...props }) => {
const AnimatedFlex = motion.custom(Flex);
const AnimatedResetButton = motion.custom(ResetButton);
const { colorMode } = useColorMode();
const { web } = useConfig();
const { mediaSize } = useMedia();
const { isSubmitting, resetForm } = useHyperglassState();
const handleFormReset = () => {
resetForm(layoutRef);
};
const resetButton = (
<AnimatePresence key="resetButton">
<AnimatedFlex
layoutTransition={headerTransition}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, width: 'unset' }}
exit={{ opacity: 0, x: -50 }}
alignItems="center"
mb={[null, 'auto']}
ml={resetButtonMl[isSubmitting]}
display={isSubmitting ? 'flex' : 'none'}>
<AnimatedResetButton isSubmitting={isSubmitting} onClick={handleFormReset} />
</AnimatedFlex>
</AnimatePresence>
);
const title = (
<AnimatedFlex
key="title"
px={1}
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
positionTransition={headerTransition}
initial={{ scale: 0.5 }}
animate={
isSubmitting && web.text.title_mode === 'text_only'
? 'smallText'
: isSubmitting && web.text.title_mode !== 'text_only'
? 'smallLogo'
: 'fullSize'
}
variants={titleVariants[mediaSize]}
justifyContent={titleJustify[isSubmitting]}
mt={[null, isSubmitting ? null : 'auto']}
maxW={widthMap[web.text.title_mode]}
flex="1 0 0"
minH={titleHeight[isSubmitting]}>
<Title isSubmitting={isSubmitting} onClick={handleFormReset} />
</AnimatedFlex>
);
const colorModeToggle = (
<AnimatedFlex
layoutTransition={headerTransition}
key="colorModeToggle"
alignItems="center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
mb={[null, 'auto']}
mr={isSubmitting ? null : 2}>
<ColorModeToggle />
</AnimatedFlex>
);
const layout = {
false: {
sm: [title, resetButton, colorModeToggle],
md: [resetButton, title, colorModeToggle],
lg: [resetButton, title, colorModeToggle],
xl: [resetButton, title, colorModeToggle],
},
true: {
sm: [resetButton, colorModeToggle, title],
md: [resetButton, title, colorModeToggle],
lg: [resetButton, title, colorModeToggle],
xl: [resetButton, title, colorModeToggle],
},
};
return (
<Flex
px={2}
zIndex="4"
as="header"
width="full"
flex="0 1 auto"
bg={bg[colorMode]}
color="gray.500"
{...props}>
<Flex
w="100%"
mx="auto"
pt={6}
justify="space-between"
flex="1 0 auto"
alignItems={isSubmitting ? 'center' : 'flex-start'}>
{layout[isSubmitting][mediaSize]}
</Flex>
</Flex>
);
};

View File

@@ -1,57 +0,0 @@
import * as React from 'react';
import { forwardRef } from 'react';
import { Flex, useColorMode } from '@chakra-ui/core';
export const Label = forwardRef(
({ value, label, labelColor, valueBg, valueColor, ...props }, ref) => {
const { colorMode } = useColorMode();
const _labelColor = { dark: 'whiteAlpha.700', light: 'blackAlpha.700' };
return (
<Flex
ref={ref}
flexWrap="nowrap"
alignItems="center"
justifyContent="flex-start"
mx={[1, 2, 2, 2]}
my={2}
{...props}>
<Flex
display="inline-flex"
justifyContent="center"
lineHeight="1.5"
px={[1, 3, 3, 3]}
whiteSpace="nowrap"
mb={2}
mr={0}
bg={valueBg || 'primary.600'}
color={valueColor || 'white'}
borderBottomLeftRadius={4}
borderTopLeftRadius={4}
borderBottomRightRadius={0}
borderTopRightRadius={0}
fontWeight="bold"
fontSize={['xs', 'sm', 'sm', 'sm']}>
{value}
</Flex>
<Flex
display="inline-flex"
justifyContent="center"
lineHeight="1.5"
px={3}
whiteSpace="nowrap"
mb={2}
ml={0}
mr={0}
boxShadow={`inset 0px 0px 0px 1px ${valueBg || 'primary.600'}`}
color={labelColor || _labelColor[colorMode]}
borderBottomRightRadius={4}
borderTopRightRadius={4}
borderBottomLeftRadius={0}
borderTopLeftRadius={0}
fontSize={['xs', 'sm', 'sm', 'sm']}>
{label}
</Flex>
</Flex>
);
},
);

View File

@@ -10,9 +10,15 @@ const color = { light: 'black', dark: 'white' };
export const Layout = ({ children }) => { export const Layout = ({ children }) => {
const config = useConfig(); const config = useConfig();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const { greetingAck, setGreetingAck } = useHyperglassState(); const { greetingAck, setGreetingAck, setSubmitting, setFormData } = useHyperglassState();
const containerRef = useRef(null); const containerRef = useRef(null);
const resetForm = () => {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
setSubmitting(false);
setFormData({});
};
return ( return (
<> <>
<Flex <Flex
@@ -23,7 +29,7 @@ export const Layout = ({ children }) => {
flexDirection="column" flexDirection="column"
color={color[colorMode]}> color={color[colorMode]}>
<Flex px={2} flex="0 1 auto" flexDirection="column"> <Flex px={2} flex="0 1 auto" flexDirection="column">
<Header layoutRef={containerRef} /> <Header resetForm={resetForm} />
</Flex> </Flex>
<Flex <Flex
px={2} px={2}

View File

@@ -1,32 +0,0 @@
import * as React from 'react';
import { Flex, Spinner, useColorMode } from '@chakra-ui/core';
export const Loading = () => {
const { colorMode } = useColorMode();
const bg = { light: 'white', dark: 'black' };
const color = { light: 'black', dark: 'white' };
return (
<Flex
flexDirection="column"
minHeight="100vh"
w="100%"
bg={bg[colorMode]}
color={color[colorMode]}>
<Flex
as="main"
w="100%"
flexGrow={1}
flexShrink={1}
flexBasis="auto"
alignItems="center"
justifyContent="start"
textAlign="center"
flexDirection="column"
px={2}
py={0}
mt={['50%', '50%', '50%', '25%']}>
<Spinner color="primary.500" w="6rem" h="6rem" />
</Flex>
</Flex>
);
};

View File

@@ -10,11 +10,12 @@ import {
InlineCode, InlineCode,
Divider, Divider,
Table, Table,
} from './MDComponents'; } from './elements';
import type { IMarkdown } from './types'; import type { ReactMarkdownProps } from 'react-markdown';
import type { TMarkdown } from './types';
const mdComponents = { const renderers = {
paragraph: Paragraph, paragraph: Paragraph,
link: Link, link: Link,
heading: Heading, heading: Heading,
@@ -25,8 +26,8 @@ const mdComponents = {
code: CodeBlock, code: CodeBlock,
table: Table, table: Table,
tableCell: TableData, tableCell: TableData,
}; } as ReactMarkdownProps['renderers'];
export const Markdown = (props: IMarkdown) => ( export const Markdown = (props: TMarkdown) => (
<ReactMarkdown renderers={mdComponents} source={props.content} /> <ReactMarkdown renderers={renderers} source={props.content} />
); );

View File

@@ -1,40 +1,56 @@
import { import {
Checkbox as ChakraCheckbox,
Divider as ChakraDivider,
Code as ChakraCode,
Heading as ChakraHeading,
Link as ChakraLink,
ListItem as ChakraListItem,
Text as ChakraText,
UnorderedList,
OrderedList, OrderedList,
} from '@chakra-ui/core'; UnorderedList,
Code as ChakraCode,
Link as ChakraLink,
Text as ChakraText,
Divider as ChakraDivider,
Heading as ChakraHeading,
Checkbox as ChakraCheckbox,
ListItem as ChakraListItem,
} from '@chakra-ui/react';
import { TableCell, TableHeader, Table as ChakraTable } from './MDTable'; import { TD, TH, Table as ChakraTable } from './table';
import { CodeBlock as CustomCodeBlock } from '~/components'; import { CodeBlock as CustomCodeBlock, If } from '~/components';
import type { BoxProps, TextProps, CodeProps, DividerProps, LinkProps } from '@chakra-ui/core'; import type {
import type { ICheckbox, IList, IHeading, ICodeBlock, ITableData } from './types'; BoxProps,
TextProps,
CodeProps,
LinkProps,
HeadingProps,
DividerProps,
} from '@chakra-ui/react';
import type { TCheckbox, TList, THeading, TCodeBlock, TTableData, TListItem } from './types';
export const Checkbox = (props: ICheckbox) => { export const Checkbox = (props: TCheckbox) => {
const { checked, ...rest } = props; const { checked, ...rest } = props;
return <ChakraCheckbox isChecked={checked} {...rest} />; return <ChakraCheckbox isChecked={checked} {...rest} />;
}; };
export const List = (props: IList) => { export const List = (props: TList) => {
const { ordered, ...rest } = props; const { ordered, ...rest } = props;
const Component = ordered ? OrderedList : UnorderedList; return (
return <Component {...rest} />; <>
<If c={ordered}>
<OrderedList {...rest} />
</If>
<If c={!ordered}>
<UnorderedList {...rest} />
</If>
</>
);
}; };
export const ListItem = (props: ICheckbox) => { export const ListItem = (props: TListItem) => {
const { checked, ...rest } = props; const { checked, ...rest } = props;
return checked ? <Checkbox checked={checked} {...rest} /> : <ChakraListItem {...rest} />; return checked ? <Checkbox checked={checked} {...rest} /> : <ChakraListItem {...rest} />;
}; };
export const Heading = (props: IHeading) => { export const Heading = (props: THeading) => {
const { level, ...rest } = props; const { level, ...rest } = props;
const levelMap = { const levelMap = {
1: { as: 'h1', size: 'lg', fontWeight: 'bold' }, 1: { as: 'h1', size: 'lg', fontWeight: 'bold' },
2: { as: 'h2', size: 'lg', fontWeight: 'normal' }, 2: { as: 'h2', size: 'lg', fontWeight: 'normal' },
@@ -42,18 +58,27 @@ export const Heading = (props: IHeading) => {
4: { as: 'h4', size: 'md', fontWeight: 'normal' }, 4: { as: 'h4', size: 'md', fontWeight: 'normal' },
5: { as: 'h5', size: 'md', fontWeight: 'bold' }, 5: { as: 'h5', size: 'md', fontWeight: 'bold' },
6: { as: 'h6', size: 'sm', fontWeight: 'bold' }, 6: { as: 'h6', size: 'sm', fontWeight: 'bold' },
}; } as { [i: number]: HeadingProps };
return <ChakraHeading {...levelMap[level]} {...rest} />; return <ChakraHeading {...levelMap[level]} {...rest} />;
}; };
export const Link = (props: LinkProps) => <ChakraLink isExternal {...props} />; export const Link = (props: LinkProps) => <ChakraLink isExternal {...props} />;
export const CodeBlock = (props: ICodeBlock) => <CustomCodeBlock>{props.value}</CustomCodeBlock>; export const CodeBlock = (props: TCodeBlock) => <CustomCodeBlock>{props.value}</CustomCodeBlock>;
export const TableData = (props: ITableData) => { export const TableData = (props: TTableData) => {
const { isHeader, ...rest } = props; const { isHeader, ...rest } = props;
const Component = isHeader ? TableHeader : TableCell; return (
return <Component {...rest} />; <>
<If c={isHeader}>
<TH {...rest} />
</If>
<If c={!isHeader}>
<TD {...rest} />
</If>
</>
);
}; };
export const Paragraph = (props: TextProps) => <ChakraText {...props} />; export const Paragraph = (props: TextProps) => <ChakraText {...props} />;

View File

@@ -1 +1 @@
export * from './Markdown'; export * from './markdown';

View File

@@ -1,28 +1,27 @@
import { Box } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { BoxProps } from '@chakra-ui/core';
import type { ITableData } from './types'; import type { BoxProps } from '@chakra-ui/react';
export const Table = (props: BoxProps) => ( export const Table = (props: BoxProps) => (
<Box as="table" textAlign="left" mt={4} width="full" {...props} /> <Box as="table" textAlign="left" mt={4} width="full" {...props} />
); );
export const TableHeader = (props: BoxProps) => { export const TH = (props: BoxProps) => {
const bg = useColorValue('blackAlpha.50', 'whiteAlpha.50'); const bg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
return <Box as="th" bg={bg} fontWeight="semibold" p={2} fontSize="sm" {...props} />; return <Box as="th" bg={bg} fontWeight="semibold" p={2} fontSize="sm" {...props} />;
}; };
export const TableCell = (props: ITableData) => { export const TD = (props: BoxProps) => {
const { isHeader = false, ...rest } = props;
return ( return (
<Box <Box
as={isHeader ? 'th' : 'td'}
p={2} p={2}
borderTopWidth="1px" as="td"
borderColor="inherit"
fontSize="sm" fontSize="sm"
whiteSpace="normal" whiteSpace="normal"
{...rest} borderTopWidth="1px"
borderColor="inherit"
{...props}
/> />
); );
}; };

View File

@@ -1,24 +1,36 @@
import type { ReactNode } from 'react'; import type {
import type { BoxProps, CheckboxProps, HeadingProps } from '@chakra-ui/core'; BoxProps,
export interface IMarkdown { CheckboxProps,
HeadingProps,
ListProps,
ListItemProps,
} from '@chakra-ui/react';
export interface TMarkdown {
content: string; content: string;
} }
export interface ICheckbox extends CheckboxProps {
export interface TCheckbox extends CheckboxProps {
checked: boolean; checked: boolean;
} }
export interface IList { export interface TListItem extends ListItemProps {
ordered: boolean; checked: boolean;
children?: ReactNode;
} }
export interface IHeading extends HeadingProps {
export interface TList extends ListProps {
ordered: boolean;
children?: React.ReactNode;
}
export interface THeading extends HeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6; level: 1 | 2 | 3 | 4 | 5 | 6;
} }
export interface ICodeBlock { export interface TCodeBlock {
value: ReactNode; value: React.ReactNode;
} }
export interface ITableData extends BoxProps { export interface TTableData extends BoxProps {
isHeader: boolean; isHeader: boolean;
} }

View File

@@ -1,55 +0,0 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useTheme } from '@chakra-ui/core';
import { useConfig } from 'app/context';
import { googleFontUrl } from 'app/util';
export const Meta = () => {
const config = useConfig();
const theme = useTheme();
const [location, setLocation] = useState({});
const title = config?.site_title || 'hyperglass';
const description = config?.site_description || 'Network Looking Glass';
const siteName = `${title} - ${description}`;
const keywords = config?.site_keywords || [
'hyperglass',
'looking glass',
'lg',
'peer',
'peering',
'ipv4',
'ipv6',
'transit',
'community',
'communities',
'bgp',
'routing',
'network',
'isp',
];
const language = config?.language ?? 'en';
const primaryFont = googleFontUrl(theme.fonts.body);
const monoFont = googleFontUrl(theme.fonts.mono);
useEffect(() => {
if (typeof window !== 'undefined' && location === {}) {
setLocation(window.location);
}
}, [location]);
return (
<Head>
<title>{title}</title>
<meta name="hg-version" content={config.hyperglass_version} />
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
<meta name="language" content={language} />
<meta name="url" content={location.href} />
<meta name="og:title" content={title} />
<meta name="og:url" content={location.href} />
<meta name="og:description" content={description} />
<meta property="og:image:alt" content={siteName} />
<link href={primaryFont} rel="stylesheet" />
<link href={monoFont} rel="stylesheet" />
</Head>
);
};

View File

@@ -1,15 +0,0 @@
import * as React from 'react';
import { Button } from '@chakra-ui/core';
import { FiChevronLeft } from '@meronex/icons/fi';
export const ResetButton = React.forwardRef(({ isSubmitting, onClick }, ref) => (
<Button
ref={ref}
color="current"
variant="ghost"
onClick={onClick}
aria-label="Reset Form"
opacity={isSubmitting ? 1 : 0}>
<FiChevronLeft size={24} />
</Button>
));

View File

@@ -1,262 +0,0 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import { forwardRef, useEffect, useState } from 'react';
import {
AccordionItem,
AccordionHeader,
AccordionPanel,
Alert,
Box,
ButtonGroup,
css,
Flex,
Tooltip,
Text,
useColorMode,
useTheme,
} from '@chakra-ui/core';
import { BsLightningFill } from '@meronex/icons/bs';
import styled from '@emotion/styled';
import useAxios from 'axios-hooks';
import strReplace from 'react-string-replace';
import format from 'string-format';
import { startCase } from 'lodash';
import { useConfig, useMedia } from 'app/context';
import {
BGPTable,
CacheTimeout,
CopyButton,
RequeryButton,
ResultHeader,
TextOutput,
} from 'app/components';
import { tableToString } from 'app/util';
format.extend(String.prototype, {});
const FormattedError = ({ keywords, message }) => {
const patternStr = keywords.map(kw => `(${kw})`).join('|');
const pattern = new RegExp(patternStr, 'gi');
let errorFmt;
try {
errorFmt = strReplace(message, pattern, match => (
<Text key={match} as="strong">
{match}
</Text>
));
} catch (err) {
errorFmt = <Text as="span">{message}</Text>;
}
return <Text as="span">{keywords.length !== 0 ? errorFmt : message}</Text>;
};
const AccordionHeaderWrapper = styled(Flex)`
justify-content: space-between;
&:hover {
background-color: ${props => props.hoverBg};
}
&:focus {
box-shadow: 'outline';
}
`;
const statusMap = {
success: 'success',
warning: 'warning',
error: 'warning',
danger: 'error',
};
const color = { dark: 'white', light: 'black' };
const scrollbar = { dark: 'whiteAlpha.300', light: 'blackAlpha.300' };
const scrollbarHover = { dark: 'whiteAlpha.400', light: 'blackAlpha.400' };
const scrollbarBg = { dark: 'whiteAlpha.50', light: 'blackAlpha.50' };
export const Result = forwardRef(
(
{
device,
timeout,
queryLocation,
queryType,
queryVrf,
queryTarget,
index,
resultsComplete,
setComplete,
},
ref,
) => {
const config = useConfig();
const theme = useTheme();
const { isSm } = useMedia();
const { colorMode } = useColorMode();
let [{ data, loading, error }, refetch] = useAxios({
url: '/api/query/',
method: 'post',
data: {
query_location: queryLocation,
query_type: queryType,
query_vrf: queryVrf,
query_target: queryTarget,
},
timeout: timeout,
useCache: false,
});
const [isOpen, setOpen] = useState(false);
const [hasOverride, setOverride] = useState(false);
const handleToggle = () => {
setOpen(!isOpen);
setOverride(true);
};
const errorKw = (error && error.response?.data?.keywords) || [];
let errorMsg;
if (error && error.response?.data?.output) {
errorMsg = error.response.data.output;
} else if (error && error.message.startsWith('timeout')) {
errorMsg = config.messages.request_timeout;
} else if (error?.response?.statusText) {
errorMsg = startCase(error.response.statusText);
} else if (error && error.message) {
errorMsg = startCase(error.message);
} else {
errorMsg = config.messages.general;
}
error && console.error(error);
const errorLevel =
(error?.response?.data?.level && statusMap[error.response?.data?.level]) ?? 'error';
const structuredDataComponent = {
bgp_route: BGPTable,
bgp_aspath: BGPTable,
bgp_community: BGPTable,
ping: TextOutput,
traceroute: TextOutput,
};
let Output = TextOutput;
let copyValue = data?.output;
if (data?.format === 'application/json') {
Output = structuredDataComponent[queryType];
copyValue = tableToString(queryTarget, data, config);
}
if (error) {
copyValue = errorMsg;
}
useEffect(() => {
!loading && resultsComplete === null && setComplete(index);
}, [loading, resultsComplete]);
useEffect(() => {
resultsComplete === index && !hasOverride && setOpen(true);
}, [resultsComplete, index]);
return (
<AccordionItem
isOpen={isOpen}
isDisabled={loading}
ref={ref}
css={css({
'&:last-of-type': { borderBottom: 'none' },
'&:first-of-type': { borderTop: 'none' },
})(theme)}>
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
<AccordionHeader
flex="1 0 auto"
py={2}
_hover={{}}
_focus={{}}
w="unset"
onClick={handleToggle}>
<ResultHeader
title={device.display_name}
loading={loading}
error={error}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime}
/>
</AccordionHeader>
<ButtonGroup px={[1, 1, 3, 3]} py={2}>
<CopyButton copyValue={copyValue} variant="ghost" isDisabled={loading} />
<RequeryButton requery={refetch} variant="ghost" isDisabled={loading} />
</ButtonGroup>
</AccordionHeaderWrapper>
<AccordionPanel
pb={4}
overflowX="auto"
css={css({
WebkitOverflowScrolling: 'touch',
'&::-webkit-scrollbar': { height: '5px' },
'&::-webkit-scrollbar-track': {
backgroundColor: scrollbarBg[colorMode],
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: scrollbar[colorMode],
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: scrollbarHover[colorMode],
},
'-ms-overflow-style': { display: 'none' },
})(theme)}>
<Flex direction="column" flexWrap="wrap">
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : null}>
{!error && data && <Output>{data?.output}</Output>}
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
</Alert>
)}
</Flex>
</Flex>
<Flex direction="row" flexWrap="wrap">
<Flex
px={3}
mt={2}
justifyContent={['flex-start', 'flex-start', 'flex-end', 'flex-end']}
flex="1 0 auto">
{config.cache.show_text && data && !error && (
<>
{!isSm && (
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
)}
<Tooltip
display={data?.cached ? null : 'none'}
hasArrow
label={config.web.text.cache_icon.format({
time: data?.timestamp,
})}
placement="top">
<Box ml={1} display={data?.cached ? 'block' : 'none'}>
<BsLightningFill color={color[colorMode]} />
</Box>
</Tooltip>
{isSm && (
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
)}
</>
)}
</Flex>
</Flex>
</AccordionPanel>
</AccordionItem>
);
},
);

View File

@@ -1,57 +0,0 @@
import * as React from 'react';
import { forwardRef } from 'react';
import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip, useColorMode } from '@chakra-ui/core';
import format from 'string-format';
import { useConfig } from 'app/context';
format.extend(String.prototype, {});
const runtimeText = (runtime, text) => {
let unit;
if (runtime === 1) {
unit = 'second';
} else {
unit = 'seconds';
}
const fmt = text.format({ seconds: runtime });
return `${fmt} ${unit}`;
};
const statusColor = { dark: 'primary.300', light: 'primary.500' };
const warningColor = { dark: 300, light: 500 };
const defaultStatusColor = {
dark: 'success.300',
light: 'success.500',
};
export const ResultHeader = forwardRef(
({ title, loading, error, errorMsg, errorLevel, runtime }, ref) => {
const { colorMode } = useColorMode();
const config = useConfig();
return (
<Stack ref={ref} isInline alignItems="center" w="100%">
{loading ? (
<Spinner size="sm" mr={4} color={statusColor[colorMode]} />
) : error ? (
<Tooltip hasArrow label={errorMsg} placement="top">
<Icon
name="warning"
color={`${errorLevel}.${warningColor[colorMode]}`}
mr={4}
size={6}
/>
</Tooltip>
) : (
<Tooltip
hasArrow
label={runtimeText(runtime, config.web.text.complete_time)}
placement="top">
<Icon name="check" color={defaultStatusColor[colorMode]} mr={4} size={6} />
</Tooltip>
)}
<Text fontSize="lg">{title}</Text>
<AccordionIcon ml="auto" />
</Stack>
);
},
);

View File

@@ -1,157 +0,0 @@
import * as React from 'react';
import { useState } from 'react';
import { Accordion, Box, Stack, useTheme } from '@chakra-ui/core';
import { motion, AnimatePresence } from 'framer-motion';
import { Label, Result } from 'app/components';
import { useConfig, useMedia } from 'app/context';
const AnimatedResult = motion.custom(Result);
const AnimatedLabel = motion.custom(Label);
const labelInitial = {
left: {
sm: { opacity: 0, x: -100 },
md: { opacity: 0, x: -100 },
lg: { opacity: 0, x: -100 },
xl: { opacity: 0, x: -100 },
},
center: {
sm: { opacity: 0 },
md: { opacity: 0 },
lg: { opacity: 0 },
xl: { opacity: 0 },
},
right: {
sm: { opacity: 0, x: 100 },
md: { opacity: 0, x: 100 },
lg: { opacity: 0, x: 100 },
xl: { opacity: 0, x: 100 },
},
};
const labelAnimate = {
left: {
sm: { opacity: 1, x: 0 },
md: { opacity: 1, x: 0 },
lg: { opacity: 1, x: 0 },
xl: { opacity: 1, x: 0 },
},
center: {
sm: { opacity: 1 },
md: { opacity: 1 },
lg: { opacity: 1 },
xl: { opacity: 1 },
},
right: {
sm: { opacity: 1, x: 0 },
md: { opacity: 1, x: 0 },
lg: { opacity: 1, x: 0 },
xl: { opacity: 1, x: 0 },
},
};
export const Results = ({
queryLocation,
queryType,
queryVrf,
queryTarget,
setSubmitting,
...props
}) => {
const config = useConfig();
const theme = useTheme();
const { mediaSize } = useMedia();
const matchedVrf =
config.vrfs.filter(v => v.id === queryVrf)[0] ?? config.vrfs.filter(v => v.id === 'default')[0];
const [resultsComplete, setComplete] = useState(null);
return (
<>
<Box
maxW={['100%', '100%', '75%', '50%']}
w="100%"
p={0}
mx="auto"
my={4}
textAlign="left"
{...props}>
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
<AnimatePresence>
{queryLocation && (
<>
<AnimatedLabel
initial={labelInitial.left[mediaSize]}
animate={labelAnimate.left[mediaSize]}
transition={{ duration: 0.3, delay: 0.3 }}
exit={{ opacity: 0, x: -100 }}
label={config.web.text.query_type}
value={config.queries[queryType].display_name}
valueBg={theme.colors.cyan[500]}
fontSize={['xs', 'sm', 'sm', 'sm']}
/>
<AnimatedLabel
initial={labelInitial.center[mediaSize]}
animate={labelAnimate.center[mediaSize]}
transition={{ duration: 0.3, delay: 0.3 }}
exit={{ opacity: 0, scale: 0.5 }}
label={config.web.text.query_target}
value={queryTarget}
valueBg={theme.colors.teal[600]}
fontSize={['xs', 'sm', 'sm', 'sm']}
/>
<AnimatedLabel
initial={labelInitial.right[mediaSize]}
animate={labelAnimate.right[mediaSize]}
transition={{ duration: 0.3, delay: 0.3 }}
exit={{ opacity: 0, x: 100 }}
label={config.web.text.query_vrf}
value={matchedVrf.display_name}
valueBg={theme.colors.blue[500]}
fontSize={['xs', 'sm', 'sm', 'sm']}
/>
</>
)}
</AnimatePresence>
</Stack>
</Box>
<Box
maxW={['100%', '100%', '75%', '75%']}
w="100%"
p={0}
mx="auto"
my={4}
textAlign="left"
borderWidth="1px"
rounded="lg"
overflow="hidden">
<Accordion
allowMultiple
initial={{ opacity: 1 }}
transition={{ duration: 0.3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 300 }}>
<AnimatePresence>
{queryLocation &&
queryLocation.map((loc, i) => (
<AnimatedResult
initial={{ opacity: 0, y: 300 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: i * 0.3 }}
exit={{ opacity: 0, y: 300 }}
key={loc}
timeout={config.request_timeout * 1000}
device={config.devices[loc]}
queryLocation={loc}
queryType={queryType}
queryVrf={queryVrf}
queryTarget={queryTarget}
setSubmitting={setSubmitting}
index={i}
resultsComplete={resultsComplete}
setComplete={setComplete}
/>
))}
</AnimatePresence>
</Accordion>
</Box>
</>
);
};

View File

@@ -1,113 +0,0 @@
import * as React from 'react';
import { forwardRef } from 'react';
import { Box, PseudoBox, Spinner, useColorMode, useTheme } from '@chakra-ui/core';
import { FiSearch } from '@meronex/icons/fi';
import { opposingColor } from 'app/util';
const btnProps = {
display: 'inline-flex',
appearance: 'none',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 250ms',
userSelect: 'none',
position: 'relative',
whiteSpace: 'nowrap',
verticalAlign: 'middle',
lineHeight: '1.2',
outline: 'none',
as: 'button',
type: 'submit',
borderRadius: 'md',
fontWeight: 'semibold',
};
const btnSizeMap = {
lg: {
height: 12,
minWidth: 12,
fontSize: 'lg',
px: 6,
},
md: {
height: 10,
minWidth: 10,
fontSize: 'md',
px: 4,
},
sm: {
height: 8,
minWidth: 8,
fontSize: 'sm',
px: 3,
},
xs: {
height: 6,
minWidth: 6,
fontSize: 'xs',
px: 2,
},
};
const btnBg = { dark: 'primary.300', light: 'primary.500' };
const btnBgActive = { dark: 'primary.400', light: 'primary.600' };
const btnBgHover = { dark: 'primary.200', light: 'primary.400' };
export const SubmitButton = forwardRef(
(
{
isLoading = false,
isDisabled = false,
isActive = false,
isFullWidth = false,
size = 'lg',
loadingText,
children,
...props
},
ref,
) => {
const _isDisabled = isDisabled || isLoading;
const { colorMode } = useColorMode();
const theme = useTheme();
const btnColor = opposingColor(theme, btnBg[colorMode]);
const btnColorActive = opposingColor(theme, btnBgActive[colorMode]);
const btnColorHover = opposingColor(theme, btnBgHover[colorMode]);
const btnSize = btnSizeMap[size];
return (
<PseudoBox
ref={ref}
disabled={_isDisabled}
aria-disabled={_isDisabled}
aria-label="Submit Query"
width={isFullWidth ? 'full' : undefined}
data-active={isActive ? 'true' : undefined}
bg={btnBg[colorMode]}
color={btnColor}
_active={{ bg: btnBgActive[colorMode], color: btnColorActive }}
_hover={{ bg: btnBgHover[colorMode], color: btnColorHover }}
_focus={{ boxShadow: theme.shadows.outline }}
{...btnProps}
{...btnSize}
{...props}>
{isLoading ? (
<Spinner
position={loadingText ? 'relative' : 'absolute'}
mr={loadingText ? 2 : 0}
color="currentColor"
size="1em"
/>
) : (
<FiSearch color={btnColor} />
)}
{isLoading
? loadingText || (
<Box as="span" opacity="0">
{children}
</Box>
)
: children}
</PseudoBox>
);
},
);

View File

@@ -1,182 +1,34 @@
import { Flex, Icon, Text } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table'; import { useColorValue } from '~/context';
import { useMedia } from '~/context';
import { CardBody, CardFooter, CardHeader, If } from '~/components';
import { TableMain } from './TableMain';
import { TableCell } from './TableCell';
import { TableHead } from './TableHead';
import { TableRow } from './TableRow';
import { TableBody } from './TableBody';
import { TableIconButton } from './TableIconButton';
import { TableSelectShow } from './TableSelectShow';
import type { ITable } from './types'; import type { BoxProps } from '@chakra-ui/react';
export const Table = (props: ITable) => { export const TableMain = (props: BoxProps) => {
const { const scrollbar = useColorValue('blackAlpha.300', 'whiteAlpha.300');
columns, const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
data, const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
heading, return (
onRowClick, <Box
striped = false, as="table"
bordersVertical = false, display="block"
bordersHorizontal = false, overflowX="auto"
cellRender, borderRadius="md"
rowHighlightProp, boxSizing="border-box"
rowHighlightBg, css={{
rowHighlightColor, '&::-webkit-scrollbar': { height: '5px' },
} = props; '&::-webkit-scrollbar-track': {
backgroundColor: scrollbarBg,
const { isSm, isMd } = useMedia(); },
'&::-webkit-scrollbar-thumb': {
const defaultColumn = { backgroundColor: scrollbar,
minWidth: 100, },
width: 150, '&::-webkit-scrollbar-thumb:hover': {
maxWidth: 300, backgroundColor: scrollbarHover,
};
let hiddenColumns = [] as string[];
for (const col of columns) {
if (col.hidden) {
hiddenColumns.push(col.accessor);
}
}
const table = useTable(
{
columns,
defaultColumn,
data,
initialState: { hiddenColumns },
}, },
useSortBy,
usePagination,
);
const { '-ms-overflow-style': { display: 'none' },
getTableProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = table;
return (
<CardBody>
{heading && <CardHeader>{heading}</CardHeader>}
<TableMain {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup, i) => (
<TableRow index={i} {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableCell
as="th"
align={column.align}
{...column.getHeaderProps()}
{...column.getSortByToggleProps()}>
<Text fontSize="sm" fontWeight="bold" display="inline-block">
{column.render('Header')}
</Text>
<If condition={column.isSorted}>
<If condition={column.isSortedDesc}>
<Icon name="chevron-down" size={4} ml={1} />
</If>
<If condition={!column.isSortedDesc}>
<Icon name="chevron-up" size={4} ml={1} />
</If>
</If>
<If condition={!column.isSorted}>{''}</If>
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map(
(row, key) =>
prepareRow(row) || (
<TableRow
index={key}
doStripe={striped}
doHorizontalBorders={bordersHorizontal}
onClick={() => onRowClick && onRowClick(row)}
key={key}
highlight={row.values[rowHighlightProp ?? ''] ?? false}
highlightBg={rowHighlightBg}
highlightColor={rowHighlightColor}
{...row.getRowProps()}>
{row.cells.map((cell, i) => {
return (
<TableCell
align={cell.column.align}
cell={cell}
bordersVertical={[bordersVertical, i]}
key={cell.row.index}
{...cell.getCellProps()}>
{cell.render(cellRender ?? 'Cell')}
</TableCell>
);
})}
</TableRow>
),
)}
</TableBody>
</TableMain>
<CardFooter>
<Flex direction="row">
<TableIconButton
mr={2}
onClick={() => gotoPage(0)}
isDisabled={!canPreviousPage}
icon={() => <Icon name="arrow-left" size={3} />}
/>
<TableIconButton
mr={2}
onClick={() => previousPage()}
isDisabled={!canPreviousPage}
icon={() => <Icon name="chevron-left" size={6} />}
/>
</Flex>
<Flex justifyContent="center" alignItems="center">
<Text fontSize="sm" mr={4} whiteSpace="nowrap">
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</Text>
{!(isSm || isMd) && (
<TableSelectShow
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}} }}
{...props}
/> />
)}
</Flex>
<Flex direction="row">
<TableIconButton
ml={2}
isDisabled={!canNextPage}
onClick={() => nextPage()}
icon={() => <Icon name="chevron-right" size={6} />}
/>
<TableIconButton
ml={2}
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
isDisabled={!canNextPage}
icon={() => <Icon name="arrow-right" size={3} />}
/>
</Flex>
</CardFooter>
</CardBody>
); );
}; };

View File

@@ -1,7 +0,0 @@
import { IconButton } from '@chakra-ui/core';
import type { IconButtonProps } from '@chakra-ui/core';
export const TableIconButton = (props: IconButtonProps) => (
<IconButton size="sm" borderWidth={1} {...props} aria-label="Table Icon Button" />
);

View File

@@ -1,34 +0,0 @@
import { Box } from '@chakra-ui/core';
import { useColorValue } from '~/context';
import type { BoxProps } from '@chakra-ui/core';
export const TableMain = (props: BoxProps) => {
const scrollbar = useColorValue('blackAlpha.300', 'whiteAlpha.300');
const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Box
as="table"
display="block"
overflowX="auto"
borderRadius="md"
boxSizing="border-box"
css={{
'&::-webkit-scrollbar': { height: '5px' },
'&::-webkit-scrollbar-track': {
backgroundColor: scrollbarBg,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: scrollbar,
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: scrollbarHover,
},
'-ms-overflow-style': { display: 'none' },
}}
{...props}
/>
);
};

View File

@@ -1,5 +1,6 @@
import { Box } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import type { BoxProps } from '@chakra-ui/core';
import type { BoxProps } from '@chakra-ui/react';
export const TableBody = (props: BoxProps) => ( export const TableBody = (props: BoxProps) => (
<Box <Box

View File

@@ -0,0 +1,7 @@
import { IconButton } from '@chakra-ui/react';
import type { TTableIconButton } from './types';
export const TableIconButton = (props: TTableIconButton) => (
<IconButton size="sm" borderWidth={1} {...props} aria-label="Table Icon Button" />
);

View File

@@ -1,10 +1,10 @@
import { Box } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { ITableCell } from './types'; import type { TTableCell } from './types';
export const TableCell = (props: ITableCell) => { export const TableCell = (props: TTableCell) => {
const { bordersVertical = [false, 0, 0], align, ...rest } = props; const { bordersVertical = [false, 0], align, ...rest } = props;
const [doVerticalBorders, index] = bordersVertical; const [doVerticalBorders, index] = bordersVertical;
const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100'); const borderLeftColor = useColorValue('blackAlpha.100', 'whiteAlpha.100');

View File

@@ -1,7 +1,7 @@
import { Box } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { BoxProps } from '@chakra-ui/core'; import type { BoxProps } from '@chakra-ui/react';
export const TableHead = (props: BoxProps) => { export const TableHead = (props: BoxProps) => {
const bg = useColorValue('blackAlpha.100', 'whiteAlpha.100'); const bg = useColorValue('blackAlpha.100', 'whiteAlpha.100');

View File

@@ -1,8 +1,8 @@
export * from './Table'; export * from './body';
export * from './TableBody'; export * from './button';
export * from './TableCell'; export * from './cell';
export * from './TableHead'; export * from './head';
export * from './TableIconButton'; export * from './main';
export * from './TableMain'; export * from './main';
export * from './TableRow'; export * from './pageSelect';
export * from './TableSelectShow'; export * from './row';

View File

@@ -0,0 +1,191 @@
import dynamic from 'next/dynamic';
import { Flex, Icon, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table';
import { useMobile } from '~/context';
import { CardBody, CardFooter, CardHeader, If } from '~/components';
import { TableMain } from './table';
import { TableCell } from './cell';
import { TableHead } from './head';
import { TableRow } from './row';
import { TableBody } from './body';
import { TableIconButton } from './button';
import { TableSelectShow } from './pageSelect';
import type { TableOptions, PluginHook, Row } from 'react-table';
import type { TTable } from './types';
const ChevronUp = dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaChevronUp));
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 function Table(props: TTable) {
const {
data,
columns,
heading,
cellRender,
rowHighlightBg,
striped = false,
rowHighlightProp,
bordersVertical = false,
bordersHorizontal = false,
} = props;
const isMobile = useMobile();
const defaultColumn = {
minWidth: 100,
width: 150,
maxWidth: 300,
};
let hiddenColumns = [] as string[];
for (const col of columns) {
if (col.hidden) {
hiddenColumns.push(col.accessor);
}
}
const options = {
columns,
defaultColumn,
data,
initialState: { hiddenColumns },
} as TableOptions<TRoute>;
const plugins = [useSortBy, usePagination] as PluginHook<TRoute>[];
const instance = useTable<TRoute>(options, ...plugins);
const {
getTableProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = instance;
return (
<CardBody>
{heading && <CardHeader>{heading}</CardHeader>}
<TableMain {...getTableProps()}>
<TableHead>
{headerGroups.map((headerGroup, i) => (
<TableRow index={i} {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableCell
as="th"
align={column.align}
{...column.getHeaderProps()}
{...column.getSortByToggleProps()}>
<Text fontSize="sm" fontWeight="bold" display="inline-block">
{column.render('Header')}
</Text>
<If c={column.isSorted}>
<If c={typeof column.isSortedDesc !== 'undefined'}>
<Icon as={ChevronDown} boxSize={4} ml={1} />
</If>
<If c={!column.isSortedDesc}>
<Icon as={ChevronUp} boxSize={4} ml={1} />
</If>
</If>
<If c={!column.isSorted}>{''}</If>
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map((row, key) => {
prepareRow(row);
return (
<TableRow
index={key}
doStripe={striped}
highlightBg={rowHighlightBg}
doHorizontalBorders={bordersHorizontal}
highlight={row.values[rowHighlightProp ?? ''] ?? false}
{...row.getRowProps()}>
{row.cells.map((cell, i) => {
return (
<TableCell
align={cell.column.align}
bordersVertical={[bordersVertical, i]}
{...cell.getCellProps()}>
{cellRender ?? cell.render('Cell')}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</TableMain>
<CardFooter>
<Flex direction="row">
<TableIconButton
mr={2}
onClick={() => gotoPage(0)}
isDisabled={!canPreviousPage}
icon={<Icon as={DoubleChevronLeft} boxSize={3} />}
/>
<TableIconButton
mr={2}
onClick={() => previousPage()}
isDisabled={!canPreviousPage}
icon={<Icon as={DoubleChevronLeft} boxSize={6} />}
/>
</Flex>
<Flex justifyContent="center" alignItems="center">
<Text fontSize="sm" mr={4} whiteSpace="nowrap">
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</Text>
{!isMobile && (
<TableSelectShow
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
/>
)}
</Flex>
<Flex direction="row">
<TableIconButton
ml={2}
onClick={nextPage}
isDisabled={!canNextPage}
icon={<Icon as={ChevronUp} boxSize={6} />}
/>
<TableIconButton
ml={2}
isDisabled={!canNextPage}
icon={<Icon as={DoubleChevronRight} boxSize={3} />}
onClick={() => gotoPage(pageCount ? pageCount - 1 : 1)}
/>
</Flex>
</CardFooter>
</CardBody>
);
}

View File

@@ -1,5 +1,5 @@
import { Select } from '@chakra-ui/core'; import { Select } from '@chakra-ui/react';
import { SelectProps } from '@chakra-ui/core'; import { SelectProps } from '@chakra-ui/react';
export const TableSelectShow = (props: SelectProps) => { export const TableSelectShow = (props: SelectProps) => {
const { value, ...rest } = props; const { value, ...rest } = props;

View File

@@ -1,19 +1,18 @@
import { Box, useColorMode } from '@chakra-ui/core'; import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks'; import { useOpposingColor } from '~/hooks';
import type { ITableRow } from './types'; import type { TTableRow } from './types';
export const TableRow = (props: ITableRow) => { export const TableRow = (props: TTableRow) => {
const { const {
index = 0,
doStripe = false,
highlight = false, highlight = false,
highlightBg = 'primary', highlightBg = 'primary',
doStripe = false,
doHorizontalBorders = false, doHorizontalBorders = false,
index = 0,
...rest ...rest
} = props; } = props;
const { colorMode } = useColorMode();
const alpha = useColorValue('100', '200'); const alpha = useColorValue('100', '200');
const alphaHover = useColorValue('200', '100'); const alphaHover = useColorValue('200', '100');

View File

@@ -1,36 +1,30 @@
import type { BoxProps } from '@chakra-ui/core'; import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
import type { Colors } from '~/types';
export interface IColumn { import type { Colors, TColumn } from '~/types';
Header: string;
accessor: string;
align: 'left' | 'right' | null;
hidden: boolean;
}
export interface ITable { export interface TTable {
columns: IColumn[]; columns: TColumn[];
data: IRoute[]; data: TRoute[];
heading?: ReactNode; heading?: React.ReactNode;
onRowClick?: (row: IRoute) => void;
striped?: boolean; striped?: boolean;
bordersVertical?: boolean; bordersVertical?: boolean;
bordersHorizontal?: boolean; bordersHorizontal?: boolean;
cellRender?: ReactFC; cellRender?: React.ReactNode;
rowHighlightProp?: keyof IRoute; rowHighlightProp?: keyof IRoute;
rowHighlightBg?: keyof Colors; rowHighlightBg?: keyof Colors;
rowHighlightColor?: string;
} }
export interface ITableCell extends BoxProps { export interface TTableCell extends Omit<BoxProps, 'align'> {
bordersVertical: [boolean, number, number]; bordersVertical?: [boolean, number];
align: BoxProps['textAlign']; align?: 'left' | 'right' | 'center';
} }
export interface ITableRow extends BoxProps { export interface TTableRow extends BoxProps {
highlight?: boolean; highlight?: boolean;
highlightBg?: keyof Colors; highlightBg?: keyof Colors;
doStripe?: boolean; doStripe?: boolean;
doHorizontalBorders?: boolean; doHorizontalBorders?: boolean;
index: number; index: number;
} }
export type TTableIconButton = Omit<IconButtonProps, 'aria-label'>;

View File

@@ -1,40 +0,0 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import { Box, css, useColorMode } from '@chakra-ui/core';
const bg = { dark: 'gray.800', light: 'blackAlpha.100' };
const color = { dark: 'white', light: 'black' };
const selectionBg = { dark: 'white', light: 'black' };
const selectionColor = { dark: 'black', light: 'white' };
export const TextOutput = ({ children, ...props }) => {
const { colorMode } = useColorMode();
return (
<Box
fontFamily="mono"
mt={5}
mx={2}
p={3}
border="1px"
borderColor="inherit"
rounded="md"
bg={bg[colorMode]}
color={color[colorMode]}
fontSize="sm"
whiteSpace="pre-wrap"
as="pre"
css={css({
'&::selection': {
backgroundColor: selectionBg[colorMode],
color: selectionColor[colorMode],
},
})}
{...props}>
{children
.split('\\n')
.join('\n')
.replace(/\n\n/g, '\n')}
</Box>
);
};

View File

@@ -1,4 +1,6 @@
export const If = (props: IIf) => { import type { TIf } from './types';
const { condition, render, children, ...rest } = props;
return condition ? (render ? render(rest) : children) : null; export const If = (props: TIf) => {
const { c, render, children, ...rest } = props;
return c ? (render ? render(rest) : children) : null;
}; };

View File

@@ -1 +1 @@
export * from './If'; export * from './if';

View File

@@ -1,5 +1,5 @@
interface IIf { export interface TIf {
condition: boolean; c: boolean;
render?: (rest: any) => JSX.Element; render?: (rest: any) => JSX.Element;
[k: string]: any; [k: string]: any;
} }

View File

@@ -0,0 +1,4 @@
import { chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
export const AnimatedDiv = chakra(motion.div);

View File

@@ -0,0 +1,24 @@
import { MonoField, Active, Weight, Age, Communities, RPKIState, ASPath } from './fields';
import type { TCell } from './types';
export const Cell = (props: TCell) => {
const { data, rawData } = props;
const cellId = data.column.id as keyof TRoute;
const component = {
med: <MonoField v={data.value} />,
age: <Age inSeconds={data.value} />,
prefix: <MonoField v={data.value} />,
next_hop: <MonoField v={data.value} />,
peer_rid: <MonoField v={data.value} />,
source_as: <MonoField v={data.value} />,
active: <Active isActive={data.value} />,
source_rid: <MonoField v={data.value} />,
local_preference: <MonoField v={data.value} />,
communities: <Communities communities={data.value} />,
as_path: <ASPath path={data.value} active={data.row.values.active} />,
rpki_state: <RPKIState state={data.value} active={data.row.values.active} />,
weight: <Weight weight={data.value} winningWeight={rawData.winning_weight} />,
};
return component[cellId] ?? <> </>;
};

View File

@@ -0,0 +1,179 @@
import dynamic from 'next/dynamic';
import {
Icon,
Text,
Popover,
Tooltip,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from '@chakra-ui/react';
import { MdLastPage } from '@meronex/icons/md';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { useConfig, useColorValue } from '~/context';
import { If } from '~/components';
import type {
TAge,
TActive,
TWeight,
TASPath,
TMonoField,
TRPKIState,
TCommunities,
} from './types';
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
const Check = dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaCheckCircle));
const More = dynamic<MeronexIcon>(() => import('@meronex/icons/cg').then(i => i.CgMoreO));
const NotAllowed = dynamic<MeronexIcon>(() =>
import('@meronex/icons/md').then(i => i.MdNotInterested),
);
const Question = dynamic<MeronexIcon>(() =>
import('@meronex/icons/bs').then(i => i.BsQuestionCircleFill),
);
const Warning = dynamic<MeronexIcon>(() => import('@meronex/icons/bi').then(i => i.BisError));
const ChevronRight = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fa').then(i => i.FaChevronRight),
);
export const MonoField = (props: TMonoField) => {
const { v, ...rest } = props;
return (
<Text as="span" fontSize="sm" fontFamily="mono" {...rest}>
{v}
</Text>
);
};
export const Active = (props: TActive) => {
const { isActive } = props;
const color = useColorValue(['gray.500', 'green.500'], ['whiteAlpha.300', 'blackAlpha.500']);
return (
<>
<If c={isActive}>
<Icon color={color[+isActive]} as={Check} />
</If>
<If c={!isActive}>
<Icon color={color[+isActive]} as={NotAllowed} />
</If>
</>
);
};
export const Age = (props: TAge) => {
const { inSeconds, ...rest } = props;
const now = dayjs.utc();
const then = now.subtract(inSeconds, 'second');
return (
<Tooltip hasArrow label={then.toString().replace('GMT', 'UTC')} placement="right">
<Text fontSize="sm" {...rest}>
{now.to(then, true)}
</Text>
</Tooltip>
);
};
export const Weight = (props: TWeight) => {
const { weight, winningWeight, ...rest } = props;
const fixMeText =
winningWeight === 'low' ? 'Lower Weight is Preferred' : 'Higher Weight is Preferred';
return (
<Tooltip hasArrow label={fixMeText} placement="right">
<Text fontSize="sm" fontFamily="mono" {...rest}>
{weight}
</Text>
</Tooltip>
);
};
export const ASPath = (props: TASPath) => {
const { path, active } = props;
const color = useColorValue(
['blackAlpha.500', 'blackAlpha.500'],
['blackAlpha.500', 'whiteAlpha.300'],
);
if (path.length === 0) {
return <Icon as={MdLastPage} />;
}
let paths = [] as JSX.Element[];
path.map((asn, i) => {
const asnStr = String(asn);
i !== 0 && paths.push(<Icon as={ChevronRight} key={`separator-${i}`} color={color[+active]} />);
paths.push(
<Text fontSize="sm" as="span" whiteSpace="pre" fontFamily="mono" key={`as-${asnStr}-${i}`}>
{asnStr}
</Text>,
);
});
return <>{paths}</>;
};
export const Communities = (props: TCommunities) => {
const { communities } = props;
const color = useColorValue('black', 'white');
return (
<>
<If c={communities.length === 0}>
<Tooltip placement="right" hasArrow label="No Communities">
<Icon as={Question} />
</Tooltip>
</If>
<If c={communities.length !== 0}>
<Popover trigger="hover" placement="right">
<PopoverTrigger>
<Icon as={More} />
</PopoverTrigger>
<PopoverContent
p={4}
width="unset"
color={color}
textAlign="left"
fontFamily="mono"
fontWeight="normal"
whiteSpace="pre-wrap">
<PopoverArrow />
{communities.join('\n')}
</PopoverContent>
</Popover>
</If>
</>
);
};
export const RPKIState = (props: TRPKIState) => {
const { state, active } = props;
const { web } = useConfig();
const color = useColorValue(
[
['red.400', 'green.500', 'yellow.400', 'gray.500'],
['red.500', 'green.500', 'yellow.500', 'gray.600'],
],
[
['red.300', 'green.300', 'yellow.300', 'gray.300'],
['red.500', 'green.600', 'yellow.500', 'gray.800'],
],
);
const icon = [NotAllowed, Check, Warning, Question];
const text = [
web.text.rpki_invalid,
web.text.rpki_valid,
web.text.rpki_unknown,
web.text.rpki_unverified,
];
return (
<Tooltip hasArrow placement="right" label={text[state] ?? text[3]}>
<Icon icon={icon[state]} color={color[+active][state]} />
</Tooltip>
);
};

View File

@@ -0,0 +1 @@
export * from './table';

View File

@@ -0,0 +1,46 @@
import { Flex } from '@chakra-ui/react';
import { useConfig } from '~/context';
import { Table } from '~/components';
import { Cell } from './cell';
import type { CellProps } from 'react-table';
import type { TColumn, TParsedDataField } from '~/types';
import type { TBGPTable } from './types';
function makeColumns(fields: TParsedDataField[]): TColumn[] {
return fields.map(pair => {
const [header, accessor, align] = pair;
let columnConfig = {
align,
accessor,
hidden: false,
Header: header,
} as TColumn;
if (align === null) {
columnConfig.hidden = true;
}
return columnConfig;
});
}
export const BGPTable = (props: TBGPTable) => {
const { children: data, ...rest } = props;
const { parsed_data_fields } = useConfig();
const columns = makeColumns(parsed_data_fields);
return (
<Flex my={8} maxW={['100%', '100%', '100%', '100%']} w="100%" {...rest}>
<Table
columns={columns}
data={data.routes}
rowHighlightProp="active"
cellRender={(d: CellProps<TRouteField>) => <Cell data={d} rawData={data} />}
bordersHorizontal
rowHighlightBg="green"
/>
</Flex>
);
};

View File

@@ -0,0 +1,42 @@
import type { FlexProps, TextProps } from '@chakra-ui/react';
import type { CellProps } from 'react-table';
export interface TActive {
isActive: boolean;
}
export interface TMonoField extends TextProps {
v: React.ReactNode;
}
export interface TAge extends TextProps {
inSeconds: number;
}
export interface TWeight extends TextProps {
weight: number;
winningWeight: 'low' | 'high';
}
export interface TASPath {
path: number[];
active: boolean;
}
export interface TCommunities {
communities: string[];
}
export interface TRPKIState {
state: 0 | 1 | 2 | 3;
active: boolean;
}
export interface TCell {
data: CellProps<TRouteField>;
rawData: TStructuredResponse;
}
export interface TBGPTable extends Omit<FlexProps, 'children'> {
children: TStructuredResponse;
}

View File

@@ -0,0 +1,25 @@
import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { BoxProps } from '@chakra-ui/react';
export const CodeBlock = (props: BoxProps) => {
const bg = useColorValue('blackAlpha.100', 'gray.800');
const color = useColorValue('black', 'white');
return (
<Box
p={3}
mt={5}
bg={bg}
as="pre"
border="1px"
rounded="md"
color={color}
fontSize="sm"
fontFamily="mono"
borderColor="inherit"
whiteSpace="pre-wrap"
{...props}
/>
);
};

View File

@@ -0,0 +1,33 @@
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip, useClipboard } from '@chakra-ui/react';
import { If } from '~/components';
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 type { TCopyButton } from './types';
export const CopyButton = (props: TCopyButton) => {
const { copyValue, ...rest } = props;
const { onCopy, hasCopied } = useClipboard(copyValue);
return (
<Tooltip hasArrow label="Copy Output" placement="top">
<Button
as="a"
mx={1}
size="sm"
variant="ghost"
onClick={onCopy}
zIndex="dropdown"
colorScheme="secondary"
{...rest}>
<If c={hasCopied}>
<Icon as={Check} boxSize="16px" />
</If>
<If c={!hasCopied}>
<Icon as={Copy} boxSize="16px" />
</If>
</Button>
</Tooltip>
);
};

View File

@@ -0,0 +1 @@
export * from './copyButton';

View File

@@ -0,0 +1,5 @@
import type { ButtonProps } from '@chakra-ui/react';
export interface TCopyButton extends ButtonProps {
copyValue: string;
}

View File

@@ -3,6 +3,8 @@ import ReactCountdown, { zeroPad } from 'react-countdown';
import { If } from '~/components'; import { If } from '~/components';
import { useColorValue } from '~/context'; import { useColorValue } from '~/context';
import type { ICountdown, IRenderer } from './types';
const Renderer = (props: IRenderer) => { const Renderer = (props: IRenderer) => {
const { hours, minutes, seconds, completed, text } = props; const { hours, minutes, seconds, completed, text } = props;
let time = [zeroPad(seconds)]; let time = [zeroPad(seconds)];

View File

@@ -0,0 +1 @@
export * from './countdown';

View File

@@ -0,0 +1,10 @@
import type { CountdownRenderProps } from 'react-countdown';
export interface IRenderer extends CountdownRenderProps {
text: string;
}
export interface ICountdown {
timeout: number;
text: string;
}

View File

@@ -0,0 +1,165 @@
import { Flex } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { useColorValue, useConfig, useGlobalState, useBreakpointValue } from '~/context';
import { AnimatedDiv, Title, ResetButton, ColorModeToggle } from '~/components';
import { useBooleanValue } from '~/hooks';
import type { ResponsiveValue } from '@chakra-ui/react';
import type { THeader, TTitleMode } from './types';
const headerTransition = {
type: 'spring',
ease: 'anticipate',
damping: 15,
stiffness: 100,
};
function getWidth(mode: TTitleMode): ResponsiveValue<string> {
let width = '100%' as ResponsiveValue<string>;
switch (mode) {
case 'text_only':
width = '100%';
break;
case 'logo_only':
width = { base: '90%', lg: '50%' };
break;
case 'logo_subtitle':
width = { base: '90%', lg: '50%' };
break;
case 'all':
width = { base: '90%', lg: '50%' };
break;
}
return width;
}
export const Header = (props: THeader) => {
const { resetForm, ...rest } = props;
const bg = useColorValue('white', 'black');
const { web } = useConfig();
const { isSubmitting } = useGlobalState();
const mlResetButton = useBooleanValue(isSubmitting.value, { base: 0, md: 2 }, undefined);
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });
const titleVariant = useBreakpointValue({
base: {
fullSize: { scale: 1, marginLeft: 0 },
smallLogo: { marginLeft: 'auto' },
smallText: { marginLeft: 'auto' },
},
md: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
lg: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
xl: {
fullSize: { scale: 1 },
smallLogo: { scale: 0.5 },
smallText: { scale: 0.8 },
},
});
const titleJustify = useBooleanValue(
isSubmitting.value,
{ base: 'flex-end', md: 'center' },
{ base: 'flex-start', md: 'center' },
);
const resetButton = (
<AnimatePresence key="resetButton">
<AnimatedDiv
layoutTransition={headerTransition}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, width: 'unset' }}
exit={{ opacity: 0, x: -50 }}
alignItems="center"
mb={{ md: 'auto' }}
ml={mlResetButton}
display={isSubmitting ? 'flex' : 'none'}>
<motion.div>
<ResetButton onClick={resetForm} />
</motion.div>
</AnimatedDiv>
</AnimatePresence>
);
const title = (
<AnimatedDiv
key="title"
px={1}
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
positionTransition={headerTransition}
initial={{ scale: 0.5 }}
animate={
isSubmitting && web.text.title_mode === 'text_only'
? 'smallText'
: isSubmitting && web.text.title_mode !== 'text_only'
? 'smallLogo'
: 'fullSize'
}
variants={titleVariant}
justifyContent={titleJustify}
mt={[null, isSubmitting ? null : 'auto']}
maxW={getWidth(web.text.title_mode)}
flex="1 0 0"
minH={titleHeight}>
<Title onClick={resetForm} />
</AnimatedDiv>
);
const colorModeToggle = (
<AnimatedDiv
layoutTransition={headerTransition}
key="colorModeToggle"
alignItems="center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
mb={[null, 'auto']}
mr={isSubmitting ? undefined : 2}>
<ColorModeToggle />
</AnimatedDiv>
);
const layout = useBooleanValue(
isSubmitting.value,
{
sm: [resetButton, colorModeToggle, title],
md: [resetButton, title, colorModeToggle],
lg: [resetButton, title, colorModeToggle],
xl: [resetButton, title, colorModeToggle],
},
{
sm: [title, resetButton, colorModeToggle],
md: [resetButton, title, colorModeToggle],
lg: [resetButton, title, colorModeToggle],
xl: [resetButton, title, colorModeToggle],
},
);
return (
<Flex
px={2}
zIndex="4"
as="header"
width="full"
flex="0 1 auto"
bg={bg}
color="gray.500"
{...rest}>
<Flex
w="100%"
mx="auto"
pt={6}
justify="space-between"
flex="1 0 auto"
alignItems={isSubmitting ? 'center' : 'flex-start'}>
{layout}
</Flex>
</Flex>
);
};

View File

@@ -0,0 +1 @@
export * from './header';

View File

@@ -0,0 +1,9 @@
import { FlexProps } from '@chakra-ui/react';
import { IConfig } from '~/types';
export interface THeader extends FlexProps {
resetForm(): void;
}
export type TTitleMode = IConfig['web']['text']['title_mode'];

View File

@@ -1,37 +1,35 @@
export * from './BGPTable'; export * from './animated';
export * from './CacheTimeout'; export * from './bgpTable';
export * from './Card'; export * from './card';
export * from './ChakraSelect'; export * from './ChakraSelect';
export * from './CodeBlock'; export * from './codeBlock';
export * from './ColorModeToggle'; export * from './ColorModeToggle';
export * from './CommunitySelect'; export * from './CommunitySelect';
export * from './CopyButton'; export * from './copyButton';
export * from './countdown';
export * from './Debugger'; export * from './Debugger';
export * from './Footer'; export * from './footer';
export * from './FormField'; export * from './FormField';
export * from './Greeting'; export * from './Greeting';
export * from './Header'; export * from './header';
export * from './HelpModal'; export * from './HelpModal';
export * from './HyperglassForm'; export * from './HyperglassForm';
export * from './Label'; export * from './label';
export * from './Layout'; export * from './Layout';
export * from './Loading'; export * from './loading';
export * from './LookingGlass'; export * from './LookingGlass';
export * from './Markdown'; export * from './markdown';
export * from './Meta'; export * from './meta';
export * from './QueryLocation'; export * from './QueryLocation';
export * from './QueryTarget'; export * from './QueryTarget';
export * from './QueryType'; export * from './QueryType';
export * from './QueryVrf'; export * from './QueryVrf';
export * from './RequeryButton'; export * from './RequeryButton';
export * from './ResetButton'; export * from './resetButton';
export * from './ResolvedTarget'; export * from './ResolvedTarget';
export * from './Result'; export * from './results';
export * from './ResultHeader'; export * from './submitButton';
export * from './Results'; export * from './table';
export * from './SubmitButton'; export * from './textOutput';
export * from './Table';
export * from './TextOutput';
export * from './Title'; export * from './Title';
export * from './Util'; export * from './util';
export * from './withAnimation';

View File

@@ -0,0 +1 @@
export * from './label';

View File

@@ -0,0 +1,61 @@
import { forwardRef } from 'react';
import { Flex } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TLabel } from './types';
export const Label = forwardRef<HTMLDivElement, TLabel>((props, ref) => {
const { value, label, labelColor, bg = 'primary.600', valueColor, ...rest } = props;
const valueColorAuto = useOpposingColor(bg);
const defaultLabelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
return (
<Flex
my={2}
ref={ref}
flexWrap="nowrap"
alignItems="center"
mx={{ base: 1, md: 2 }}
justifyContent="flex-start"
{...rest}>
<Flex
mb={2}
mr={0}
bg={bg}
lineHeight="1.5"
fontWeight="bold"
whiteSpace="nowrap"
display="inline-flex"
px={{ base: 1, md: 3 }}
justifyContent="center"
borderTopLeftRadius={4}
borderTopRightRadius={0}
borderBottomLeftRadius={4}
borderBottomRightRadius={0}
fontSize={{ base: 'xs', md: 'sm' }}
color={valueColor ?? valueColorAuto}>
{value}
</Flex>
<Flex
px={3}
mb={2}
ml={0}
mr={0}
lineHeight="1.5"
whiteSpace="nowrap"
display="inline-flex"
justifyContent="center"
borderTopLeftRadius={0}
borderTopRightRadius={4}
borderBottomLeftRadius={0}
borderBottomRightRadius={4}
fontSize={{ base: 'xs', md: 'sm' }}
color={labelColor ?? defaultLabelColor}
boxShadow={`inset 0px 0px 0px 1px ${bg}`}>
{label}
</Flex>
</Flex>
);
});

View File

@@ -0,0 +1,9 @@
import { FlexProps } from '@chakra-ui/react';
export interface TLabel extends FlexProps {
value: string;
label: string;
bg: string;
valueColor?: string;
labelColor?: string;
}

View File

@@ -0,0 +1,26 @@
import { Flex, Spinner } from '@chakra-ui/react';
import { useColorValue } from '~/context';
export const Loading: React.FC = () => {
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
return (
<Flex flexDirection="column" minHeight="100vh" w="100%" bg={bg} color={color}>
<Flex
px={2}
py={0}
w="100%"
as="main"
flexGrow={1}
flexShrink={1}
flexBasis="auto"
textAlign="center"
alignItems="center"
justifyContent="start"
flexDirection="column"
mt={{ base: '50%', xl: '25%' }}>
<Spinner color="primary.500" w="6rem" h="6rem" />
</Flex>
</Flex>
);
};

View File

@@ -0,0 +1,59 @@
import { useEffect, useMemo, useState } from 'react';
import Head from 'next/head';
import { useTheme } from '@chakra-ui/react';
import { useConfig } from '~/context';
import { googleFontUrl } from '~/util';
export const Meta = () => {
const config = useConfig();
const { fonts } = useTheme();
const [location, setLocation] = useState('/');
const {
site_title: title = 'hyperglass',
site_description: description = 'Network Looking Glass',
site_keywords: keywords = [
'hyperglass',
'looking glass',
'lg',
'peer',
'peering',
'ipv4',
'ipv6',
'transit',
'community',
'communities',
'bgp',
'routing',
'network',
'isp',
],
} = useConfig();
const siteName = `${title} - ${description}`;
const primaryFont = useMemo(() => googleFontUrl(fonts.body), []);
const monoFont = useMemo(() => googleFontUrl(fonts.mono), []);
useEffect(() => {
if (typeof window !== 'undefined' && location === '/') {
setLocation(window.location.href);
}
}, []);
return (
<Head>
<title>{title}</title>
<meta name="language" content="en" />
<meta name="url" content={location} />
<meta name="og:title" content={title} />
<meta name="og:url" content={location} />
<link href={monoFont} rel="stylesheet" />
<link href={primaryFont} rel="stylesheet" />
<meta name="description" content={description} />
<meta property="og:image:alt" content={siteName} />
<meta name="og:description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
<meta name="hg-version" content={config.hyperglass_version} />
</Head>
);
};

View File

@@ -0,0 +1,25 @@
import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon } from '@chakra-ui/react';
import { useGlobalState } from '~/context';
import type { ButtonProps } from '@chakra-ui/react';
const ChevronLeft = dynamic<MeronexIcon>(() =>
import('@meronex/icons/fi').then(i => i.FiChevronLeft),
);
export const ResetButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { isSubmitting } = useGlobalState();
return (
<Button
ref={ref}
color="current"
variant="ghost"
aria-label="Reset Form"
opacity={isSubmitting.value ? 1 : 0}
{...props}>
<Icon as={ChevronLeft} boxSize={24} />
</Button>
);
});

View File

@@ -0,0 +1,21 @@
import { Text } from '@chakra-ui/react';
import strReplace from 'react-string-replace';
import type { TFormattedError } from './types';
export const FormattedError = (props: TFormattedError) => {
const { keywords, message } = props;
const patternStr = keywords.map(kw => `(${kw})`).join('|');
const pattern = new RegExp(patternStr, 'gi');
let errorFmt;
try {
errorFmt = strReplace(message, pattern, match => (
<Text key={match} as="strong">
{match}
</Text>
));
} catch (err) {
errorFmt = <Text as="span">{message}</Text>;
}
return <Text as="span">{keywords.length !== 0 ? errorFmt : message}</Text>;
};

View File

@@ -0,0 +1,164 @@
import { useState } from 'react';
import { Accordion, Box, Stack, useToken } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { AnimatedDiv, Label } from '~/components';
import { useConfig, useBreakpointValue } from '~/context';
import { Result } from './individual';
import type { TResults } from './types';
export const Results = (props: TResults) => {
const { queryLocation, queryType, queryVrf, queryTarget, setSubmitting, ...rest } = props;
const { request_timeout, devices, queries, vrfs, web } = useConfig();
const targetBg = useToken('colors', 'teal.600');
const queryBg = useToken('colors', 'cyan.500');
const vrfBg = useToken('colors', 'blue.500');
const animateLeft = useBreakpointValue({
base: { opacity: 1, x: 0 },
md: { opacity: 1, x: 0 },
lg: { opacity: 1, x: 0 },
xl: { opacity: 1, x: 0 },
});
const animateCenter = useBreakpointValue({
base: { opacity: 1 },
md: { opacity: 1 },
lg: { opacity: 1 },
xl: { opacity: 1 },
});
const animateRight = useBreakpointValue({
base: { opacity: 1, x: 0 },
md: { opacity: 1, x: 0 },
lg: { opacity: 1, x: 0 },
xl: { opacity: 1, x: 0 },
});
const initialLeft = useBreakpointValue({
base: { opacity: 0, x: -100 },
md: { opacity: 0, x: -100 },
lg: { opacity: 0, x: -100 },
xl: { opacity: 0, x: -100 },
});
const initialCenter = useBreakpointValue({
base: { opacity: 0 },
md: { opacity: 0 },
lg: { opacity: 0 },
xl: { opacity: 0 },
});
const initialRight = useBreakpointValue({
base: { opacity: 0, x: 100 },
md: { opacity: 0, x: 100 },
lg: { opacity: 0, x: 100 },
xl: { opacity: 0, x: 100 },
});
const [resultsComplete, setComplete] = useState<number | null>(null);
const matchedVrf =
vrfs.filter(v => v.id === queryVrf)[0] ?? vrfs.filter(v => v.id === 'default')[0];
return (
<>
<Box
p={0}
my={4}
w="100%"
mx="auto"
textAlign="left"
maxW={{ base: '100%', lg: '75%', xl: '50%' }}
{...rest}>
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
<AnimatePresence>
{queryLocation && (
<>
<motion.div
initial={initialLeft}
animate={animateLeft}
exit={{ opacity: 0, x: -100 }}
transition={{ duration: 0.3, delay: 0.3 }}>
<Label
bg={queryBg}
label={web.text.query_type}
fontSize={{ base: 'xs', md: 'sm' }}
value={queries[queryType].display_name}
/>
</motion.div>
<motion.div
initial={initialCenter}
animate={animateCenter}
exit={{ opacity: 0, scale: 0.5 }}
transition={{ duration: 0.3, delay: 0.3 }}>
<Label
bg={targetBg}
value={queryTarget}
label={web.text.query_target}
fontSize={{ base: 'xs', md: 'sm' }}
/>
</motion.div>
<motion.div
initial={initialRight}
animate={animateRight}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.3, delay: 0.3 }}>
<Label
bg={vrfBg}
label={web.text.query_vrf}
value={matchedVrf.display_name}
fontSize={{ base: 'xs', md: 'sm' }}
/>
</motion.div>
</>
)}
</AnimatePresence>
</Stack>
</Box>
<AnimatedDiv
p={0}
my={4}
w="100%"
mx="auto"
rounded="lg"
textAlign="left"
borderWidth="1px"
overflow="hidden"
initial={{ opacity: 1 }}
exit={{ opacity: 0, y: 300 }}
transition={{ duration: 0.3 }}
animate={{ opacity: 1, y: 0 }}
maxW={{ base: '100%', md: '75%' }}>
<Accordion allowMultiple>
<AnimatePresence>
{queryLocation &&
queryLocation.map((loc, i) => {
const device = devices.filter(d => d.name === loc)[0];
return (
<motion.div
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: 300 }}
transition={{ duration: 0.3, delay: i * 0.3 }}
exit={{ opacity: 0, y: 300 }}>
<Result
key={loc}
index={i}
device={device}
queryLocation={loc}
queryVrf={queryVrf}
queryType={queryType}
queryTarget={queryTarget}
setComplete={setComplete}
timeout={request_timeout * 1000}
resultsComplete={resultsComplete}
/>
</motion.div>
);
})}
</AnimatePresence>
</Accordion>
</AnimatedDiv>
</>
);
};

View File

@@ -0,0 +1,48 @@
import dynamic from 'next/dynamic';
import { forwardRef, useMemo } from 'react';
import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip, useColorMode } from '@chakra-ui/react';
import { useConfig, useColorValue } from '~/context';
import { useStrf } from '~/hooks';
import type { TResultHeader } from './types';
const Check = dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaCheckCircle));
const Warning = dynamic<MeronexIcon>(() => import('@meronex/icons/bi').then(i => i.BisError));
const runtimeText = (runtime: number, text: string): string => {
let unit = 'seconds';
if (runtime === 1) {
unit = 'second';
}
return `${text} ${unit}`;
};
export const ResultHeader = forwardRef<HTMLDivElement, TResultHeader>((props, ref) => {
const { title, loading, error, errorMsg, errorLevel, runtime } = props;
const status = useColorValue('primary.500', 'primary.300');
const warning = useColorValue(`${errorLevel}.500`, `${errorLevel}.300`);
const defaultStatus = useColorValue('success.500', 'success.300');
const { web } = useConfig();
const text = useStrf(web.text.complete_time, { seconds: runtime }, [runtime]);
const label = useMemo(() => runtimeText(runtime, text), [runtime]);
return (
<Stack ref={ref} isInline alignItems="center" w="100%">
{loading ? (
<Spinner size="sm" mr={4} color={status} />
) : error ? (
<Tooltip hasArrow label={errorMsg} placement="top">
<Icon as={Warning} color={warning} mr={4} boxSize={6} />
</Tooltip>
) : (
<Tooltip hasArrow label={label} placement="top">
<Icon as={Check} color={defaultStatus} mr={4} boxSize={6} />
</Tooltip>
)}
<Text fontSize="lg">{title}</Text>
<AccordionIcon ml="auto" />
</Stack>
);
});

View File

@@ -0,0 +1 @@
export * from './group';

View File

@@ -0,0 +1,236 @@
import { forwardRef, useEffect, useMemo, useState } from 'react';
import {
Box,
Flex,
Alert,
Tooltip,
ButtonGroup,
AccordionItem,
AccordionPanel,
AccordionButton,
} from '@chakra-ui/react';
import { BsLightningFill } from '@meronex/icons/bs';
import useAxios from 'axios-hooks';
import { startCase } from 'lodash';
import { BGPTable, Countdown, CopyButton, RequeryButton, TextOutput, If } from '~/components';
import { useColorValue, useConfig, useMobile } from '~/context';
import { useStrf, useTableToString } from '~/hooks';
import { FormattedError } from './error';
import { ResultHeader } from './header';
import type { TAccordionHeaderWrapper, TResult } from './types';
type TErrorLevels = 'success' | 'warning' | 'error';
const AccordionHeaderWrapper = (props: TAccordionHeaderWrapper) => {
const { hoverBg, ...rest } = props;
return (
<Flex
justify="space-between"
_hover={{ bg: hoverBg }}
_focus={{ boxShadow: 'outline' }}
{...rest}
/>
);
};
export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
const {
index,
device,
timeout,
queryVrf,
queryType,
queryTarget,
setComplete,
queryLocation,
resultsComplete,
} = props;
const { web, cache, messages } = useConfig();
const isMobile = useMobile();
const color = useColorValue('black', 'white');
const scrollbar = useColorValue('blackAlpha.300', 'whiteAlpha.300');
const scrollbarHover = useColorValue('blackAlpha.400', 'whiteAlpha.400');
const scrollbarBg = useColorValue('blackAlpha.50', 'whiteAlpha.50');
let [{ data, loading, error }, refetch] = useAxios(
{
url: '/api/query/',
method: 'post',
data: {
query_vrf: queryVrf,
query_type: queryType,
query_target: queryTarget,
query_location: queryLocation,
},
timeout,
},
{ useCache: false },
);
const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]);
const [isOpen, setOpen] = useState(false);
const [hasOverride, setOverride] = useState(false);
const handleToggle = () => {
setOpen(!isOpen);
setOverride(true);
};
const errorKw = (error && error.response?.data?.keywords) || [];
let errorMsg;
if (error && error.response?.data?.output) {
errorMsg = error.response.data.output;
} else if (error && error.message.startsWith('timeout')) {
errorMsg = messages.request_timeout;
} else if (error?.response?.statusText) {
errorMsg = startCase(error.response.statusText);
} else if (error && error.message) {
errorMsg = startCase(error.message);
} else {
errorMsg = messages.general;
}
error && console.error(error);
const getErrorLevel = (): TErrorLevels => {
const statusMap = {
success: 'success',
warning: 'warning',
error: 'warning',
danger: 'error',
} as { [k in TResponseLevel]: 'success' | 'warning' | 'error' };
let e: TErrorLevels = 'error';
if (error?.response?.data?.level) {
const idx = error.response.data.level as TResponseLevel;
e = statusMap[idx];
}
return e;
};
const errorLevel = useMemo(() => getErrorLevel(), [error]);
const tableComponent = useMemo(() => typeof queryType.match(/^bgp_\w+$/) !== null, [queryType]);
let copyValue = data?.output;
const formatData = useTableToString(queryTarget, data, [data.format]);
if (data?.format === 'application/json') {
copyValue = formatData();
}
if (error) {
copyValue = errorMsg;
}
useEffect(() => {
!loading && resultsComplete === null && setComplete(index);
}, [loading, resultsComplete]);
useEffect(() => {
resultsComplete === index && !hasOverride && setOpen(true);
}, [resultsComplete, index]);
return (
<AccordionItem
ref={ref}
isOpen={isOpen}
isDisabled={loading}
css={{
'&:last-of-type': { borderBottom: 'none' },
'&:first-of-type': { borderTop: 'none' },
}}>
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
<AccordionButton
py={2}
w="unset"
_hover={{}}
_focus={{}}
flex="1 0 auto"
onClick={handleToggle}>
<ResultHeader
error={error}
loading={loading}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime}
title={device.display_name}
/>
</AccordionButton>
<ButtonGroup px={[1, 1, 3, 3]} py={2}>
<CopyButton copyValue={copyValue} isDisabled={loading} />
<RequeryButton requery={refetch} variant="ghost" isDisabled={loading} />
</ButtonGroup>
</AccordionHeaderWrapper>
<AccordionPanel
pb={4}
overflowX="auto"
css={{
WebkitOverflowScrolling: 'touch',
'&::-webkit-scrollbar': { height: '5px' },
'&::-webkit-scrollbar-track': {
backgroundColor: scrollbarBg,
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: scrollbar,
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: scrollbarHover,
},
'-ms-overflow-style': { display: 'none' },
}}>
<Flex direction="column" flexWrap="wrap">
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}>
<If c={!error && data}>
<If c={tableComponent}>
<BGPTable>{data?.output}</BGPTable>
</If>
<If c={!tableComponent}>
<TextOutput>{data?.output}</TextOutput>
</If>
</If>
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
</Alert>
)}
</Flex>
</Flex>
<Flex direction="row" flexWrap="wrap">
<Flex
px={3}
mt={2}
justifyContent={['flex-start', 'flex-start', 'flex-end', 'flex-end']}
flex="1 0 auto">
<If c={cache.show_text && data && !error}>
<If c={!isMobile}>
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
</If>
<Tooltip
display={!data?.cached ? 'none' : undefined}
hasArrow
label={cacheLabel}
placement="top">
<Box ml={1} display={data?.cached ? 'block' : 'none'}>
<BsLightningFill color={color} />
</Box>
</Tooltip>
<If c={isMobile}>
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
</If>
</If>
</Flex>
</Flex>
</AccordionPanel>
</AccordionItem>
);
});

View File

@@ -0,0 +1,40 @@
import type { BoxProps, FlexProps } from '@chakra-ui/react';
import type { TDevice, TQueryTypes } from '~/types';
export interface TResultHeader {
title: string;
loading: boolean;
error?: Error;
errorMsg: string;
errorLevel: 'success' | 'warning' | 'error';
runtime: number;
}
export interface TFormattedError {
keywords: string[];
message: string;
}
export interface TAccordionHeaderWrapper extends FlexProps {
hoverBg: FlexProps['bg'];
}
export interface TResult {
index: number;
device: TDevice;
timeout: number;
queryVrf: string;
queryType: TQueryTypes;
queryTarget: string;
setComplete(v: number | null): void;
queryLocation: string;
resultsComplete: number | null;
}
export interface TResults extends BoxProps {
setSubmitting(v: boolean): boolean;
queryType: TQueryTypes;
queryLocation: string[];
queryTarget: string;
queryVrf: string;
}

View File

@@ -0,0 +1 @@
export * from './submitButton';

View File

@@ -0,0 +1,107 @@
import { forwardRef } from 'react';
import { Box, Spinner } from '@chakra-ui/react';
import { FiSearch } from '@meronex/icons/fi';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { TSubmitButton, TButtonSizeMap } from './types';
const btnSizeMap = {
lg: {
height: 12,
minWidth: 12,
fontSize: 'lg',
px: 6,
},
md: {
height: 10,
minWidth: 10,
fontSize: 'md',
px: 4,
},
sm: {
height: 8,
minWidth: 8,
fontSize: 'sm',
px: 3,
},
xs: {
height: 6,
minWidth: 6,
fontSize: 'xs',
px: 2,
},
} as TButtonSizeMap;
export const SubmitButton = forwardRef<HTMLDivElement, TSubmitButton>((props, ref) => {
const {
isLoading = false,
isDisabled = false,
isActive = false,
isFullWidth = false,
size = 'lg',
loadingText,
children,
...rest
} = props;
const _isDisabled = isDisabled || isLoading;
const bg = useColorValue('primary.500', 'primary.300');
const bgActive = useColorValue('primary.600', 'primary.400');
const bgHover = useColorValue('primary.400', 'primary.200');
const color = useOpposingColor(bg);
const colorActive = useOpposingColor(bgActive);
const colorHover = useOpposingColor(bgHover);
const btnSize = btnSizeMap[size];
return (
<Box
bg={bg}
ref={ref}
as="button"
color={color}
type="submit"
outline="none"
lineHeight="1.2"
appearance="none"
userSelect="none"
borderRadius="md"
alignItems="center"
position="relative"
whiteSpace="nowrap"
display="inline-flex"
fontWeight="semibold"
disabled={_isDisabled}
transition="all 250ms"
verticalAlign="middle"
justifyContent="center"
aria-label="Submit Query"
aria-disabled={_isDisabled}
_focus={{ boxShadow: 'outline' }}
width={isFullWidth ? 'full' : undefined}
data-active={isActive ? 'true' : undefined}
_hover={{ bg: bgHover, color: colorHover }}
_active={{ bg: bgActive, color: colorActive }}
{...btnSize}
{...rest}>
{isLoading ? (
<Spinner
position={loadingText ? 'relative' : 'absolute'}
mr={loadingText ? 2 : 0}
color="currentColor"
size="1em"
/>
) : (
<FiSearch color={color} />
)}
{isLoading
? loadingText || (
<Box as="span" opacity="0">
{children}
</Box>
)
: children}
</Box>
);
});

View File

@@ -0,0 +1,17 @@
import { BoxProps } from '@chakra-ui/react';
export type TButtonSizeMap = {
xs: BoxProps;
sm: BoxProps;
md: BoxProps;
lg: BoxProps;
};
export interface TSubmitButton extends BoxProps {
isLoading: boolean;
isDisabled: boolean;
isActive: boolean;
isFullWidth: boolean;
size: keyof TButtonSizeMap;
loadingText: string;
}

View File

@@ -0,0 +1 @@
export * from './textOutput';

View File

@@ -0,0 +1,38 @@
import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { TTextOutput } from './types';
export const TextOutput = (props: TTextOutput) => {
const { children, ...rest } = props;
const bg = useColorValue('blackAlpha.100', 'gray.800');
const color = useColorValue('black', 'white');
const selectionBg = useColorValue('black', 'white');
const selectionColor = useColorValue('white', 'black');
return (
<Box
p={3}
mt={5}
mx={2}
bg={bg}
as="pre"
border="1px"
rounded="md"
color={color}
fontSize="sm"
fontFamily="mono"
borderColor="inherit"
whiteSpace="pre-wrap"
css={{
'&::selection': {
backgroundColor: selectionBg,
color: selectionColor,
},
}}
{...rest}>
{children.split('\\n').join('\n').replace(/\n\n/g, '\n')}
</Box>
);
};

View File

@@ -0,0 +1,5 @@
import type { BoxProps } from '@chakra-ui/react';
export interface TTextOutput extends Omit<BoxProps, 'children'> {
children: string;
}

View File

@@ -1,5 +0,0 @@
import { motion } from 'framer-motion';
export function withAnimation<P>(Component: React.FunctionComponent) {
return motion.custom<Omit<P, 'transition'>>(Component);
}

View File

@@ -1,5 +1,5 @@
import { createState, useState } from '@hookstate/core'; import { createState, useState } from '@hookstate/core';
import type { IGlobalState } from './types'; import type { TGlobalState } from './types';
// const StateContext = createContext(null); // const StateContext = createContext(null);
@@ -26,7 +26,7 @@ import type { IGlobalState } from './types';
// export const useHyperglassState = () => useContext(StateContext); // export const useHyperglassState = () => useContext(StateContext);
export const globalState = createState<IGlobalState>({ export const globalState = createState<TGlobalState>({
isSubmitting: false, isSubmitting: false,
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' }, formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
}); });

View File

@@ -1,13 +1,19 @@
import { createContext, useContext, useMemo } from 'react'; import { createContext, useContext, useMemo } from 'react';
import { ChakraProvider, useTheme as useChakraTheme } from '@chakra-ui/core'; import {
useToken,
ChakraProvider,
useColorModeValue,
useBreakpointValue,
useTheme as useChakraTheme,
} from '@chakra-ui/react';
import { makeTheme, defaultTheme } from '~/util'; import { makeTheme, defaultTheme } from '~/util';
import type { IConfig, ITheme } from '~/types'; import type { IConfig, ITheme } from '~/types';
import type { IHyperglassProvider } from './types'; import type { THyperglassProvider } from './types';
const HyperglassContext = createContext<IConfig>(Object()); const HyperglassContext = createContext<IConfig>(Object());
export const HyperglassProvider = (props: IHyperglassProvider) => { export const HyperglassProvider = (props: THyperglassProvider) => {
const { config, children } = props; const { config, children } = props;
const value = useMemo(() => config, []); const value = useMemo(() => config, []);
const userTheme = value && makeTheme(value.web.theme); const userTheme = value && makeTheme(value.web.theme);
@@ -19,6 +25,13 @@ export const HyperglassProvider = (props: IHyperglassProvider) => {
); );
}; };
export const useConfig = () => useContext(HyperglassContext); export const useConfig = (): IConfig => useContext(HyperglassContext);
export const useTheme = (): ITheme => useChakraTheme(); export const useTheme = (): ITheme => useChakraTheme();
export { useColorModeValue as useColorValue } from '@chakra-ui/core';
export const useMobile = (): boolean =>
useBreakpointValue({ base: true, md: true, lg: false, xl: false }) ?? true;
export const useColorToken = (light: string, dark: string): ValueOf<ITheme['colors']> =>
useColorModeValue(useToken('colors', light), useToken('colors', dark));
export { useColorModeValue as useColorValue, useBreakpointValue } from '@chakra-ui/react';

View File

@@ -1,12 +1,11 @@
import type { ReactNode } from 'react';
import type { IConfig, IFormData } from '~/types'; import type { IConfig, IFormData } from '~/types';
export interface IHyperglassProvider { export interface THyperglassProvider {
config: IConfig; config: IConfig;
children: ReactNode; children: React.ReactNode;
} }
export interface IGlobalState { export interface TGlobalState {
isSubmitting: boolean; isSubmitting: boolean;
formData: IFormData; formData: IFormData;
} }

View File

@@ -1,29 +0,0 @@
import type { MotionProps } from 'framer-motion';
declare global {
import * as React from 'react';
interface IRoute {
prefix: string;
active: boolean;
age: number;
weight: number;
med: number;
local_preference: number;
as_path: number[];
communities: string[];
next_hop: string;
source_as: number;
source_rid: string;
peer_rid: string;
rpki_state: 0 | 1 | 2 | 3;
}
type ReactRef<T = HTMLElement> = MutableRefObject<T>;
type Dict<T = string> = Record<string, T>;
type Animated<T> = Omit<T, keyof MotionProps> &
Omit<MotionProps, keyof T> & { transition?: MotionProps['transition'] };
type ReactNode = React.ReactNode;
type ReactFC = React.FunctionComponent;
}

View File

@@ -1,4 +0,0 @@
interface IOpposingOptions {
light?: string;
dark?: string;
}

View File

@@ -1,2 +1,5 @@
export * from './useSessionStorage'; export * from './useStrf';
export * from './useBooleanValue';
export * from './useOpposingColor'; export * from './useOpposingColor';
export * from './useTableToString';
export * from './useSessionStorage';

View File

@@ -0,0 +1,8 @@
export interface TOpposingOptions {
light?: string;
dark?: string;
}
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
output: TStructuredResponse;
}

View File

@@ -0,0 +1,15 @@
import { useMemo } from 'react';
export function useBooleanValue<T extends any, F extends any>(
status: boolean,
ifTrue: T,
ifFalse: F,
): T | F {
return useMemo(() => {
if (status) {
return ifTrue;
} else {
return ifFalse;
}
}, [status]);
}

View File

@@ -1,8 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useToken } from '@chakra-ui/core'; import { useToken } from '@chakra-ui/react';
import { getColor, isLight } from '@chakra-ui/theme-tools'; import { getColor, isLight } from '@chakra-ui/theme-tools';
import { useTheme } from '~/context'; import { useTheme } from '~/context';
import type { TOpposingOptions } from './types';
export function useIsDark(color: string) { export function useIsDark(color: string) {
const theme = useTheme(); const theme = useTheme();
if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) { if (typeof color === 'string' && color.match(/[a-zA-Z]+\.[a-zA-Z0-9]+/g)) {
@@ -17,7 +19,7 @@ export function useIsDark(color: string) {
return opposingShouldBeDark; return opposingShouldBeDark;
} }
export function useOpposingColor(color: string, options?: IOpposingOptions): string { export function useOpposingColor(color: string, options?: TOpposingOptions): string {
const [opposingColor, setOpposingColor] = useState<string>('inherit'); const [opposingColor, setOpposingColor] = useState<string>('inherit');
const isBlack = useIsDark(color); const isBlack = useIsDark(color);
@@ -30,7 +32,7 @@ export function useOpposingColor(color: string, options?: IOpposingOptions): str
return opposingColor; return opposingColor;
} }
export function useOpposingToken(color: string, options?: IOpposingOptions): string { export function useOpposingToken(color: string, options?: TOpposingOptions): string {
const [opposingColor, setOpposingColor] = useState<string>('inherit'); const [opposingColor, setOpposingColor] = useState<string>('inherit');
const isBlack = useIsDark(color); const isBlack = useIsDark(color);
const dark = options?.dark ?? 'dark'; const dark = options?.dark ?? 'dark';

View File

@@ -0,0 +1,8 @@
import { useMemo } from 'react';
import format from 'string-format';
type FmtArgs = { [k: string]: any } | string;
export function useStrf(str: string, fmt: FmtArgs, ...deps: any[]): string {
return useMemo(() => format(str, fmt), deps);
}

View File

@@ -0,0 +1,112 @@
import { useCallback } from 'react';
import dayjs from 'dayjs';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import utcPlugin from 'dayjs/plugin/utc';
import { useConfig } from '~/context';
import { TStringTableData } from './types';
dayjs.extend(relativeTimePlugin);
dayjs.extend(utcPlugin);
type TFormatter = (v: any) => string;
type TFormatted = {
age: (v: number) => string;
active: (v: boolean) => string;
as_path: (v: number[]) => string;
communities: (v: string[]) => string;
rpki_state: (v: number, n: TRPKIStates) => string;
};
function formatAsPath(path: number[]): string {
return path.join(' → ');
}
function formatCommunities(comms: string[]): string {
const commsStr = comms.map(c => ` - ${c}`);
return '\n' + commsStr.join('\n');
}
function formatBool(val: boolean): string {
let fmt = '';
if (val === true) {
fmt = 'yes';
} else if (val === false) {
fmt = 'no';
}
return fmt;
}
function formatTime(val: number): string {
const now = dayjs.utc();
const then = now.subtract(val, 'second');
const timestamp = then.toString().replace('GMT', 'UTC');
const relative = now.to(then, true);
return `${relative} (${timestamp})`;
}
export function useTableToString(
target: string,
data: TStringTableData,
...deps: any
): () => string {
const { web, parsed_data_fields } = useConfig();
function formatRpkiState(val: number): string {
const rpkiStates = [
web.text.rpki_invalid,
web.text.rpki_valid,
web.text.rpki_unknown,
web.text.rpki_unverified,
];
return rpkiStates[val];
}
const tableFormatMap = {
age: formatTime,
active: formatBool,
as_path: formatAsPath,
communities: formatCommunities,
rpki_state: formatRpkiState,
};
function isFormatted(key: string): key is keyof TFormatted {
return key in tableFormatMap;
}
function getFmtFunc(accessor: keyof TRoute): TFormatter {
if (isFormatted(accessor)) {
return tableFormatMap[accessor];
} else {
return String;
}
}
function doFormat(target: string, data: TStringTableData): string {
try {
let tableStringParts = [`Routes For: ${target}`, `Timestamp: ${data.timestamp} UTC`];
data.output.routes.map(route => {
parsed_data_fields.map(field => {
const [header, accessor, align] = field;
if (align !== null) {
let value = route[accessor];
const fmtFunc = getFmtFunc(accessor);
value = fmtFunc(value);
if (accessor === 'prefix') {
tableStringParts.push(` - ${header}: ${value}`);
} else {
tableStringParts.push(` - ${header}: ${value}`);
}
}
});
});
return tableStringParts.join('\n');
} catch (err) {
console.error(err);
return `An error occurred while parsing the output: '${err.message}'`;
}
}
return useCallback(() => doFormat(target, data), deps);
}

View File

@@ -15,15 +15,17 @@
}, },
"browserslist": "> 0.25%, not dead", "browserslist": "> 0.25%, not dead",
"dependencies": { "dependencies": {
"@chakra-ui/core": "1.0.0-rc.8", "@chakra-ui/react": "^1.0.1",
"@chakra-ui/theme": "1.0.0-rc.8", "@emotion/react": "^11.1.1",
"@emotion/styled": "^11.0.0",
"@hookstate/core": "^3.0.1", "@hookstate/core": "^3.0.1",
"@meronex/icons": "^4.0.0", "@meronex/icons": "^4.0.0",
"add": "^2.0.6",
"axios": "^0.19.2", "axios": "^0.19.2",
"axios-hooks": "^1.9.0", "axios-hooks": "^1.9.0",
"chroma-js": "^2.1.0", "chroma-js": "^2.1.0",
"dayjs": "^1.8.25", "dayjs": "^1.8.25",
"framer-motion": "^1.10.0", "framer-motion": "^2.9.4",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"next": "^9.5.4", "next": "^9.5.4",
"react": "16.14.0", "react": "16.14.0",
@@ -33,16 +35,16 @@
"react-markdown": "^4.3.1", "react-markdown": "^4.3.1",
"react-select": "^3.0.8", "react-select": "^3.0.8",
"react-string-replace": "^0.4.4", "react-string-replace": "^0.4.4",
"react-table": "^7.0.4", "react-table": "^7.6.2",
"string-format": "^2.0.0", "string-format": "^2.0.0",
"tempy": "^0.5.0", "tempy": "^0.5.0",
"use-media": "^1.4.0", "yarn": "^1.22.10",
"yup": "^0.28.3" "yup": "^0.28.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^14.11.10", "@types/node": "^14.11.10",
"@types/react-select": "^3.0.22", "@types/react-select": "^3.0.22",
"@types/react-table": "7.0.4", "@types/react-table": "^7.0.25",
"@types/string-format": "^2.0.0", "@types/string-format": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^2.24.0", "@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0", "@typescript-eslint/parser": "^2.24.0",

View File

@@ -1,41 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */ "target": "ES5",
/* Basic Options */ "module": "esnext",
// "incremental": true, /* Enable incremental compilation */ "downlevelIteration": true,
"target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "strict": true,
"module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */
// "checkJs": true, /* Report errors in .js files. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "." /* Base directory to resolve non-absolute module names. */, "baseUrl": "." /* Base directory to resolve non-absolute module names. */,
"paths": { "paths": {
/* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"~/components": ["components/index"], "~/components": ["components/index"],
"~/components/*": ["components/*"], "~/components/*": ["components/*"],
"~/context": ["context/index"], "~/context": ["context/index"],
@@ -49,23 +19,9 @@
"~/util": ["util/index"], "~/util": ["util/index"],
"~/util/*": ["util/*"] "~/util/*": ["util/*"]
}, },
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "esModuleInterop": true,
// "typeRoots": [] /* List of folders to include type definitions from. */, "skipLibCheck": true,
// "types": [] /* Type declaration files to be included in compilation. */, "forceConsistentCasingInFileNames": true,
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"noEmit": true, "noEmit": true,
@@ -75,5 +31,5 @@
"jsx": "preserve" "jsx": "preserve"
}, },
"exclude": ["node_modules"], "exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "globals.d.ts"] "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/globals.d.ts"]
} }

View File

@@ -94,16 +94,17 @@ export interface IDeviceVrf extends IDeviceVrfBase {
ipv6: boolean; ipv6: boolean;
} }
interface IDeviceBase { interface TDeviceBase {
name: string;
network: string; network: string;
display_name: string; display_name: string;
} }
export interface IDevice extends IDeviceBase { export interface TDevice extends TDeviceBase {
vrfs: IDeviceVrf[]; vrfs: IDeviceVrf[];
} }
export interface INetworkLocation extends IDeviceBase { export interface INetworkLocation extends TDeviceBase {
vrfs: IDeviceVrfBase[]; vrfs: IDeviceVrfBase[];
} }
@@ -112,7 +113,7 @@ export interface INetwork {
locations: INetworkLocation[]; locations: INetworkLocation[];
} }
export type TParsedDataField = [string, string, 'left' | 'right' | 'center' | null]; export type TParsedDataField = [string, keyof TRoute, 'left' | 'right' | 'center' | null];
export interface IQueryContent { export interface IQueryContent {
content: string; content: string;
@@ -152,11 +153,12 @@ export interface IConfig {
google_analytics?: string; google_analytics?: string;
site_title: string; site_title: string;
site_keywords: string[]; site_keywords: string[];
site_description: string;
web: IConfigWeb; web: IConfigWeb;
messages: IConfigMessages; messages: IConfigMessages;
hyperglass_version: string; hyperglass_version: string;
queries: IConfigQueries; queries: IConfigQueries;
devices: IDevice[]; devices: TDevice[];
vrfs: IDeviceVrfBase[]; vrfs: IDeviceVrfBase[];
parsed_data_fields: TParsedDataField[]; parsed_data_fields: TParsedDataField[];
content: IConfigContent; content: IConfigContent;

Some files were not shown because too many files have changed in this diff Show More