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:
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
export * from './Countdown';
|
|
11
hyperglass/ui/components/Card/Card.d.ts
vendored
11
hyperglass/ui/components/Card/Card.d.ts
vendored
@@ -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 {}
|
|
@@ -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');
|
@@ -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
|
@@ -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');
|
@@ -1,3 +1,3 @@
|
|||||||
export * from './CardBody';
|
export * from './body';
|
||||||
export * from './CardFooter';
|
export * from './footer';
|
||||||
export * from './CardHeader';
|
export * from './header';
|
||||||
|
9
hyperglass/ui/components/Card/types.ts
Normal file
9
hyperglass/ui/components/Card/types.ts
Normal 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 {}
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
20
hyperglass/ui/components/Footer/Footer.d.ts
vendored
20
hyperglass/ui/components/Footer/Footer.d.ts
vendored
@@ -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';
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
28
hyperglass/ui/components/Footer/content.tsx
Normal file
28
hyperglass/ui/components/Footer/content.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
101
hyperglass/ui/components/Footer/footer.tsx
Normal file
101
hyperglass/ui/components/Footer/footer.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -1 +1 @@
|
|||||||
export * from './FooterMain';
|
export * from './footer';
|
||||||
|
16
hyperglass/ui/components/Footer/types.ts
Normal file
16
hyperglass/ui/components/Footer/types.ts
Normal 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';
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@@ -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}
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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} />
|
||||||
);
|
);
|
||||||
|
@@ -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} />;
|
@@ -1 +1 @@
|
|||||||
export * from './Markdown';
|
export * from './markdown';
|
||||||
|
@@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
));
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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" />
|
|
||||||
);
|
|
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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
|
7
hyperglass/ui/components/Table/button.tsx
Normal file
7
hyperglass/ui/components/Table/button.tsx
Normal 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" />
|
||||||
|
);
|
@@ -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');
|
||||||
|
|
@@ -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');
|
@@ -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';
|
||||||
|
191
hyperglass/ui/components/Table/main.tsx
Normal file
191
hyperglass/ui/components/Table/main.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@@ -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;
|
@@ -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');
|
@@ -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'>;
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
@@ -1 +1 @@
|
|||||||
export * from './If';
|
export * from './if';
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
4
hyperglass/ui/components/animated.ts
Normal file
4
hyperglass/ui/components/animated.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { chakra } from '@chakra-ui/react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const AnimatedDiv = chakra(motion.div);
|
24
hyperglass/ui/components/bgpTable/cell.tsx
Normal file
24
hyperglass/ui/components/bgpTable/cell.tsx
Normal 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] ?? <> </>;
|
||||||
|
};
|
179
hyperglass/ui/components/bgpTable/fields.tsx
Normal file
179
hyperglass/ui/components/bgpTable/fields.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
hyperglass/ui/components/bgpTable/index.ts
Normal file
1
hyperglass/ui/components/bgpTable/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './table';
|
46
hyperglass/ui/components/bgpTable/table.tsx
Normal file
46
hyperglass/ui/components/bgpTable/table.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
42
hyperglass/ui/components/bgpTable/types.ts
Normal file
42
hyperglass/ui/components/bgpTable/types.ts
Normal 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;
|
||||||
|
}
|
25
hyperglass/ui/components/codeBlock.tsx
Normal file
25
hyperglass/ui/components/codeBlock.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
33
hyperglass/ui/components/copyButton/copyButton.tsx
Normal file
33
hyperglass/ui/components/copyButton/copyButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
hyperglass/ui/components/copyButton/index.ts
Normal file
1
hyperglass/ui/components/copyButton/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './copyButton';
|
5
hyperglass/ui/components/copyButton/types.ts
Normal file
5
hyperglass/ui/components/copyButton/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ButtonProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export interface TCopyButton extends ButtonProps {
|
||||||
|
copyValue: string;
|
||||||
|
}
|
@@ -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)];
|
1
hyperglass/ui/components/countdown/index.ts
Normal file
1
hyperglass/ui/components/countdown/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './countdown';
|
10
hyperglass/ui/components/countdown/types.ts
Normal file
10
hyperglass/ui/components/countdown/types.ts
Normal 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;
|
||||||
|
}
|
165
hyperglass/ui/components/header/header.tsx
Normal file
165
hyperglass/ui/components/header/header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
1
hyperglass/ui/components/header/index.ts
Normal file
1
hyperglass/ui/components/header/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './header';
|
9
hyperglass/ui/components/header/types.ts
Normal file
9
hyperglass/ui/components/header/types.ts
Normal 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'];
|
@@ -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';
|
|
||||||
|
1
hyperglass/ui/components/label/index.ts
Normal file
1
hyperglass/ui/components/label/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './label';
|
61
hyperglass/ui/components/label/label.tsx
Normal file
61
hyperglass/ui/components/label/label.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
9
hyperglass/ui/components/label/types.ts
Normal file
9
hyperglass/ui/components/label/types.ts
Normal 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;
|
||||||
|
}
|
26
hyperglass/ui/components/loading.tsx
Normal file
26
hyperglass/ui/components/loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
59
hyperglass/ui/components/meta.tsx
Normal file
59
hyperglass/ui/components/meta.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
25
hyperglass/ui/components/resetButton.tsx
Normal file
25
hyperglass/ui/components/resetButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
21
hyperglass/ui/components/results/error.tsx
Normal file
21
hyperglass/ui/components/results/error.tsx
Normal 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>;
|
||||||
|
};
|
164
hyperglass/ui/components/results/group.tsx
Normal file
164
hyperglass/ui/components/results/group.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
48
hyperglass/ui/components/results/header.tsx
Normal file
48
hyperglass/ui/components/results/header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
1
hyperglass/ui/components/results/index.ts
Normal file
1
hyperglass/ui/components/results/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './group';
|
236
hyperglass/ui/components/results/individual.tsx
Normal file
236
hyperglass/ui/components/results/individual.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
40
hyperglass/ui/components/results/types.ts
Normal file
40
hyperglass/ui/components/results/types.ts
Normal 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;
|
||||||
|
}
|
1
hyperglass/ui/components/submitButton/index.ts
Normal file
1
hyperglass/ui/components/submitButton/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './submitButton';
|
107
hyperglass/ui/components/submitButton/submitButton.tsx
Normal file
107
hyperglass/ui/components/submitButton/submitButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
17
hyperglass/ui/components/submitButton/types.ts
Normal file
17
hyperglass/ui/components/submitButton/types.ts
Normal 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;
|
||||||
|
}
|
1
hyperglass/ui/components/textOutput/index.ts
Normal file
1
hyperglass/ui/components/textOutput/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './textOutput';
|
38
hyperglass/ui/components/textOutput/textOutput.tsx
Normal file
38
hyperglass/ui/components/textOutput/textOutput.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
5
hyperglass/ui/components/textOutput/types.ts
Normal file
5
hyperglass/ui/components/textOutput/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { BoxProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export interface TTextOutput extends Omit<BoxProps, 'children'> {
|
||||||
|
children: string;
|
||||||
|
}
|
@@ -1,5 +0,0 @@
|
|||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
export function withAnimation<P>(Component: React.FunctionComponent) {
|
|
||||||
return motion.custom<Omit<P, 'transition'>>(Component);
|
|
||||||
}
|
|
@@ -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: '' },
|
||||||
});
|
});
|
||||||
|
@@ -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';
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
29
hyperglass/ui/globals.d.ts
vendored
29
hyperglass/ui/globals.d.ts
vendored
@@ -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;
|
|
||||||
}
|
|
4
hyperglass/ui/hooks/hooks.d.ts
vendored
4
hyperglass/ui/hooks/hooks.d.ts
vendored
@@ -1,4 +0,0 @@
|
|||||||
interface IOpposingOptions {
|
|
||||||
light?: string;
|
|
||||||
dark?: string;
|
|
||||||
}
|
|
@@ -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';
|
||||||
|
8
hyperglass/ui/hooks/types.ts
Normal file
8
hyperglass/ui/hooks/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface TOpposingOptions {
|
||||||
|
light?: string;
|
||||||
|
dark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
|
||||||
|
output: TStructuredResponse;
|
||||||
|
}
|
15
hyperglass/ui/hooks/useBooleanValue.ts
Normal file
15
hyperglass/ui/hooks/useBooleanValue.ts
Normal 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]);
|
||||||
|
}
|
@@ -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';
|
||||||
|
8
hyperglass/ui/hooks/useStrf.ts
Normal file
8
hyperglass/ui/hooks/useStrf.ts
Normal 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);
|
||||||
|
}
|
112
hyperglass/ui/hooks/useTableToString.ts
Normal file
112
hyperglass/ui/hooks/useTableToString.ts
Normal 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);
|
||||||
|
}
|
14
hyperglass/ui/package.json
vendored
14
hyperglass/ui/package.json
vendored
@@ -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",
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -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
Reference in New Issue
Block a user