1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 08:48:05 +00:00

continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads 2020-12-17 16:37:36 -07:00
parent 0bb19082a0
commit 1fb1dfe182
34 changed files with 363 additions and 361 deletions

View file

@ -125,6 +125,7 @@ class Text(HyperglassModel):
query_target: StrictStr = "Target"
query_vrf: StrictStr = "Routing Table"
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
fqdn_message: StrictStr = "Your browser has resolved {fqdn} to" # Formatted by Javascript
cache_prefix: StrictStr = "Results cached for "
cache_icon: StrictStr = "Cached from {time} UTC" # Formatted by Javascript
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript

View file

@ -82,6 +82,6 @@ export const TableData = (props: TTableData) => {
};
export const Paragraph = (props: TextProps) => <ChakraText {...props} />;
export const InlineCode = (props: CodeProps) => <ChakraCode {...props} />;
export const InlineCode = (props: CodeProps) => <ChakraCode children={props.children} />;
export const Divider = (props: DividerProps) => <ChakraDivider {...props} />;
export const Table = (props: BoxProps) => <ChakraTable {...props} />;

View file

@ -14,8 +14,9 @@ export interface TCheckbox extends CheckboxProps {
checked: boolean;
}
export interface TListItem extends ListItemProps {
export interface TListItem {
checked: boolean;
children?: React.ReactNode;
}
export interface TList extends ListProps {

View file

@ -1,4 +1,3 @@
import * as React from 'react';
import dynamic from 'next/dynamic';
import { Flex, Icon, Text } from '@chakra-ui/react';
import { usePagination, useSortBy, useTable } from 'react-table';
@ -12,7 +11,8 @@ import { TableBody } from './body';
import { TableIconButton } from './button';
import { TableSelectShow } from './pageSelect';
import type { TableOptions, PluginHook, Row } from 'react-table';
import type { TableOptions, PluginHook } from 'react-table';
import type { TCellRender } from '~/types';
import type { TTable } from './types';
const ChevronRight = dynamic<MeronexIcon>(() =>
@ -39,7 +39,7 @@ export function Table(props: TTable) {
data,
columns,
heading,
cellRender,
Cell,
rowHighlightBg,
striped = false,
rowHighlightProp,
@ -132,13 +132,17 @@ export function Table(props: TTable) {
highlight={row.values[rowHighlightProp ?? ''] ?? false}
{...row.getRowProps()}>
{row.cells.map((cell, i) => {
const { column, row, value } = cell as TCellRender;
return (
<TableCell
align={cell.column.align}
bordersVertical={[bordersVertical, i]}
{...cell.getCellProps()}>
{/* {cellRender ?? cell.render('Cell')} */}
{React.createElement(cellRender, cell)}
{typeof Cell !== 'undefined' ? (
<Cell column={column} row={row} value={value} />
) : (
cell.render('Cell')
)}
</TableCell>
);
})}

View file

@ -1,6 +1,6 @@
import type { BoxProps, IconButtonProps } from '@chakra-ui/react';
import type { Colors, TColumn } from '~/types';
import type { Colors, TColumn, TCellRender } from '~/types';
export interface TTable {
columns: TColumn[];
@ -9,7 +9,7 @@ export interface TTable {
striped?: boolean;
bordersVertical?: boolean;
bordersHorizontal?: boolean;
cellRender?: React.ReactNode;
Cell?: React.FC<TCellRender>;
rowHighlightProp?: keyof IRoute;
rowHighlightBg?: keyof Colors;
}

View file

@ -1,6 +1,5 @@
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));
@ -18,15 +17,9 @@ export const CopyButton = (props: TCopyButton) => {
size="sm"
variant="ghost"
onClick={onCopy}
zIndex="dropdown"
colorScheme="secondary"
colorScheme="primary"
{...rest}>
<If c={hasCopied}>
<Icon as={Check} boxSize="16px" />
</If>
<If c={!hasCopied}>
<Icon as={Copy} boxSize="16px" />
</If>
<Icon as={hasCopied ? Check : Copy} boxSize="16px" />
</Button>
</Tooltip>
);

View file

@ -1,3 +1,4 @@
import { forwardRef } from 'react';
import dynamic from 'next/dynamic';
import { Button, Icon, Tooltip } from '@chakra-ui/react';
@ -5,13 +6,23 @@ import type { TRequeryButton } from './types';
const Repeat = dynamic<MeronexIcon>(() => import('@meronex/icons/fi').then(i => i.FiRepeat));
export const RequeryButton = (props: TRequeryButton) => {
const { requery, bg = 'secondary', ...rest } = props;
export const RequeryButton = forwardRef<HTMLButtonElement, TRequeryButton>((props, ref) => {
const { requery, ...rest } = props;
return (
<Tooltip hasArrow label="Reload Query" placement="top">
<Button mx={1} as="a" size="sm" zIndex="1" variantColor={bg} onClick={requery} {...rest}>
<Button
mx={1}
as="a"
ref={ref}
size="sm"
zIndex="1"
variant="ghost"
onClick={requery}
colorScheme="secondary"
{...rest}>
<Icon as={Repeat} boxSize="16px" />
</Button>
</Tooltip>
);
};
});

View file

@ -1,107 +1,51 @@
import { forwardRef } from 'react';
import { Box, Spinner } from '@chakra-ui/react';
import {
IconButton,
Popover,
PopoverTrigger,
PopoverArrow,
PopoverCloseButton,
PopoverBody,
PopoverContent,
} from '@chakra-ui/react';
import { FiSearch } from '@meronex/icons/fi';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import { ResolvedTarget } from '~/components';
import { useLGState } from '~/hooks';
import type { TSubmitButton, TButtonSizeMap } from './types';
import type { TSubmitButton } 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 = (props: TSubmitButton) => {
const { children, handleChange, ...rest } = props;
const { btnLoading, resolvedIsOpen, resolvedClose } = useLGState();
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.400', 'primary.500');
const bgActive = useColorValue('primary.500', 'primary.600');
const bgHover = useColorValue('primary.300', 'primary.400');
const color = useOpposingColor(bg);
const colorActive = useOpposingColor(bgActive);
const colorHover = useOpposingColor(bgHover);
const btnSize = btnSizeMap[size];
function handleClose(): void {
btnLoading.set(false);
resolvedClose();
}
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>
<>
<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>
</>
);
});
};

View file

@ -1,4 +1,5 @@
import type { BoxProps, ButtonProps } from '@chakra-ui/react';
import type { IconButtonProps, ButtonProps } from '@chakra-ui/react';
import type { OnChangeArgs } from '~/types';
export interface TCopyButton extends ButtonProps {
copyValue: string;
@ -7,20 +8,9 @@ export interface TCopyButton extends ButtonProps {
export interface TColorModeToggle extends ButtonProps {
size?: string;
}
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;
export interface TSubmitButton extends Omit<IconButtonProps, 'aria-label'> {
handleChange(e: OnChangeArgs): void;
}
export interface TRequeryButton extends ButtonProps {

View file

@ -15,7 +15,7 @@ function buildOptions(communities: TBGPCommunity[]): TSelectOption[] {
}));
}
const Option = (props: OptionProps<Dict>) => {
const Option = (props: OptionProps<Dict, false>) => {
const { label, data } = props;
return (
<components.Option {...props}>

View file

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { Select } from '~/components';
import { useConfig } from '~/context';
import type { TNetwork, TSelectOptionMulti } from '~/types';
import type { TNetwork, TSelectOption } from '~/types';
import type { TQuerySelectField } from './types';
function buildOptions(networks: TNetwork[]) {
@ -23,12 +23,16 @@ export const QueryLocation = (props: TQuerySelectField) => {
const { networks } = useConfig();
const options = useMemo(() => buildOptions(networks), [networks.length]);
function handleChange(e: TSelectOptionMulti): void {
function handleChange(e: TSelectOption | TSelectOption[]): void {
if (e === null) {
e = [];
} else if (typeof e === 'string') {
e = [e];
}
if (Array.isArray(e)) {
const value = e.map(sel => sel!.value);
onChange({ field: 'query_location', value });
}
const value = e.map(sel => sel.value);
onChange({ field: 'query_location', value });
}
return (

View file

@ -17,8 +17,8 @@ export const QueryType = (props: TQuerySelectField) => {
const options = useMemo(() => buildOptions(queries.list), [queries.list.length]);
function handleChange(e: TSelectOption): void {
if (e !== null) {
function handleChange(e: TSelectOption | TSelectOption[]): void {
if (!Array.isArray(e) && e !== null) {
onChange({ field: 'query_type', value: e.value });
}
}

View file

@ -13,8 +13,8 @@ export const QueryVrf = (props: TQueryVrf) => {
const options = useMemo(() => buildOptions(vrfs), [vrfs.length]);
function handleChange(e: TSelectOption): void {
if (e !== null) {
function handleChange(e: TSelectOption | TSelectOption[]): void {
if (!Array.isArray(e) && e !== null) {
onChange({ field: 'query_vrf', value: e.value });
}
}

View file

@ -1,10 +1,11 @@
import { useEffect } from 'react';
import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip } from '@chakra-ui/react';
import { useEffect, useMemo } from 'react';
import { Button, Stack, Text, VStack } from '@chakra-ui/react';
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import { useStrf } from '~/hooks';
import { FiArrowRightCircle as RightArrow } from '@meronex/icons/fi';
import { useConfig, useColorValue, useGlobalState } from '~/context';
import { useStrf, useLGState } from '~/hooks';
import type { DnsOverHttps, ColorNames } from '~/types';
import type { DnsOverHttps } from '~/types';
import type { TResolvedTarget } from './types';
function findAnswer(data: DnsOverHttps.Response | undefined): string {
@ -17,26 +18,32 @@ function findAnswer(data: DnsOverHttps.Response | undefined): string {
}
export const ResolvedTarget = (props: TResolvedTarget) => {
const { fqdnTarget, setTarget, queryTarget, families, availVrfs } = props;
const { setTarget } = props;
const { web } = useConfig();
const { isSubmitting } = useGlobalState();
const { fqdnTarget, queryTarget, families, formData } = useLGState();
const color = useColorValue('secondary.500', 'secondary.300');
const dnsUrl = web.dns_provider.url;
const query4 = Array.from(families).includes(4);
const query6 = Array.from(families).includes(6);
const query4 = Array.from(families.value).includes(4);
const query6 = Array.from(families.value).includes(6);
const tooltip4 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv4' });
const tooltip6 = useStrf(web.text.fqdn_tooltip, { protocol: 'IPv6' });
const [messageStart, messageEnd] = useMemo(() => web.text.fqdn_message.split('{fqdn}'), [
web.text.fqdn_message,
]);
const { data: data4, isLoading: isLoading4, isError: isError4 } = useQuery(
[fqdnTarget, 4],
[fqdnTarget.value, 4],
dnsQuery,
);
const { data: data6, isLoading: isLoading6, isError: isError6 } = useQuery(
[fqdnTarget, 6],
[fqdnTarget.value, 6],
dnsQuery,
);
async function dnsQuery(
target: string,
family: 4 | 6,
@ -58,13 +65,9 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
function handleOverride(value: string): void {
setTarget({ field: 'query_target', value });
}
function isSelected(value: string): ColorNames {
if (value === queryTarget) {
return 'success';
} else {
return 'secondary';
}
function selectTarget(value: string): void {
formData.set(p => ({ ...p, query_target: value }));
isSubmitting.set(true);
}
useEffect(() => {
@ -78,69 +81,42 @@ export const ResolvedTarget = (props: TResolvedTarget) => {
}, [data4, data6]);
return (
<Stack
isInline
w="100%"
justifyContent={
query4 && data4?.Answer && query6 && data6?.Answer && availVrfs.length > 1
? 'space-between'
: 'flex-end'
}
flexWrap="wrap">
{isLoading4 ||
isError4 ||
(query4 && findAnswer(data4) && (
<Tag my={2}>
<Tooltip hasArrow label={tooltip4} placement="bottom">
<Button
px={2}
mr={2}
py="0.1rem"
minW="unset"
fontSize="xs"
height="unset"
borderRadius="md"
colorScheme={isSelected(findAnswer(data4))}
onClick={() => handleOverride(findAnswer(data4))}>
IPv4
</Button>
</Tooltip>
{isLoading4 && <Spinner />}
{isError4 && <Icon name="warning" />}
{findAnswer(data4) && (
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
{findAnswer(data4)}
</Text>
)}
</Tag>
))}
{isLoading6 ||
isError6 ||
(query6 && findAnswer(data6) && (
<Tag my={2}>
<Tooltip hasArrow label={tooltip6} placement="bottom">
<Button
px={2}
mr={2}
py="0.1rem"
minW="unset"
fontSize="xs"
height="unset"
borderRadius="md"
colorScheme={isSelected(findAnswer(data6))}
onClick={() => handleOverride(findAnswer(data6))}>
IPv6
</Button>
</Tooltip>
{isLoading6 && <Spinner />}
{isError6 && <Icon name="warning" />}
{findAnswer(data6) && (
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
{findAnswer(data6)}
</Text>
)}
</Tag>
))}
</Stack>
<VStack w="100%" spacing={4} justify="center">
<Text fontSize="sm" textAlign="center">
{messageStart}
<Text as="span" fontSize="sm" fontWeight="bold" color={color}>
{fqdnTarget.value}
</Text>
{messageEnd}
</Text>
<Stack spacing={2}>
{!isLoading4 && !isError4 && query4 && findAnswer(data4) && (
<Button
size="sm"
fontSize="xs"
colorScheme="primary"
justifyContent="space-between"
rightIcon={<RightArrow size="18px" />}
title={tooltip4}
fontFamily="mono"
onClick={() => selectTarget(findAnswer(data4))}>
{findAnswer(data4)}
</Button>
)}
{!isLoading6 && !isError6 && query6 && findAnswer(data6) && (
<Button
size="sm"
fontSize="xs"
colorScheme="secondary"
justifyContent="space-between"
rightIcon={<RightArrow size="18px" />}
title={tooltip6}
fontFamily="mono"
onClick={() => selectTarget(findAnswer(data6))}>
{findAnswer(data6)}
</Button>
)}
</Stack>
</VStack>
);
};

View file

@ -1,6 +1,6 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError, Control } from 'react-hook-form';
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, Families, TFormData } from '~/types';
import type { TDeviceVrf, TBGPCommunity, OnChangeArgs, TFormData } from '~/types';
export interface TField extends FormControlProps {
name: string;
@ -59,9 +59,5 @@ export interface TQueryTarget {
}
export interface TResolvedTarget {
families: Families;
queryTarget: string;
availVrfs: TDeviceVrf[];
fqdnTarget: string | null;
setTarget(e: OnChangeArgs): void;
}

View file

@ -75,7 +75,7 @@ export const Header = (props: THeader) => {
const resetButton = (
<AnimatePresence key="resetButton">
<AnimatedDiv
layoutTransition={headerTransition}
transition={headerTransition}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, width: 'unset' }}
exit={{ opacity: 0, x: -50 }}
@ -94,7 +94,7 @@ export const Header = (props: THeader) => {
key="title"
px={1}
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
positionTransition={headerTransition}
transition={headerTransition}
initial={{ scale: 0.5 }}
animate={
isSubmitting && web.text.title_mode === 'text_only'
@ -114,7 +114,7 @@ export const Header = (props: THeader) => {
);
const colorModeToggle = (
<AnimatedDiv
layoutTransition={headerTransition}
transition={headerTransition}
key="colorModeToggle"
alignItems="center"
initial={{ opacity: 0 }}

View file

@ -1,12 +1,13 @@
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, formData } = useGlobalState();
const { isSubmitting } = useGlobalState();
const { formData } = useLGState();
return (
<Frame>
<If
@ -19,12 +20,7 @@ export const Layout: React.FC = () => {
formData.query_vrf.value,
)
}>
<Results
queryLocation={formData.query_location.value}
queryType={formData.query_type.value}
queryVrf={formData.query_vrf.value}
queryTarget={formData.query_target.value}
/>
<Results />
</If>
<AnimatePresence>
<If c={!isSubmitting.value}>

View file

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { Flex } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { intersectionWith } from 'lodash';
@ -14,19 +14,20 @@ import {
QueryTarget,
SubmitButton,
QueryLocation,
ResolvedTarget,
CommunitySelect,
} from '~/components';
import { useConfig, useGlobalState } from '~/context';
import { useStrf, useGreeting, useDevice } from '~/hooks';
import { useStrf, useGreeting, useDevice, useLGState } from '~/hooks';
import { isQueryType, isString } from '~/types';
import type { Families, TFormData, TDeviceVrf, TQueryTypes, OnChangeArgs } from '~/types';
import type { TFormData, TDeviceVrf, OnChangeArgs } from '~/types';
const fqdnPattern = /^(?!:\/\/)([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-][a-zA-Z0-9-]+\.[a-zA-Z-]{2,6}?$/gim;
export const HyperglassForm = () => {
const { web, content, messages, queries } = useConfig();
const { formData, isSubmitting } = useGlobalState();
const { isSubmitting } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
const getDevice = useDevice();
@ -41,24 +42,34 @@ export const HyperglassForm = () => {
query_target: yup.string().required(noQueryTarget),
});
const { handleSubmit, register, unregister, setValue, errors } = useForm<TFormData>({
const { handleSubmit, register, unregister, setValue, errors, reset } = useForm<TFormData>({
validationSchema: formSchema,
defaultValues: { query_vrf: 'default', query_target: '' },
defaultValues: { query_vrf: 'default', query_target: '', query_location: [], query_type: '' },
});
const [queryLocation, setQueryLocation] = useState<string[]>([]);
const [queryType, setQueryType] = useState<TQueryTypes>('');
const [queryVrf, setQueryVrf] = useState<string>('');
const [queryTarget, setQueryTarget] = useState<string>('');
const [availVrfs, setAvailVrfs] = useState<TDeviceVrf[]>([]);
const [fqdnTarget, setFqdnTarget] = useState<string | null>('');
const [displayTarget, setDisplayTarget] = useState<string>('');
const [families, setFamilies] = useState<Families>([]);
const {
queryVrf,
families,
queryType,
availVrfs,
fqdnTarget,
btnLoading,
queryTarget,
resolvedOpen,
queryLocation,
displayTarget,
formData,
} = useLGState();
function onSubmit(values: TFormData): void {
function submitHandler(values: TFormData) {
if (!greetingAck && web.greeting.required) {
window.location.reload(false);
setGreetingAck(false);
} else if (fqdnPattern.test(values.query_target)) {
btnLoading.set(true);
fqdnTarget.set(values.query_target);
formData.set(values);
resolvedOpen();
} else {
formData.set(values);
isSubmitting.set(true);
@ -68,7 +79,7 @@ export const HyperglassForm = () => {
function handleLocChange(locations: string[]): void {
const allVrfs = [] as TDeviceVrf[][];
setQueryLocation(locations);
queryLocation.set(locations);
// Create an array of each device's VRFs.
for (const loc of locations) {
@ -82,11 +93,14 @@ export const HyperglassForm = () => {
(a: TDeviceVrf, b: TDeviceVrf) => a.id === b.id,
);
setAvailVrfs(intersecting);
availVrfs.set(intersecting);
// If there are no intersecting VRFs, use the default VRF.
if (intersecting.filter(i => i.id === queryVrf).length === 0 && queryVrf !== 'default') {
setQueryVrf('default');
if (
intersecting.filter(i => i.id === queryVrf.value).length === 0 &&
queryVrf.value !== 'default'
) {
queryVrf.set('default');
}
let ipv4 = 0;
@ -104,13 +118,13 @@ export const HyperglassForm = () => {
}
if (ipv4 !== 0 && ipv4 === ipv6) {
setFamilies([4, 6]);
families.set([4, 6]);
} else if (ipv4 > ipv6) {
setFamilies([4]);
families.set([4]);
} else if (ipv4 < ipv6) {
setFamilies([6]);
families.set([6]);
} else {
setFamilies([]);
families.set([]);
}
}
@ -120,35 +134,35 @@ export const HyperglassForm = () => {
if (e.field === 'query_location' && Array.isArray(e.value)) {
handleLocChange(e.value);
} else if (e.field === 'query_type' && isQueryType(e.value)) {
setQueryType(e.value);
queryType.set(e.value);
} else if (e.field === 'query_vrf' && isString(e.value)) {
setQueryVrf(e.value);
queryVrf.set(e.value);
} else if (e.field === 'query_target' && isString(e.value)) {
setQueryTarget(e.value);
queryTarget.set(e.value);
}
}
const vrfContent = useMemo(() => {
if (Object.keys(content.vrf).includes(queryVrf) && queryType !== '') {
return content.vrf[queryVrf][queryType];
if (Object.keys(content.vrf).includes(queryVrf.value) && queryType.value !== '') {
return content.vrf[queryVrf.value][queryType.value];
}
}, [queryVrf]);
const isFqdnQuery = useMemo(() => {
return ['bgp_route', 'ping', 'traceroute'].includes(queryType);
return ['bgp_route', 'ping', 'traceroute'].includes(queryType.value);
}, [queryType]);
const fqdnQuery = useMemo(() => {
let result = null;
if (fqdnTarget && queryVrf === 'default' && fqdnTarget) {
if (fqdnTarget && queryVrf.value === 'default' && fqdnTarget) {
result = fqdnTarget;
}
return result;
}, [queryVrf, queryType]);
useEffect(() => {
register({ name: 'query_location' });
register({ name: 'query_type' });
register({ name: 'query_location', required: true });
register({ name: 'query_type', required: true });
register({ name: 'query_vrf' });
}, [register]);
@ -165,7 +179,7 @@ export const HyperglassForm = () => {
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
onSubmit={handleSubmit(onSubmit)}
onSubmit={handleSubmit(submitHandler)}
maxW={{ base: '100%', lg: '75%' }}>
<FormRow>
<FormField
@ -185,26 +199,11 @@ export const HyperglassForm = () => {
<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} onChange={handleChange} />
<QueryVrf label={web.text.query_vrf} vrfs={availVrfs.value} onChange={handleChange} />
</FormField>
</If>
<FormField
name="query_target"
errors={errors.query_target}
label={web.text.query_target}
fieldAddOn={
queryLocation.length !== 0 &&
fqdnQuery !== null && (
<ResolvedTarget
families={families}
availVrfs={availVrfs}
fqdnTarget={fqdnQuery}
setTarget={handleChange}
queryTarget={queryTarget}
/>
)
}>
<If c={queryType === 'bgp_community' && queries.bgp_community.mode === 'select'}>
<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}
@ -213,17 +212,17 @@ export const HyperglassForm = () => {
communities={queries.bgp_community.communities}
/>
</If>
<If c={!(queryType === 'bgp_community' && queries.bgp_community.mode === 'select')}>
<If c={!(queryType.value === 'bgp_community' && queries.bgp_community.mode === 'select')}>
<QueryTarget
name="query_target"
register={register}
value={queryTarget}
value={queryTarget.value}
unregister={unregister}
setFqdn={setFqdnTarget}
setFqdn={fqdnTarget.set}
setTarget={handleChange}
resolveTarget={isFqdnQuery}
displayValue={displayTarget}
setDisplayValue={setDisplayTarget}
displayValue={displayTarget.value}
setDisplayValue={displayTarget.set}
placeholder={web.text.query_target}
/>
</If>
@ -238,7 +237,7 @@ export const HyperglassForm = () => {
flex="0 0 0"
flexDir="column"
mr={{ base: 0, lg: 2 }}>
<SubmitButton isLoading={isSubmitting.value} />
<SubmitButton handleChange={handleChange} />
</Flex>
</FormRow>
</AnimatedForm>

View file

@ -2,14 +2,15 @@ import { forwardRef } from 'react';
import { Icon, Text, Box, Tooltip, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { CgMoreO as More } from '@meronex/icons/cg';
import { BisError as Warning } from '@meronex/icons/bi';
import { MdNotInterested as NotAllowed, MdLastPage } from '@meronex/icons/md';
import { MdNotInterested as NotAllowed } from '@meronex/icons/md';
import { RiHome2Fill as End } from '@meronex/icons/ri';
import { BsQuestionCircleFill as Question } from '@meronex/icons/bs';
import { FaCheckCircle as Check, FaChevronRight as ChevronRight } from '@meronex/icons/fa';
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 { useConfig, useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type {
@ -85,7 +86,7 @@ export const ASPath = (props: TASPath) => {
);
if (path.length === 0) {
return <Icon as={MdLastPage} />;
return <Icon as={End} />;
}
let paths = [] as JSX.Element[];
@ -108,7 +109,8 @@ export const ASPath = (props: TASPath) => {
export const Communities = (props: TCommunities) => {
const { communities } = props;
const color = useColorValue('black', 'white');
const bg = useColorValue('white', 'gray.900');
const color = useOpposingColor(bg);
return (
<>
<If c={communities.length === 0}>
@ -123,6 +125,7 @@ export const Communities = (props: TCommunities) => {
</MenuButton>
<MenuList
p={3}
bg={bg}
width="unset"
color={color}
textAlign="left"

View file

@ -3,8 +3,7 @@ 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 { TColumn, TParsedDataField, TCellRender } from '~/types';
import type { TBGPTable } from './types';
function makeColumns(fields: TParsedDataField[]): TColumn[] {
@ -37,7 +36,7 @@ export const BGPTable = (props: TBGPTable) => {
columns={columns}
data={data.routes}
rowHighlightProp="active"
cellRender={(d: CellProps<TRouteField>) => <Cell data={d} rawData={data} />}
Cell={(d: TCellRender) => <Cell data={d} rawData={data} />}
bordersHorizontal
rowHighlightBg="green"
/>

View file

@ -1,5 +1,5 @@
import type { BoxProps, FlexProps, TextProps } from '@chakra-ui/react';
import type { CellProps } from 'react-table';
import type { TCellRender } from '~/types';
export interface TTextOutput extends Omit<BoxProps, 'children'> {
children: string;
@ -41,7 +41,7 @@ export interface TRPKIState {
}
export interface TCell {
data: CellProps<TRouteField>;
data: TCellRender;
rawData: TStructuredResponse;
}

View file

@ -3,15 +3,19 @@ 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 { useDevice } from '~/hooks';
import { useDevice, useLGState } from '~/hooks';
import { isQueryType } from '~/types';
import { Result } from './individual';
import type { TResults } from './types';
export const Results = (props: TResults) => {
const { queryLocation, queryType, queryVrf, queryTarget, ...rest } = props;
const { request_timeout, queries, vrfs, web } = useConfig();
export const Results = () => {
const { queries, vrfs, web } = useConfig();
const { formData } = useLGState();
const {
query_location: queryLocation,
query_target: queryTarget,
query_type: queryType,
query_vrf: queryVrf,
} = formData;
const getDevice = useDevice();
const targetBg = useToken('colors', 'teal.600');
const queryBg = useToken('colors', 'cyan.500');
@ -62,11 +66,11 @@ export const Results = (props: TResults) => {
const [resultsComplete, setComplete] = useState<number | null>(null);
const matchedVrf =
vrfs.filter(v => v.id === queryVrf)[0] ?? vrfs.filter(v => v.id === 'default')[0];
vrfs.filter(v => v.id === queryVrf.value)[0] ?? vrfs.filter(v => v.id === 'default')[0];
let queryTypeLabel = '';
if (isQueryType(queryType)) {
queryTypeLabel = queries[queryType].display_name;
if (isQueryType(queryType.value)) {
queryTypeLabel = queries[queryType.value].display_name;
}
return (
@ -77,8 +81,7 @@ export const Results = (props: TResults) => {
w="100%"
mx="auto"
textAlign="left"
maxW={{ base: '100%', lg: '75%', xl: '50%' }}
{...rest}>
maxW={{ base: '100%', lg: '75%', xl: '50%' }}>
<Stack isInline align="center" justify="center" mt={4} flexWrap="wrap">
<AnimatePresence>
{queryLocation && (
@ -102,7 +105,7 @@ export const Results = (props: TResults) => {
transition={{ duration: 0.3, delay: 0.3 }}>
<Label
bg={targetBg}
value={queryTarget}
value={queryTarget.value}
label={web.text.query_target}
fontSize={{ base: 'xs', md: 'sm' }}
/>
@ -138,27 +141,26 @@ export const Results = (props: TResults) => {
transition={{ duration: 0.3 }}
animate={{ opacity: 1, y: 0 }}
maxW={{ base: '100%', md: '75%' }}>
<Accordion allowMultiple>
<Accordion allowMultiple allowToggle>
<AnimatePresence>
{queryLocation &&
queryLocation.map((loc, i) => {
const device = getDevice(loc);
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
key={loc}
index={i}
device={device}
queryLocation={loc}
queryVrf={queryVrf}
queryType={queryType}
queryTarget={queryTarget}
queryLocation={loc.value}
queryVrf={queryVrf.value}
setComplete={setComplete}
timeout={request_timeout * 1000}
queryType={queryType.value}
queryTarget={queryTarget.value}
resultsComplete={resultsComplete}
/>
</motion.div>

View file

@ -145,7 +145,6 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
return (
<AccordionItem
ref={ref}
isOpen={isOpen}
isDisabled={isLoading}
css={{
'&:last-of-type': { borderBottom: 'none' },
@ -170,7 +169,7 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
</AccordionButton>
<ButtonGroup px={[1, 1, 3, 3]} py={2}>
<CopyButton copyValue={copyValue} isDisabled={isLoading} />
<RequeryButton requery={refetch} variant="ghost" isDisabled={isLoading} />
<RequeryButton requery={refetch} isDisabled={isLoading} />
</ButtonGroup>
</AccordionHeaderWrapper>
<AccordionPanel
@ -200,11 +199,12 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
) : isStringOutput(data) && data.level === 'success' && !tableComponent ? (
<TextOutput>{data.output}</TextOutput>
) : isStringOutput(data) && data.level !== 'success' ? (
<FormattedError message={data.output} keywords={errorKeywords} />
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError message={data.output} keywords={errorKeywords} />
</Alert>
) : null}
</>
)}
{isError && <Alert rounded="lg" my={2} py={4} status={errorLevel}></Alert>}
</Flex>
</Box>
@ -214,7 +214,7 @@ export const Result = forwardRef<HTMLDivElement, TResult>((props, ref) => {
mt={2}
justifyContent={['flex-start', 'flex-start', 'flex-end', 'flex-end']}
flex="1 0 auto">
<If c={cache.show_text && data && !error}>
<If c={cache.show_text && typeof data !== 'undefined' && !error}>
<If c={!isMobile}>
<Countdown timeout={cache.timeout} text={web.text.cache_prefix} />
</If>

View file

@ -1,5 +1,5 @@
import type { BoxProps, FlexProps } from '@chakra-ui/react';
import type { TDevice, TQueryTypes, TFormState } from '~/types';
import type { FlexProps } from '@chakra-ui/react';
import type { TDevice, TQueryTypes } from '~/types';
export interface TResultHeader {
title: string;
@ -26,10 +26,8 @@ export interface TResult {
queryType: TQueryTypes;
queryTarget: string;
setComplete(v: number | null): void;
queryLocation: string[];
queryLocation: string;
resultsComplete: number | null;
}
export type TResults = TFormState & BoxProps;
export type TErrorLevels = 'success' | 'warning' | 'error';

View file

@ -10,7 +10,7 @@ import type {
PlaceholderProps,
} from 'react-select';
import type { BoxProps } from '@chakra-ui/react';
import type { ColorNames, TSelectOption, TSelectOptionGroup } from '~/types';
import type { ColorNames, TSelectOption, TSelectOptionMulti, TSelectOptionGroup } from '~/types';
export interface TSelectState {
[k: string]: string[];
@ -27,7 +27,7 @@ export interface TSelectBase extends TBoxAsReactSelect {
options: TOptions;
required?: boolean;
onSelect?: (s: TSelectOption[]) => void;
onChange?: (c: TSelectOption) => void;
onChange?: (c: TSelectOption | TSelectOptionMulti) => void;
colorScheme?: ColorNames;
}
@ -52,19 +52,19 @@ export interface TRSTheme extends Omit<Theme, 'borderRadius'> {
borderRadius: string | number;
}
export type TControl = ControlProps<TOptions>;
export type TControl = ControlProps<TOptions, false>;
export type TMenu = MenuProps<TOptions>;
export type TMenu = MenuProps<TOptions, false>;
export type TMenuList = MenuListComponentProps<TOptions>;
export type TMenuList = MenuListComponentProps<TOptions, false>;
export type TOption = OptionProps<TOptions>;
export type TOption = OptionProps<TOptions, false>;
export type TMultiValueState = MultiValueProps<TOptions>;
export type TIndicator = IndicatorProps<TOptions>;
export type TIndicator = IndicatorProps<TOptions, false>;
export type TPlaceholder = PlaceholderProps<TOptions>;
export type TPlaceholder = PlaceholderProps<TOptions, false>;
export type TMultiValue = Pick<TSelectContext, 'colorMode'>;

View file

@ -11,13 +11,13 @@ import type { TTitle, TTextOnly } from './types';
const TextOnly = (props: TTextOnly) => {
const { showSubtitle, ...rest } = props;
const textAlign = useBooleanValue(
showSubtitle,
{ base: 'right', md: 'center' },
{ base: 'left', md: 'center' },
);
return (
<Stack spacing={2} maxW="100%" textAlign={textAlign} {...rest}>
<Stack
spacing={2}
maxW="100%"
textAlign={showSubtitle ? ['right', 'center'] : ['left', 'center']}
{...rest}>
<TitleOnly showSubtitle={showSubtitle} />
<If c={showSubtitle}>
<SubtitleOnly />

View file

@ -1,5 +1,5 @@
import { createState, useState } from '@hookstate/core';
import type { TGlobalState } from './types';
import type { TGlobalState, TUseGlobalState } from './types';
// const StateContext = createContext(null);
@ -26,8 +26,23 @@ import type { TGlobalState } from './types';
// export const useHyperglassState = () => useContext(StateContext);
export const globalState = createState<TGlobalState>({
const defaultFormData = {
query_location: [],
query_target: '',
query_type: '',
query_vrf: '',
} as TGlobalState['formData'];
const globalState = createState<TGlobalState>({
isSubmitting: false,
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
formData: defaultFormData,
});
export const useGlobalState = () => useState(globalState);
export function useGlobalState(): TUseGlobalState {
const state = useState<TGlobalState>(globalState);
function resetForm(): void {
state.formData.set(defaultFormData);
state.isSubmitting.set(false);
}
return { resetForm, ...state };
}

View file

@ -1,3 +1,4 @@
import type { State } from '@hookstate/core';
import type { IConfig, TFormData } from '~/types';
export interface THyperglassProvider {
@ -9,3 +10,9 @@ export interface TGlobalState {
isSubmitting: boolean;
formData: TFormData;
}
interface TGlobalStateFunctions {
resetForm(): void;
}
export type TUseGlobalState = State<TGlobalState> & TGlobalStateFunctions;

View file

@ -6,3 +6,4 @@ export * from './useOpposingColor';
export * from './useSessionStorage';
export * from './useStrf';
export * from './useTableToString';
export * from './useLGState';

View file

@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
import { useConfig } from '~/context';
import type { TFormState } from '~/types';
import type { TFormQuery } from '~/types';
/**
* Fetch Wrapper that incorporates a timeout via a passed AbortController instance.
@ -30,11 +30,11 @@ export async function fetchWithTimeout(
return await fetch(uri, config);
}
export function useLGQuery(query: TFormState) {
export function useLGQuery(query: TFormQuery) {
const { request_timeout } = useConfig();
const controller = new AbortController();
async function runQuery(url: string, requestData: TFormState): Promise<TQueryResponse> {
async function runQuery(url: string, requestData: TFormQuery): Promise<TQueryResponse> {
const { queryLocation, queryTarget, queryType, queryVrf } = requestData;
const res = await fetchWithTimeout(
url,

View file

@ -0,0 +1,49 @@
import { useState, createState } from '@hookstate/core';
import type { State } from '@hookstate/core';
import type { Families, TDeviceVrf, TQueryTypes, TFormData } from '~/types';
type TLGState = {
queryVrf: string;
families: Families;
queryTarget: string;
btnLoading: boolean;
displayTarget: string;
queryType: TQueryTypes;
queryLocation: string[];
availVrfs: TDeviceVrf[];
resolvedIsOpen: boolean;
fqdnTarget: string | null;
formData: TFormData;
};
type TLGStateHandlers = {
resolvedOpen(): void;
resolvedClose(): void;
};
const LGState = createState<TLGState>({
resolvedIsOpen: false,
displayTarget: '',
queryLocation: [],
btnLoading: false,
fqdnTarget: null,
queryTarget: '',
queryType: '',
availVrfs: [],
queryVrf: '',
families: [],
formData: { query_location: [], query_target: '', query_type: '', query_vrf: '' },
});
export function useLGState(): State<TLGState> & TLGStateHandlers {
const state = useState<TLGState>(LGState);
function resolvedOpen() {
state.resolvedIsOpen.set(true);
}
function resolvedClose() {
state.resolvedIsOpen.set(false);
}
return { resolvedOpen, resolvedClose, ...state };
}

View file

@ -35,6 +35,7 @@ export interface IConfigWebText {
query_target: string;
query_vrf: string;
fqdn_tooltip: string;
fqdn_message: string;
cache_prefix: string;
cache_icon: string;
complete_time: string;

View file

@ -15,6 +15,10 @@ export interface TFormState {
queryTarget: string;
}
export interface TFormQuery extends Omit<TFormState, 'queryLocation'> {
queryLocation: string;
}
export interface TStringTableData extends Omit<TQueryResponse, 'output'> {
output: TStructuredResponse;
}

View file

@ -1,6 +1,14 @@
import type { CellProps } from 'react-table';
export interface TColumn {
Header: string;
accessor: keyof TRoute;
align: string;
hidden: boolean;
}
export type TCellRender = {
column: CellProps<TRouteField>['column'];
row: CellProps<TRouteField>['row'];
value: CellProps<TRouteField>['value'];
};