mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
fix results panel expansion, closes #100
This commit is contained in:
@@ -1,72 +1,15 @@
|
||||
import { useEffect, 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 { useEffect } from 'react';
|
||||
import { Accordion } from '@chakra-ui/react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { AnimatedDiv } from '~/components';
|
||||
import { useDevice, useLGState } from '~/hooks';
|
||||
import { isQueryType } from '~/types';
|
||||
import { Result } from './individual';
|
||||
import { Tags } from './tags';
|
||||
|
||||
export const Results: React.FC = () => {
|
||||
const { queries, vrfs, web } = useConfig();
|
||||
const { queryLocation, queryTarget, queryType, queryVrf } = useLGState();
|
||||
|
||||
const getDevice = useDevice();
|
||||
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[]>([]);
|
||||
|
||||
const matchedVrf =
|
||||
vrfs.filter(v => v.id === queryVrf.value)[0] ?? vrfs.filter(v => v.id === 'default')[0];
|
||||
|
||||
let queryTypeLabel = '';
|
||||
if (isQueryType(queryType.value)) {
|
||||
queryTypeLabel = queries[queryType.value].display_name;
|
||||
}
|
||||
|
||||
// Scroll to the top of the page when results load - primarily for mobile.
|
||||
useEffect(() => {
|
||||
@@ -77,62 +20,7 @@ export const Results: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
p={0}
|
||||
my={4}
|
||||
w="100%"
|
||||
mx="auto"
|
||||
textAlign="left"
|
||||
maxW={{ base: '100%', lg: '75%', xl: '50%' }}
|
||||
>
|
||||
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
|
||||
<AnimatePresence>
|
||||
{queryLocation.value && (
|
||||
<>
|
||||
<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={queryTypeLabel}
|
||||
/>
|
||||
</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.value}
|
||||
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>
|
||||
<Tags />
|
||||
<AnimatedDiv
|
||||
p={0}
|
||||
my={4}
|
||||
@@ -148,22 +36,20 @@ export const Results: React.FC = () => {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
maxW={{ base: '100%', md: '75%' }}
|
||||
>
|
||||
<Accordion allowMultiple allowToggle index={resultsComplete}>
|
||||
<Accordion allowMultiple allowToggle>
|
||||
<AnimatePresence>
|
||||
{queryLocation.value &&
|
||||
queryLocation.map((loc, i) => {
|
||||
const device = getDevice(loc.value);
|
||||
return (
|
||||
<Result
|
||||
key={i}
|
||||
index={i}
|
||||
device={device}
|
||||
key={device.name}
|
||||
queryLocation={loc.value}
|
||||
queryVrf={queryVrf.value}
|
||||
setComplete={setComplete}
|
||||
queryType={queryType.value}
|
||||
queryTarget={queryTarget.value}
|
||||
resultsComplete={resultsComplete}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@@ -12,3 +12,10 @@ export function isFetchError(error: any): error is Response {
|
||||
export function isLGError(error: any): error is TQueryResponse {
|
||||
return typeof error !== 'undefined' && error !== null && 'output' in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the response is an LG error, false if not.
|
||||
*/
|
||||
export function isLGOutputOrError(data: any): data is TQueryResponse {
|
||||
return typeof data !== 'undefined' && data !== null && data?.level !== 'success';
|
||||
}
|
||||
|
@@ -2,12 +2,14 @@ import { forwardRef, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
chakra,
|
||||
Icon,
|
||||
Alert,
|
||||
HStack,
|
||||
Tooltip,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
useAccordionContext,
|
||||
AccordionButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -17,43 +19,30 @@ import { BGPTable, Countdown, TextOutput, If, Path } from '~/components';
|
||||
import { useColorValue, useConfig, useMobile } from '~/context';
|
||||
import { useStrf, useLGQuery, useLGState, useTableToString } from '~/hooks';
|
||||
import { isStructuredOutput, isStringOutput } from '~/types';
|
||||
import { isStackError, isFetchError, isLGError } from './guards';
|
||||
import { isStackError, isFetchError, isLGError, isLGOutputOrError } from './guards';
|
||||
import { RequeryButton } from './requeryButton';
|
||||
import { CopyButton } from './copyButton';
|
||||
import { FormattedError } from './error';
|
||||
import { ResultHeader } from './header';
|
||||
|
||||
import type { TAccordionHeaderWrapper, TResult, TErrorLevels } from './types';
|
||||
import type { TResult, TErrorLevels } from './types';
|
||||
|
||||
const AnimatedAccordionItem = motion.custom(AccordionItem);
|
||||
|
||||
const AccordionHeaderWrapper: React.FC<TAccordionHeaderWrapper> = (
|
||||
props: TAccordionHeaderWrapper,
|
||||
) => {
|
||||
const { hoverBg, ...rest } = props;
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
_hover={{ bg: hoverBg }}
|
||||
_focus={{ boxShadow: 'outline' }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const AccordionHeaderWrapper = chakra('div', {
|
||||
baseStyle: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
_hover: { bg: 'blackAlpha.50' },
|
||||
_focus: { boxShadow: 'outline' },
|
||||
},
|
||||
});
|
||||
|
||||
const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props: TResult, ref) => {
|
||||
const {
|
||||
index,
|
||||
device,
|
||||
queryVrf,
|
||||
queryType,
|
||||
queryTarget,
|
||||
setComplete,
|
||||
queryLocation,
|
||||
resultsComplete,
|
||||
} = props;
|
||||
const { index, device, queryVrf, queryType, queryTarget, queryLocation } = props;
|
||||
|
||||
const { web, cache, messages } = useConfig();
|
||||
const { index: indices, setIndex } = useAccordionContext();
|
||||
|
||||
const isMobile = useMobile();
|
||||
const color = useColorValue('black', 'white');
|
||||
@@ -82,17 +71,6 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||
|
||||
const cacheLabel = useStrf(web.text.cache_icon, { time: data?.timestamp }, [data?.timestamp]);
|
||||
|
||||
const handleToggle = () => {
|
||||
// Close if open.
|
||||
if (resultsComplete.includes(index)) {
|
||||
setComplete(p => p.filter(i => i !== index));
|
||||
}
|
||||
// Open if closed.
|
||||
else if (!resultsComplete.includes(index)) {
|
||||
setComplete(p => [...p, index]);
|
||||
}
|
||||
};
|
||||
|
||||
const errorKeywords = useMemo(() => {
|
||||
let kw = [] as string[];
|
||||
if (isLGError(data)) {
|
||||
@@ -101,19 +79,22 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||
return kw;
|
||||
}, [data]);
|
||||
|
||||
let errorMsg;
|
||||
|
||||
if (isLGError(error)) {
|
||||
errorMsg = error.output as string;
|
||||
} else if (isFetchError(error)) {
|
||||
errorMsg = startCase(error.statusText);
|
||||
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
|
||||
errorMsg = messages.request_timeout;
|
||||
} else if (isStackError(error)) {
|
||||
errorMsg = startCase(error.message);
|
||||
} else {
|
||||
errorMsg = messages.general;
|
||||
}
|
||||
// Parse the the response and/or the error to determine from where to extract the error message.
|
||||
const errorMsg = useMemo(() => {
|
||||
if (isLGError(error)) {
|
||||
return error.output as string;
|
||||
} else if (isLGOutputOrError(data)) {
|
||||
return data.output as string;
|
||||
} else if (isFetchError(error)) {
|
||||
return startCase(error.statusText);
|
||||
} else if (isStackError(error) && error.message.toLowerCase().startsWith('timeout')) {
|
||||
return messages.request_timeout;
|
||||
} else if (isStackError(error)) {
|
||||
return startCase(error.message);
|
||||
} else {
|
||||
return messages.general;
|
||||
}
|
||||
}, [isError, error, data]);
|
||||
|
||||
isError && console.error(error);
|
||||
|
||||
@@ -132,7 +113,7 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||
e = statusMap[idx];
|
||||
}
|
||||
return e;
|
||||
}, [error]);
|
||||
}, [isError, isLoading, data]);
|
||||
|
||||
const tableComponent = useMemo<boolean>(() => {
|
||||
let result = false;
|
||||
@@ -154,15 +135,20 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||
copyValue = errorMsg;
|
||||
}
|
||||
|
||||
// If this is the first completed result, open it.
|
||||
// Signal to the group that this result is done loading.
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isError && resultsComplete.length === 0) {
|
||||
setComplete([index]);
|
||||
// Only set the index if it's not already set and the query is finished loading.
|
||||
if (Array.isArray(indices) && indices.length === 0 && !isLoading) {
|
||||
// Only set the index if the response has data or an error.
|
||||
if (data || isError) {
|
||||
setIndex([index]);
|
||||
}
|
||||
}
|
||||
}, [isLoading, isError]);
|
||||
}, [data, isError]);
|
||||
|
||||
return (
|
||||
<AnimatedAccordionItem
|
||||
id={device.name}
|
||||
ref={ref}
|
||||
isDisabled={isLoading}
|
||||
exit={{ opacity: 0, y: 300 }}
|
||||
@@ -170,105 +156,100 @@ const _Result: React.ForwardRefRenderFunction<HTMLDivElement, TResult> = (props:
|
||||
initial={{ opacity: 0, y: 300 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.3 }}
|
||||
css={{
|
||||
'&:last-of-type': { borderBottom: 'none' },
|
||||
'&:first-of-type': { borderTop: 'none' },
|
||||
'&:last-of-type': { borderBottom: 'none' },
|
||||
}}
|
||||
>
|
||||
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
|
||||
<AccordionButton
|
||||
py={2}
|
||||
w="unset"
|
||||
_hover={{}}
|
||||
_focus={{}}
|
||||
flex="1 0 auto"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ResultHeader
|
||||
isError={isError}
|
||||
loading={isLoading}
|
||||
errorMsg={errorMsg}
|
||||
errorLevel={errorLevel}
|
||||
runtime={data?.runtime ?? 0}
|
||||
title={device.display_name}
|
||||
/>
|
||||
</AccordionButton>
|
||||
<HStack py={2} spacing={1}>
|
||||
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
|
||||
<Path device={device.name} />
|
||||
)}
|
||||
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
|
||||
<RequeryButton requery={refetch} isDisabled={isLoading} />
|
||||
</HStack>
|
||||
</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' },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}>
|
||||
{!isError && typeof data !== 'undefined' ? (
|
||||
<>
|
||||
{isStructuredOutput(data) && data.level === 'success' && tableComponent ? (
|
||||
<BGPTable>{data.output}</BGPTable>
|
||||
) : isStringOutput(data) && data.level === 'success' && !tableComponent ? (
|
||||
<TextOutput>{data.output}</TextOutput>
|
||||
) : isStringOutput(data) && data.level !== 'success' ? (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={data.output} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={errorMsg} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={errorMsg} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
<>
|
||||
<AccordionHeaderWrapper>
|
||||
<AccordionButton py={2} w="unset" _hover={{}} _focus={{}} flex="1 0 auto">
|
||||
<ResultHeader
|
||||
isError={isLGOutputOrError(data)}
|
||||
loading={isLoading}
|
||||
errorMsg={errorMsg}
|
||||
errorLevel={errorLevel}
|
||||
runtime={data?.runtime ?? 0}
|
||||
title={device.display_name}
|
||||
/>
|
||||
</AccordionButton>
|
||||
<HStack py={2} spacing={1}>
|
||||
{isStructuredOutput(data) && data.level === 'success' && tableComponent && (
|
||||
<Path device={device.name} />
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Flex direction="row" flexWrap="wrap">
|
||||
<HStack
|
||||
px={3}
|
||||
mt={2}
|
||||
spacing={1}
|
||||
flex="1 0 auto"
|
||||
justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
|
||||
>
|
||||
<If c={cache.show_text && !isError && isCached}>
|
||||
<If c={!isMobile}>
|
||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
||||
</If>
|
||||
<Tooltip hasArrow label={cacheLabel} placement="top">
|
||||
<Box>
|
||||
<Icon as={BsLightningFill} color={color} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<If c={isMobile}>
|
||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
||||
</If>
|
||||
</If>
|
||||
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
|
||||
<RequeryButton requery={refetch} isDisabled={isLoading} />
|
||||
</HStack>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</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' },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Flex direction="column" flex="1 0 auto" maxW={error ? '100%' : undefined}>
|
||||
{!isError && typeof data !== 'undefined' ? (
|
||||
<>
|
||||
{isStructuredOutput(data) && data.level === 'success' && tableComponent ? (
|
||||
<BGPTable>{data.output}</BGPTable>
|
||||
) : isStringOutput(data) && data.level === 'success' && !tableComponent ? (
|
||||
<TextOutput>{data.output}</TextOutput>
|
||||
) : isStringOutput(data) && data.level !== 'success' ? (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={data.output} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={errorMsg} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
|
||||
<FormattedError message={errorMsg} keywords={errorKeywords} />
|
||||
</Alert>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Flex direction="row" flexWrap="wrap">
|
||||
<HStack
|
||||
px={3}
|
||||
mt={2}
|
||||
spacing={1}
|
||||
flex="1 0 auto"
|
||||
justifyContent={{ base: 'flex-start', lg: 'flex-end' }}
|
||||
>
|
||||
<If c={cache.show_text && !isError && isCached}>
|
||||
<If c={!isMobile}>
|
||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
||||
</If>
|
||||
<Tooltip hasArrow label={cacheLabel} placement="top">
|
||||
<Box>
|
||||
<Icon as={BsLightningFill} color={color} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<If c={isMobile}>
|
||||
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
|
||||
</If>
|
||||
</If>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</>
|
||||
</AnimatedAccordionItem>
|
||||
);
|
||||
};
|
||||
|
128
hyperglass/ui/components/results/tags.tsx
Normal file
128
hyperglass/ui/components/results/tags.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Box, Stack, useToken } from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Label } from '~/components';
|
||||
import { useConfig, useBreakpointValue } from '~/context';
|
||||
import { useLGState } from '~/hooks';
|
||||
import { isQueryType } from '~/types';
|
||||
|
||||
import type { Transition } from 'framer-motion';
|
||||
|
||||
const transition = { duration: 0.3, delay: 0.5 } as Transition;
|
||||
|
||||
export const Tags: React.FC = () => {
|
||||
const { queries, vrfs, web } = useConfig();
|
||||
const { queryLocation, queryTarget, queryType, queryVrf } = useLGState();
|
||||
|
||||
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%' },
|
||||
});
|
||||
|
||||
let queryTypeLabel = '';
|
||||
if (isQueryType(queryType.value)) {
|
||||
queryTypeLabel = queries[queryType.value].display_name;
|
||||
}
|
||||
|
||||
const matchedVrf =
|
||||
vrfs.filter(v => v.id === queryVrf.value)[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%' }}
|
||||
>
|
||||
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
|
||||
<AnimatePresence>
|
||||
{queryLocation.value && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={initialLeft}
|
||||
animate={animateLeft}
|
||||
exit={{ opacity: 0, x: '-100%' }}
|
||||
transition={transition}
|
||||
>
|
||||
<Label
|
||||
bg={queryBg}
|
||||
label={web.text.query_type}
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
value={queryTypeLabel}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={initialCenter}
|
||||
animate={animateCenter}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={transition}
|
||||
>
|
||||
<Label
|
||||
bg={targetBg}
|
||||
value={queryTarget.value}
|
||||
label={web.text.query_target}
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={initialRight}
|
||||
animate={animateRight}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
transition={transition}
|
||||
>
|
||||
<Label
|
||||
bg={vrfBg}
|
||||
label={web.text.query_vrf}
|
||||
value={matchedVrf.display_name}
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
/>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
@@ -1,4 +1,5 @@
|
||||
import type { ButtonProps, FlexProps } from '@chakra-ui/react';
|
||||
import type { State } from '@hookstate/core';
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from 'react-query';
|
||||
import type { TDevice, TQueryTypes } from '~/types';
|
||||
|
||||
@@ -16,10 +17,6 @@ export interface TFormattedError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TAccordionHeaderWrapper extends FlexProps {
|
||||
hoverBg: FlexProps['bg'];
|
||||
}
|
||||
|
||||
export interface TResult {
|
||||
index: number;
|
||||
device: TDevice;
|
||||
@@ -27,8 +24,6 @@ export interface TResult {
|
||||
queryTarget: string;
|
||||
queryLocation: string;
|
||||
queryType: TQueryTypes;
|
||||
resultsComplete: number[];
|
||||
setComplete: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
}
|
||||
|
||||
export type TErrorLevels = 'success' | 'warning' | 'error';
|
||||
@@ -40,3 +35,18 @@ export interface TCopyButton extends ButtonProps {
|
||||
export interface TRequeryButton extends ButtonProps {
|
||||
requery: UseQueryResult<TQueryResponse>['refetch'];
|
||||
}
|
||||
|
||||
export type TUseResults = {
|
||||
firstOpen: number | null;
|
||||
locations: { [k: string]: { complete: boolean; open: boolean; index: number } };
|
||||
};
|
||||
|
||||
export type TUseResultsMethods = {
|
||||
toggle(loc: string): void;
|
||||
setComplete(loc: string): void;
|
||||
getOpen(): number[];
|
||||
};
|
||||
|
||||
export type UseResultsReturn = {
|
||||
results: State<TUseResults>;
|
||||
} & TUseResultsMethods;
|
||||
|
100
hyperglass/ui/components/results/useResults.ts
Normal file
100
hyperglass/ui/components/results/useResults.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useEffect } from 'react';
|
||||
import { createState, useState } from '@hookstate/core';
|
||||
|
||||
import type { Plugin, State, PluginStateControl } from '@hookstate/core';
|
||||
import type { TUseResults, TUseResultsMethods, UseResultsReturn } from './types';
|
||||
|
||||
const MethodsId = Symbol('UseResultsMethods');
|
||||
|
||||
/**
|
||||
* Plugin methods.
|
||||
*/
|
||||
class MethodsInstance {
|
||||
/**
|
||||
* Toggle a location's open/closed state.
|
||||
*/
|
||||
public toggle(state: State<TUseResults>, loc: string) {
|
||||
state.locations[loc].open.set(p => !p);
|
||||
}
|
||||
/**
|
||||
* Set a location's completion state.
|
||||
*/
|
||||
public setComplete(state: State<TUseResults>, loc: string) {
|
||||
state.locations[loc].merge({ complete: true });
|
||||
const thisLoc = state.locations[loc];
|
||||
if (
|
||||
state.firstOpen.value === null &&
|
||||
state.locations.keys.includes(loc) &&
|
||||
state.firstOpen.value !== thisLoc.index.value
|
||||
) {
|
||||
state.firstOpen.set(thisLoc.index.value);
|
||||
this.toggle(state, loc);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the currently open panels. Passed to Chakra UI's index prop for internal state management.
|
||||
*/
|
||||
public getOpen(state: State<TUseResults>) {
|
||||
const open = state.locations.keys
|
||||
.filter(k => state.locations[k].complete.value && state.locations[k].open.value)
|
||||
.map(k => state.locations[k].index.value);
|
||||
return open;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hookstate plugin to provide convenience functions & tracking for the useResults hook.
|
||||
*/
|
||||
function Methods(inst?: State<TUseResults>): Plugin | TUseResultsMethods {
|
||||
if (inst) {
|
||||
const [instance] = inst.attach(MethodsId) as [
|
||||
MethodsInstance | Error,
|
||||
PluginStateControl<TUseResults>,
|
||||
];
|
||||
|
||||
if (instance instanceof Error) {
|
||||
throw instance;
|
||||
}
|
||||
|
||||
return {
|
||||
toggle: (loc: string) => instance.toggle(inst, loc),
|
||||
setComplete: (loc: string) => instance.setComplete(inst, loc),
|
||||
getOpen: () => instance.getOpen(inst),
|
||||
} as TUseResultsMethods;
|
||||
}
|
||||
return {
|
||||
id: MethodsId,
|
||||
init: () => {
|
||||
/* eslint @typescript-eslint/ban-types: 0 */
|
||||
return new MethodsInstance() as {};
|
||||
},
|
||||
} as Plugin;
|
||||
}
|
||||
const initialState = { firstOpen: null, locations: {} } as TUseResults;
|
||||
const resultsState = createState<TUseResults>(initialState);
|
||||
|
||||
/**
|
||||
* Track the state of each result, and whether or not each panel is open.
|
||||
*/
|
||||
export function useResults(initial: TUseResults['locations']): UseResultsReturn {
|
||||
// Initialize the global state before instantiating the hook, only once.
|
||||
useEffect(() => {
|
||||
if (resultsState.firstOpen.value === null && resultsState.locations.keys.length === 0) {
|
||||
resultsState.set({ firstOpen: null, locations: initial });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const results = useState(resultsState);
|
||||
results.attach(Methods as () => Plugin);
|
||||
|
||||
const methods = Methods(results) as TUseResultsMethods;
|
||||
|
||||
// Reset the state on unmount.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
results.set(initialState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { results, ...methods };
|
||||
}
|
Reference in New Issue
Block a user