continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads 2020-12-20 01:21:24 -07:00
parent 257f802f3b
commit 0f0e61f403
27 changed files with 363 additions and 310 deletions

View file

@ -1,7 +1,7 @@
import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon } from '@chakra-ui/react';
import { useGlobalState } from '~/context';
import { useLGState } from '~/hooks';
import type { ButtonProps } from '@chakra-ui/react';
@ -10,7 +10,7 @@ const ChevronLeft = dynamic<MeronexIcon>(() =>
);
export const ResetButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { isSubmitting } = useGlobalState();
const { isSubmitting } = useLGState();
return (
<Button
ref={ref}

View file

@ -1,51 +1,110 @@
import {
IconButton,
Modal,
Popover,
PopoverTrigger,
PopoverArrow,
PopoverCloseButton,
ModalBody,
IconButton,
PopoverBody,
ModalOverlay,
ModalContent,
PopoverArrow,
PopoverTrigger,
PopoverContent,
ModalCloseButton,
PopoverCloseButton,
} from '@chakra-ui/react';
import { FiSearch } from '@meronex/icons/fi';
import { ResolvedTarget } from '~/components';
import { If, ResolvedTarget } from '~/components';
import { useMobile } from '~/context';
import { useLGState } from '~/hooks';
import type { TSubmitButton } from './types';
import type { IconButtonProps } from '@chakra-ui/react';
import type { OnChangeArgs } from '~/types';
import type { TSubmitButton, TRSubmitButton } from './types';
const SubmitIcon = (props: Omit<IconButtonProps, 'aria-label'>) => {
const { isLoading } = props;
return (
<IconButton
size="lg"
width={16}
type="submit"
icon={<FiSearch />}
title="Submit Query"
colorScheme="primary"
isLoading={isLoading}
aria-label="Submit Query"
/>
);
};
/**
* Mobile Submit Button
*/
const MSubmitButton = (props: TRSubmitButton) => {
const { children, isOpen, onClose, onChange } = props;
return (
<>
{children}
<Modal
size="xs"
isCentered
isOpen={isOpen}
onClose={onClose}
closeOnEsc={false}
closeOnOverlayClick={false}
motionPreset="slideInBottom">
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalBody px={4} py={10}>
{isOpen && <ResolvedTarget setTarget={onChange} />}
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
/**
* Desktop Submit Button
*/
const DSubmitButton = (props: TRSubmitButton) => {
const { children, isOpen, onClose, onChange } = props;
return (
<Popover isOpen={isOpen} onClose={onClose} closeOnBlur={false}>
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody p={6}>{isOpen && <ResolvedTarget setTarget={onChange} />}</PopoverBody>
</PopoverContent>
</Popover>
);
};
export const SubmitButton = (props: TSubmitButton) => {
const { children, handleChange, ...rest } = props;
const { btnLoading, resolvedIsOpen, resolvedClose } = useLGState();
const { handleChange } = props;
const { btnLoading, resolvedIsOpen, resolvedClose, resetForm } = useLGState();
const isMobile = useMobile();
function handleClose(): void {
btnLoading.set(false);
resetForm();
resolvedClose();
}
return (
<>
<Popover isOpen={resolvedIsOpen.value} onClose={handleClose} closeOnBlur={false}>
<PopoverTrigger>
<IconButton
size="lg"
width={16}
type="submit"
icon={<FiSearch />}
title="Submit Query"
aria-label="Submit Query"
colorScheme="primary"
isLoading={btnLoading.value}
{...rest}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody p={6}>
{resolvedIsOpen.value && <ResolvedTarget setTarget={handleChange} />}
</PopoverBody>
</PopoverContent>
</Popover>
<If c={isMobile}>
<MSubmitButton isOpen={resolvedIsOpen.value} onClose={handleClose} onChange={handleChange}>
<SubmitIcon isLoading={btnLoading.value} />
</MSubmitButton>
</If>
<If c={!isMobile}>
<DSubmitButton isOpen={resolvedIsOpen.value} onClose={handleClose} onChange={handleChange}>
<SubmitIcon isLoading={btnLoading.value} />
</DSubmitButton>
</If>
</>
);
};

View file

@ -16,3 +16,10 @@ export interface TSubmitButton extends Omit<IconButtonProps, 'aria-label'> {
export interface TRequeryButton extends ButtonProps {
requery(): void;
}
export interface TRSubmitButton {
isOpen: boolean;
onClose(): void;
onChange(e: OnChangeArgs): void;
children: React.ReactNode;
}

View file

@ -1,7 +1,7 @@
import {
Tag,
Modal,
Stack,
HStack,
Button,
useTheme,
ModalBody,
@ -14,6 +14,30 @@ import {
} from '@chakra-ui/react';
import { useConfig, useColorValue, useBreakpointValue } from '~/context';
import { CodeBlock } from '~/components';
import type { UseDisclosureReturn } from '@chakra-ui/react';
interface TViewer extends Pick<UseDisclosureReturn, 'isOpen' | 'onClose'> {
title: string;
children: React.ReactNode;
}
const Viewer = (props: TViewer) => {
const { title, isOpen, onClose, children } = props;
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
return (
<Modal isOpen={isOpen} onClose={onClose} size="full" scrollBehavior="inside">
<ModalOverlay />
<ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%">
<ModalHeader>{title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<CodeBlock>{children}</CodeBlock>
</ModalBody>
</ModalContent>
</Modal>
);
};
export const Debugger = () => {
const { isOpen: configOpen, onOpen: onConfigOpen, onClose: configClose } = useDisclosure();
@ -21,17 +45,16 @@ export const Debugger = () => {
const { colorMode } = useColorMode();
const config = useConfig();
const theme = useTheme();
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
const borderColor = useColorValue('gray.100', 'gray.600');
const mediaSize =
useBreakpointValue({ base: 'SMALL', md: 'MEDIUM', lg: 'LARGE', xl: 'X-LARGE' }) ?? 'UNKNOWN';
const tagSize = useBreakpointValue({ base: 'sm', lg: 'lg' }) ?? 'lg';
const btnSize = useBreakpointValue({ base: 'xs', lg: 'sm' }) ?? 'sm';
return (
<>
<Stack
<HStack
py={4}
px={4}
isInline
left={0}
right={0}
bottom={0}
@ -40,36 +63,27 @@ export const Debugger = () => {
borderWidth="1px"
position="relative"
justifyContent="center"
borderColor={borderColor}>
<Tag colorScheme="gray">{colorMode.toUpperCase()}</Tag>
<Tag colorScheme="teal">{mediaSize}</Tag>
<Button size="sm" colorScheme="cyan" onClick={onConfigOpen}>
borderColor={borderColor}
spacing={{ base: 2, lg: 8 }}>
<Tag size={tagSize} colorScheme="gray">
{colorMode.toUpperCase()}
</Tag>
<Button size={btnSize} colorScheme="blue" onClick={onConfigOpen}>
View Config
</Button>
<Button size="sm" colorScheme="purple" onClick={onThemeOpen}>
<Button size={btnSize} colorScheme="red" onClick={onThemeOpen}>
View Theme
</Button>
</Stack>
<Modal isOpen={configOpen} onClose={configClose} size="full">
<ModalOverlay />
<ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%">
<ModalHeader>Loaded Configuration</ModalHeader>
<ModalCloseButton />
<ModalBody>
<CodeBlock>{JSON.stringify(config, null, 4)}</CodeBlock>
</ModalBody>
</ModalContent>
</Modal>
<Modal isOpen={themeOpen} onClose={themeClose} size="full">
<ModalOverlay />
<ModalContent bg={bg} color={color} py={4} borderRadius="md" maxW="90%">
<ModalHeader>Loaded Theme</ModalHeader>
<ModalCloseButton />
<ModalBody>
<CodeBlock>{JSON.stringify(theme, null, 4)}</CodeBlock>
</ModalBody>
</ModalContent>
</Modal>
<Tag size={tagSize} colorScheme="teal">
{mediaSize}
</Tag>
</HStack>
<Viewer isOpen={configOpen} onClose={configClose} title="Config">
{JSON.stringify(config, null, 4)}
</Viewer>
<Viewer isOpen={themeOpen} onClose={themeClose} title="Theme">
{JSON.stringify(theme, null, 4)}
</Viewer>
</>
);
};

View file

@ -20,6 +20,7 @@ const Option = (props: OptionProps<Dict, false>) => {
return (
<components.Option {...props}>
<Text as="span">{label}</Text>
<br />
<Text fontSize="xs" as="span">
{data.description}
</Text>

View file

@ -1,34 +1,24 @@
import { useMemo } from 'react';
import { Flex, FormControl, FormLabel, FormErrorMessage } from '@chakra-ui/react';
import { useFormContext } from 'react-hook-form';
import { If } from '~/components';
import { useColorValue } from '~/context';
import { useBooleanValue } from '~/hooks';
import { TField } from './types';
import { TField, TFormError } from './types';
export const FormField = (props: TField) => {
const {
name,
label,
errors,
children,
labelAddOn,
fieldAddOn,
hiddenLabels = false,
...rest
} = props;
const { name, label, children, labelAddOn, fieldAddOn, hiddenLabels = false, ...rest } = props;
const labelColor = useColorValue('blackAlpha.700', 'whiteAlpha.700');
const errorColor = useColorValue('red.500', 'red.300');
const opacity = useBooleanValue(hiddenLabels, 0, undefined);
const error = useMemo<string | undefined>(() => {
let result;
if (Array.isArray(errors)) {
result = errors.join(', ');
} else if (typeof errors === 'string') {
result = errors;
}
return result;
}, [errors]);
const { errors } = useFormContext();
const error = name in errors && (errors[name] as TFormError);
if (error !== false) {
console.warn(`${label} Error: ${error.message}`);
}
return (
<FormControl
@ -38,7 +28,7 @@ export const FormField = (props: TField) => {
maxW="100%"
flexDir="column"
my={{ base: 2, lg: 4 }}
isInvalid={typeof error !== 'undefined'}
isInvalid={error !== false}
flex={{ base: '1 0 100%', lg: '1 0 33.33%' }}
{...rest}>
<FormLabel
@ -47,7 +37,7 @@ export const FormField = (props: TField) => {
htmlFor={name}
display="flex"
opacity={opacity}
color={labelColor}
color={error !== false ? errorColor : labelColor}
alignItems="center"
justifyContent="space-between">
{label}
@ -59,7 +49,7 @@ export const FormField = (props: TField) => {
{fieldAddOn}
</Flex>
</If>
<FormErrorMessage opacity={opacity}>{error}</FormErrorMessage>
<FormErrorMessage opacity={opacity}>{error && error.message}</FormErrorMessage>
</FormControl>
);
};

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { Select } from '~/components';
import { useConfig } from '~/context';
@ -21,6 +22,8 @@ export const QueryLocation = (props: TQuerySelectField) => {
const { onChange, label } = props;
const { networks } = useConfig();
const { errors } = useFormContext();
const options = useMemo(() => buildOptions(networks), [networks.length]);
function handleChange(e: TSelectOption | TSelectOption[]): void {
@ -44,6 +47,7 @@ export const QueryLocation = (props: TQuerySelectField) => {
name="query_location"
onChange={handleChange}
closeMenuOnSelect={false}
isError={typeof errors.query_location !== 'undefined'}
/>
);
};

View file

@ -1,69 +1,42 @@
import { useEffect } from 'react';
import { Input } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import { useLGState } from '~/hooks';
import type { TQueryTarget } from './types';
const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/gim;
export const QueryTarget = (props: TQueryTarget) => {
const {
name,
value,
setFqdn,
register,
setTarget,
unregister,
placeholder,
displayValue,
resolveTarget,
setDisplayValue,
} = props;
const { name, register, setTarget, placeholder, resolveTarget } = props;
const bg = useColorValue('white', 'whiteAlpha.100');
const color = useColorValue('gray.400', 'whiteAlpha.800');
const border = useColorValue('gray.100', 'whiteAlpha.50');
const placeholderColor = useColorValue('gray.600', 'whiteAlpha.700');
function handleBlur(): void {
if (resolveTarget && displayValue && fqdnPattern.test(displayValue)) {
setFqdn(displayValue);
} else if (resolveTarget && !displayValue) {
setFqdn(null);
}
}
const { queryTarget, fqdnTarget, displayTarget } = useLGState();
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
setDisplayValue(e.target.value);
displayTarget.set(e.target.value);
setTarget({ field: name, value: e.target.value });
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void {
if (['Tab', 'NumpadEnter'].includes(e.key)) {
handleBlur();
if (resolveTarget && displayTarget.value && fqdnPattern.test(displayTarget.value)) {
fqdnTarget.set(displayTarget.value);
}
}
useEffect(() => {
register({ name });
return () => unregister(name);
}, [register, unregister, name]);
return (
<>
<input hidden readOnly name={name} ref={register} value={value} />
<input hidden readOnly name={name} ref={register} value={queryTarget.value} />
<Input
bg={bg}
size="lg"
color={color}
borderRadius="md"
onBlur={handleBlur}
onFocus={handleBlur}
value={displayValue}
value={displayTarget.value}
borderColor={border}
onChange={handleChange}
aria-label={placeholder}
onKeyDown={handleKeyDown}
placeholder={placeholder}
name="query_target_display"
_placeholder={{ color: placeholderColor }}

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { Select } from '~/components';
import { useConfig } from '~/context';
@ -14,6 +15,7 @@ function buildOptions(queryTypes: TQuery[]): TSelectOption[] {
export const QueryType = (props: TQuerySelectField) => {
const { onChange, label } = props;
const { queries } = useConfig();
const { errors } = useFormContext();
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
@ -30,6 +32,7 @@ export const QueryType = (props: TQuerySelectField) => {
options={options}
aria-label={label}
onChange={handleChange}
isError={typeof errors.query_type !== 'undefined'}
/>
);
};

View file

@ -2,7 +2,7 @@ import { useEffect, useMemo } from 'react';
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { useQuery } from 'react-query';
import { FiArrowRightCircle as RightArrow } from '@meronex/icons/fi';
import { useConfig, useColorValue, useGlobalState } from '~/context';
import { useConfig, useColorValue } from '~/context';
import { useStrf, useLGState } from '~/hooks';
import type { DnsOverHttps } from '~/types';
@ -20,8 +20,7 @@ function findAnswer(data: DnsOverHttps.Response | undefined): string {
export const ResolvedTarget = (props: TResolvedTarget) => {
const { setTarget } = props;
const { web } = useConfig();
const { isSubmitting } = useGlobalState();
const { fqdnTarget, queryTarget, families, formData } = useLGState();
const { fqdnTarget, isSubmitting, families, formData } = useLGState();
const color = useColorValue('secondary.500', 'secondary.300');

View file

@ -1,11 +1,13 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError, Control } from 'react-hook-form';
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, TFormData } from '~/types';
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs } from '~/types';
import type { ValidationError } from 'yup';
export type TFormError = Pick<ValidationError, 'message' | 'type'>;
export interface TField extends FormControlProps {
name: string;
label: string;
errors?: FieldError | FieldError[];
hiddenLabels?: boolean;
labelAddOn?: React.ReactNode;
fieldAddOn?: React.ReactNode;
@ -30,32 +32,12 @@ export interface TCommunitySelect {
unregister: Control['unregister'];
}
/**
* placeholder,
register,
unregister,
setFqdn,
name,
value,
setTarget,
resolveTarget,
displayValue,
setDisplayValue,
*/
export interface TQueryTarget {
name: string;
placeholder: string;
displayValue: string;
resolveTarget: boolean;
setFqdn(f: string | null): void;
setTarget(e: OnChangeArgs): void;
register: Control['register'];
value: TFormData['query_target'];
setDisplayValue(d: string): void;
unregister: Control['unregister'];
setTarget(e: OnChangeArgs): void;
}
export interface TResolvedTarget {

View file

@ -11,31 +11,40 @@ import {
} from '@chakra-ui/react';
import { If, Markdown } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useGreeting, useOpposingColor } from '~/hooks';
import type { TGreeting } from './types';
export const Greeting = (props: TGreeting) => {
const { onClickThrough, ...rest } = props;
const { web, content } = useConfig();
const { isOpen, onClose } = useDisclosure();
const [greetingAck, setGreetingAck] = useGreeting();
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
const bg = useColorValue('white', 'gray.800');
const color = useOpposingColor(bg);
function handleClick(): void {
onClickThrough();
onClose();
return;
function handleClose(ack: boolean = false): void {
if (web.greeting.required && !greetingAck && !ack) {
setGreetingAck(false);
} else if (web.greeting.required && !greetingAck && ack) {
setGreetingAck();
onClose();
} else if (web.greeting.required && greetingAck) {
onClose();
} else if (!web.greeting.required) {
setGreetingAck();
onClose();
}
}
return (
<Modal
size="lg"
isCentered
size="full"
isOpen={isOpen}
onClose={handleClick}
closeOnEsc={!web.greeting.required}
closeOnOverlayClick={!web.greeting.required}>
onClose={handleClose}
motionPreset="slideInBottom"
closeOnEsc={web.greeting.required}
isOpen={!greetingAck ? true : isOpen}
closeOnOverlayClick={web.greeting.required}>
<ModalOverlay />
<ModalContent
py={4}
@ -43,7 +52,7 @@ export const Greeting = (props: TGreeting) => {
color={color}
borderRadius="md"
maxW={{ base: '95%', md: '75%' }}
{...rest}>
{...props}>
<ModalHeader>{web.greeting.title}</ModalHeader>
<If c={!web.greeting.required}>
<ModalCloseButton />
@ -52,7 +61,7 @@ export const Greeting = (props: TGreeting) => {
<Markdown content={content.greeting} />
</ModalBody>
<ModalFooter>
<Button colorScheme="primary" onClick={handleClick}>
<Button colorScheme="primary" onClick={() => handleClose(true)}>
{web.greeting.button}
</Button>
</ModalFooter>

View file

@ -1,5 +1,3 @@
import { BoxProps } from '@chakra-ui/react';
export interface TGreeting extends BoxProps {
onClickThrough(): void;
}
export interface TGreeting extends BoxProps {}

View file

@ -1,8 +1,8 @@
import { Flex } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { AnimatedDiv, Title, ResetButton, ColorModeToggle } from '~/components';
import { useColorValue, useConfig, useGlobalState, useBreakpointValue } from '~/context';
import { useBooleanValue } from '~/hooks';
import { useColorValue, useConfig, useBreakpointValue } from '~/context';
import { useBooleanValue, useLGState } from '~/hooks';
import type { ResponsiveValue } from '@chakra-ui/react';
import type { THeader, TTitleMode, THeaderLayout } from './types';
@ -39,7 +39,7 @@ export const Header = (props: THeader) => {
const bg = useColorValue('white', 'black');
const { web } = useConfig();
const { isSubmitting } = useGlobalState();
const { isSubmitting } = useLGState();
const mlResetButton = useBooleanValue(isSubmitting.value, { base: 0, md: 2 }, undefined);
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });

View file

@ -1,33 +1,31 @@
import { useRef } from 'react';
import { Flex } from '@chakra-ui/react';
import { useConfig, useColorValue, useGlobalState } from '~/context';
import { useConfig, useColorValue } from '~/context';
import { If, Debugger, Greeting, Footer, Header } from '~/components';
import { useGreeting } from '~/hooks';
import { useLGState } from '~/hooks';
import type { TFrame } from './types';
export const Frame = (props: TFrame) => {
const { web, developer_mode } = useConfig();
const { isSubmitting, formData } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
const { developer_mode } = useConfig();
const { isSubmitting, resetForm } = useLGState();
const bg = useColorValue('white', 'black');
const color = useColorValue('black', 'white');
const containerRef = useRef<HTMLDivElement>({} as HTMLDivElement);
function resetForm(): void {
function handleReset(): void {
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
isSubmitting.set(false);
formData.set({ query_location: [], query_target: '', query_type: '', query_vrf: '' });
return;
resetForm();
}
return (
<>
<Flex bg={bg} w="100%" color={color} flexDir="column" minHeight="100vh" ref={containerRef}>
<Flex px={2} flex="0 1 auto" flexDirection="column">
<Header resetForm={resetForm} />
<Header resetForm={handleReset} />
</Flex>
<Flex
px={2}
@ -46,9 +44,7 @@ export const Frame = (props: TFrame) => {
<Debugger />
</If>
</Flex>
<If c={web.greeting.enable && !greetingAck}>
<Greeting onClickThrough={setGreetingAck} />
</If>
<Greeting />
</>
);
};

View file

@ -1,13 +1,11 @@
import { AnimatePresence } from 'framer-motion';
import { If, HyperglassForm, Results } from '~/components';
import { useGlobalState } from '~/context';
import { useLGState } from '~/hooks';
import { all } from '~/util';
import { Frame } from './frame';
export const Layout: React.FC = () => {
const { isSubmitting } = useGlobalState();
const { formData } = useLGState();
const { isSubmitting, formData } = useLGState();
return (
<Frame>
<If

View file

@ -1,8 +1,9 @@
import { useEffect, useMemo } from 'react';
import { Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { intersectionWith } from 'lodash';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import {
If,
AnimatedForm,
@ -16,7 +17,7 @@ import {
QueryLocation,
CommunitySelect,
} from '~/components';
import { useConfig, useGlobalState } from '~/context';
import { useConfig } from '~/context';
import { useStrf, useGreeting, useDevice, useLGState } from '~/hooks';
import { isQueryType, isString } from '~/types';
@ -27,7 +28,6 @@ const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-z
export const HyperglassForm = () => {
const { web, content, messages, queries } = useConfig();
const { isSubmitting } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
const getDevice = useDevice();
@ -42,23 +42,24 @@ export const HyperglassForm = () => {
query_target: yup.string().required(noQueryTarget),
});
const { handleSubmit, register, unregister, setValue, errors, reset } = useForm<TFormData>({
validationSchema: formSchema,
const formInstance = useForm<TFormData>({
resolver: yupResolver(formSchema),
defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' },
});
const { handleSubmit, register, unregister, setValue, errors } = formInstance;
const {
queryVrf,
families,
formData,
queryType,
availVrfs,
fqdnTarget,
btnLoading,
queryTarget,
isSubmitting,
resolvedOpen,
queryLocation,
displayTarget,
formData,
} = useLGState();
function submitHandler(values: TFormData) {
@ -150,96 +151,81 @@ export const HyperglassForm = () => {
const isFqdnQuery = useMemo(() => {
return ['bgp_route', 'ping', 'traceroute'].includes(queryType.value);
}, [queryType]);
const fqdnQuery = useMemo(() => {
let result = null;
if (fqdnTarget && queryVrf.value === 'default' && fqdnTarget) {
result = fqdnTarget;
}
return result;
}, [queryVrf, queryType]);
}, [queryType.value]);
useEffect(() => {
register({ name: 'query_location', required: true });
register({ name: 'query_type', required: true });
register({ name: 'query_vrf' });
register({ name: 'query_target', required: true });
}, [register]);
Object.keys(errors).length >= 1 && console.error(errors);
return (
<AnimatedForm
p={0}
my={4}
w="100%"
mx="auto"
textAlign="left"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
onSubmit={handleSubmit(submitHandler)}
maxW={{ base: '100%', lg: '75%' }}>
<FormRow>
<FormField
name="query_location"
errors={errors.query_location}
label={web.text.query_location}>
<QueryLocation onChange={handleChange} label={web.text.query_location} />
</FormField>
<FormField
name="query_type"
errors={errors.query_type}
label={web.text.query_type}
labelAddOn={vrfContent && <HelpModal item={vrfContent} name="query_type" />}>
<QueryType onChange={handleChange} label={web.text.query_type} />
</FormField>
</FormRow>
<FormRow>
<If c={availVrfs.length > 1}>
<FormField label={web.text.query_vrf} name="query_vrf" errors={errors.query_vrf}>
<QueryVrf label={web.text.query_vrf} vrfs={availVrfs.value} onChange={handleChange} />
<FormProvider {...formInstance}>
<AnimatedForm
p={0}
my={4}
w="100%"
mx="auto"
textAlign="left"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
maxW={{ base: '100%', lg: '75%' }}
onSubmit={handleSubmit(submitHandler)}>
<FormRow>
<FormField name="query_location" label={web.text.query_location}>
<QueryLocation onChange={handleChange} label={web.text.query_location} />
</FormField>
</If>
<FormField name="query_target" errors={errors.query_target} label={web.text.query_target}>
<If c={queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select'}>
<CommunitySelect
name="query_target"
register={register}
unregister={unregister}
onChange={handleChange}
communities={queries.bgp_community.communities}
/>
<FormField
name="query_type"
label={web.text.query_type}
labelAddOn={vrfContent && <HelpModal item={vrfContent} name="query_type" />}>
<QueryType onChange={handleChange} label={web.text.query_type} />
</FormField>
</FormRow>
<FormRow>
<If c={availVrfs.length > 1}>
<FormField label={web.text.query_vrf} name="query_vrf">
<QueryVrf label={web.text.query_vrf} vrfs={availVrfs.value} onChange={handleChange} />
</FormField>
</If>
<If c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
<QueryTarget
name="query_target"
register={register}
value={queryTarget.value}
unregister={unregister}
setFqdn={fqdnTarget.set}
setTarget={handleChange}
resolveTarget={isFqdnQuery}
displayValue={displayTarget.value}
setDisplayValue={displayTarget.set}
placeholder={web.text.query_target}
/>
</If>
</FormField>
</FormRow>
<FormRow mt={0} justifyContent="flex-end">
<Flex
my={2}
w="100%"
ml="auto"
maxW="100%"
flex="0 0 0"
flexDir="column"
mr={{ base: 0, lg: 2 }}>
<SubmitButton handleChange={handleChange} />
</Flex>
</FormRow>
</AnimatedForm>
<FormField name="query_target" label={web.text.query_target}>
<If c={queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select'}>
<CommunitySelect
name="query_target"
register={register}
unregister={unregister}
onChange={handleChange}
communities={queries.bgp_community.communities}
/>
</If>
<If
c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
<QueryTarget
name="query_target"
register={register}
setTarget={handleChange}
resolveTarget={isFqdnQuery}
placeholder={web.text.query_target}
/>
</If>
</FormField>
</FormRow>
<FormRow mt={0} justifyContent="flex-end">
<Flex
my={2}
w="100%"
ml="auto"
maxW="100%"
flex="0 0 0"
flexDir="column"
mr={{ base: 0, lg: 2 }}>
<SubmitButton handleChange={handleChange} />
</Flex>
</FormRow>
</AnimatedForm>
</FormProvider>
);
};

View file

@ -147,23 +147,16 @@ export const Results = () => {
queryLocation.map((loc, i) => {
const device = getDevice(loc.value);
return (
<motion.div
key={loc.value}
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: 300 }}
transition={{ duration: 0.3, delay: i * 0.3 }}
exit={{ opacity: 0, y: 300 }}>
<Result
index={i}
device={device}
queryLocation={loc.value}
queryVrf={queryVrf.value}
setComplete={setComplete}
queryType={queryType.value}
queryTarget={queryTarget.value}
resultsComplete={resultsComplete}
/>
</motion.div>
<Result
index={i}
device={device}
queryLocation={loc.value}
queryVrf={queryVrf.value}
setComplete={setComplete}
queryType={queryType.value}
queryTarget={queryTarget.value}
resultsComplete={resultsComplete}
/>
);
})}
</AnimatePresence>

View file

@ -9,6 +9,7 @@ import {
AccordionPanel,
AccordionButton,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { BsLightningFill } from '@meronex/icons/bs';
import { startCase } from 'lodash';
import { BGPTable, Countdown, CopyButton, RequeryButton, TextOutput, If } from '~/components';
@ -21,6 +22,8 @@ import { isStackError, isFetchError, isLGError } from './guards';
import type { TAccordionHeaderWrapper, TResult, TErrorLevels } from './types';
const AnimatedAccordionItem = motion.custom(AccordionItem);
const AccordionHeaderWrapper = (props: TAccordionHeaderWrapper) => {
const { hoverBg, ...rest } = props;
return (
@ -143,9 +146,13 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
}, [resultsComplete, index]);
return (
<AccordionItem
<AnimatedAccordionItem
ref={ref}
isDisabled={isLoading}
exit={{ opacity: 0, y: 300 }}
animate={{ opacity: 1, y: 0 }}
initial={{ opacity: 0, y: 300 }}
transition={{ duration: 0.3, delay: index * 0.3 }}
css={{
'&:last-of-type': { borderBottom: 'none' },
'&:first-of-type': { borderTop: 'none' },
@ -234,6 +241,6 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
</Flex>
</Flex>
</AccordionPanel>
</AccordionItem>
</AnimatedAccordionItem>
);
});

View file

@ -26,12 +26,16 @@ export const useSelectContext = () => useContext(SelectContext);
const ReactSelectAsBox = (props: TBoxAsReactSelect) => <Box as={ReactSelect} {...props} />;
export const Select = (props: TSelectBase) => {
const { ctl, options, multi, onSelect, ...rest } = props;
const { ctl, options, multi, onSelect, isError = false, ...rest } = props;
const { isOpen, onOpen, onClose } = useDisclosure();
const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>(() => ({ colorMode, isOpen }), [colorMode, isOpen]);
const selectContext = useMemo<TSelectContext>(() => ({ colorMode, isOpen, isError }), [
colorMode,
isError,
isOpen,
]);
const handleChange = (changed: TSelectOption | TSelectOption[]) => {
if (!Array.isArray(changed)) {

View file

@ -20,7 +20,7 @@ import type {
export const useControlStyle = (base: TStyles, state: TControl): TStyles => {
const { isFocused } = state;
const { colorMode } = useSelectContext();
const { colorMode, isError } = useSelectContext();
const borderHover = useColorValue(
useToken('colors', 'gray.300'),
useToken('colors', 'whiteAlpha.400'),
@ -41,14 +41,18 @@ export const useControlStyle = (base: TStyles, state: TControl): TStyles => {
color,
minHeight,
transition: 'all 0.2s',
borderColor: isFocused ? focusBorder : borderColor,
boxShadow: isFocused ? `0 0 0 1px ${focusBorder}` : undefined,
borderColor: isError ? invalidBorder : isFocused ? focusBorder : borderColor,
boxShadow: isError
? `0 0 0 1px ${invalidBorder}`
: isFocused
? `0 0 0 1px ${focusBorder}`
: undefined,
'&:hover': { borderColor: isFocused ? focusBorder : borderHover },
'&:hover > div > span': { backgroundColor: borderHover },
'&:focus': { borderColor: focusBorder },
'&:focus': { borderColor: isError ? invalidBorder : focusBorder },
'&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` },
};
return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused]);
return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused, isError]);
};
export const useMenuStyle = (base: TStyles, state: TMenu): TStyles => {

View file

@ -24,6 +24,7 @@ export type TBoxAsReactSelect = Omit<IReactSelect, 'isMulti' | 'onSelect' | 'onC
export interface TSelectBase extends TBoxAsReactSelect {
name: string;
multi?: boolean;
isError?: boolean;
options: TOptions;
required?: boolean;
onSelect?: (s: TSelectOption[]) => void;
@ -34,6 +35,7 @@ export interface TSelectBase extends TBoxAsReactSelect {
export interface TSelectContext {
colorMode: 'light' | 'dark';
isOpen: boolean;
isError: boolean;
}
export interface TMultiValueRemoveProps {

View file

@ -1,8 +1,8 @@
import { forwardRef } from 'react';
import { Button, Stack } from '@chakra-ui/react';
import { If } from '~/components';
import { useConfig, useGlobalState } from '~/context';
import { useBooleanValue } from '~/hooks';
import { useConfig } from '~/context';
import { useBooleanValue, useLGState } from '~/hooks';
import { TitleOnly } from './titleOnly';
import { SubtitleOnly } from './subtitleOnly';
import { Logo } from './logo';
@ -47,7 +47,7 @@ export const Title = forwardRef<HTMLButtonElement, TTitle>((props, ref) => {
const { web } = useConfig();
const titleMode = web.text.title_mode;
const { isSubmitting } = useGlobalState();
const { isSubmitting } = useLGState();
const justify = useBooleanValue(
isSubmitting.value,

View file

@ -1,10 +1,12 @@
import { useState } from '@hookstate/core';
import { createState, useState } from '@hookstate/core';
import { Persistence } from '@hookstate/persistence';
import type { TUseGreetingReturn } from './types';
const greeting = createState<boolean>(false);
export function useGreeting(): TUseGreetingReturn {
const state = useState<boolean>(false);
const state = useState<boolean>(greeting);
if (typeof window !== 'undefined') {
state.attach(Persistence('hyperglass-greeting'));
}
@ -13,7 +15,6 @@ export function useGreeting(): TUseGreetingReturn {
if (!state.get()) {
state.set(v);
}
return;
}
return [state.value, setAck];

View file

@ -4,6 +4,7 @@ import type { State } from '@hookstate/core';
import type { Families, TDeviceVrf, TQueryTypes, TFormData } from '~/types';
type TLGState = {
isSubmitting: boolean;
queryVrf: string;
families: Families;
queryTarget: string;
@ -20,9 +21,11 @@ type TLGState = {
type TLGStateHandlers = {
resolvedOpen(): void;
resolvedClose(): void;
resetForm(): void;
};
const LGState = createState<TLGState>({
isSubmitting: false,
resolvedIsOpen: false,
displayTarget: '',
queryLocation: [],
@ -44,6 +47,20 @@ export function useLGState(): State<TLGState> & TLGStateHandlers {
function resolvedClose() {
state.resolvedIsOpen.set(false);
}
function resetForm() {
state.merge({
queryVrf: '',
families: [],
queryType: '',
queryTarget: '',
fqdnTarget: null,
queryLocation: [],
displayTarget: '',
resolvedIsOpen: false,
btnLoading: false,
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
});
}
return { resolvedOpen, resolvedClose, ...state };
return { resetForm, resolvedOpen, resolvedClose, ...state };
}

View file

@ -19,6 +19,7 @@
"@chakra-ui/react": "^1.0.3",
"@emotion/react": "^11.1.1",
"@emotion/styled": "^11.0.0",
"@hookform/resolvers": "^1.2.0",
"@hookstate/core": "^3.0.1",
"@hookstate/persistence": "^3.0.0",
"@meronex/icons": "^4.0.0",
@ -34,7 +35,7 @@
"react-countdown": "^2.2.1",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-hook-form": "^5.7",
"react-hook-form": "^6.13.1",
"react-markdown": "^4.3.1",
"react-query": "^2.26.4",
"react-select": "^3.1.1",

View file

@ -906,6 +906,11 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
"@hookform/resolvers@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-1.2.0.tgz#3a429b4bd3ec9981764fcdcc2491202f9f72d847"
integrity sha512-YCKEj/3Kdo3uNt+zrWKV8txaiuATtvgHyz+KYmun3n5JDjxdI0HcVQgfcmJabmkBXBzKuIIrYfxaV8sRuAPZ8w==
"@hookstate/core@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@hookstate/core/-/core-3.0.1.tgz#dc46ea71e3bf0ab5c2dc024029c9210ed12fbb84"
@ -5487,10 +5492,10 @@ react-focus-lock@2.4.1:
use-callback-ref "^1.2.1"
use-sidecar "^1.0.1"
react-hook-form@^5.7:
version "5.7.2"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-5.7.2.tgz#a84e259e5d37dd30949af4f79c4dac31101b79ac"
integrity sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg==
react-hook-form@^6.13.1:
version "6.13.1"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.13.1.tgz#b9c0aa61f746db8169ed5e1050de21cacb1947d6"
integrity sha512-Q0N7MYcbA8SigYufb02h9z97ZKCpIbe62rywOTPsK4Ntvh6fRTGDXSuzWuRhLHhArLoWbGrWYSNSS4tlb+OFXg==
react-input-autosize@^2.2.2:
version "2.2.2"