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

continue typescript & chakra v1 migrations [skip ci]

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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